635 lines
27 KiB
Python
635 lines
27 KiB
Python
"""
|
||
weather.py — головной модуль генератора погоды ACKS II.
|
||
|
||
Использует классификацию климата Кёппена (Köppen) + сезон для генерации
|
||
случайной погоды на основе бросков 2d6 с модификаторами.
|
||
|
||
Экспортирует функцию generate_weather(constraints), которую вызывает тул с фронта.
|
||
|
||
constraints — список объектов вида:
|
||
{
|
||
"koppen": "Cfb", # Köppen-код климата (обязательно)
|
||
"season": "Winter", # Winter / Spring / Summer / Fall (обязательно)
|
||
"count": 3, # сколько дней сгенерировать (default 1)
|
||
}
|
||
|
||
Возвращает строку в формате Markdown.
|
||
"""
|
||
|
||
import random
|
||
from typing import Any
|
||
from typing_extensions import TypedDict
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Единая температурная таблица [-7 … 19]
|
||
# Устраняет скачок между Temp_cold и Temp_hot:
|
||
# оба конца шкалы теперь в одном непрерывном словаре.
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
TEMP_TABLE: dict[int, str] = {
|
||
-7: "Frigid (-75 °F или ниже)",
|
||
-6: "Frigid (-65 до -74 °F)",
|
||
-5: "Frigid (-55 до -64 °F)",
|
||
-4: "Frigid (-50 до -54 °F)",
|
||
-3: "Frigid (-40 до -49 °F)",
|
||
-2: "Frigid (-25 до -39 °F)",
|
||
-1: "Frigid (-12 до -24 °F)",
|
||
0: "Frigid (-6 до -11 °F)",
|
||
1: "Frigid (-5 до 0 °F)",
|
||
2: "Cold (6–15 °F)",
|
||
3: "Cold (16–25 °F)",
|
||
4: "Cold (26–32 °F)",
|
||
5: "Very Chilly (33–36 °F)",
|
||
6: "Very Chilly (37–40 °F)",
|
||
7: "Chilly (41–50 °F)",
|
||
8: "Chilly (51–55 °F)",
|
||
9: "Brisk (56–60 °F)",
|
||
10: "Brisk (61–65 °F)",
|
||
11: "Balmy (66–68 °F)",
|
||
12: "Balmy (69–72 °F)",
|
||
13: "Warm (73–76 °F)",
|
||
14: "Warm (77–80 °F)",
|
||
15: "Hot (81–86 °F)",
|
||
16: "Hot (87–92 °F)",
|
||
17: "Sweltering (93–100 °F)",
|
||
18: "Sweltering (101–110 °F)",
|
||
19: "Sweltering (111 °F или выше)",
|
||
}
|
||
|
||
# Порог температурного индекса, ниже которого осадки — снег/метель,
|
||
# а не дождь. Индекс 4 = Cold (26–32 °F) — около точки замерзания.
|
||
SNOW_THRESHOLD = 4
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Таблица осадков [-7 … 19]
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
PRECIPITATION_TABLE: dict[int, str] = {
|
||
-7: "Sunbaked",
|
||
-6: "Sunbaked",
|
||
-5: "Sunbaked",
|
||
-4: "Sunbaked",
|
||
-3: "Sunbaked",
|
||
-2: "Sunbaked",
|
||
-1: "Clear",
|
||
0: "Clear",
|
||
1: "Clear",
|
||
2: "Clear",
|
||
3: "Clear",
|
||
4: "Partly Cloudy",
|
||
5: "Mostly Cloudy",
|
||
6: "Overcast",
|
||
7: "Drizzly",
|
||
8: "Drizzly",
|
||
9: "Drizzly",
|
||
10: "Rainy",
|
||
11: "Rainy",
|
||
12: "Rainy",
|
||
13: "Rainy",
|
||
14: "Rainy",
|
||
15: "Rainy",
|
||
16: "Rainy",
|
||
17: "Rainy",
|
||
18: "Rainy",
|
||
19: "Rainy",
|
||
}
|
||
|
||
# Соответствие «тёплых» осадков → «зимним» при t_index < SNOW_THRESHOLD.
|
||
# Drizzly → Flurries (снежная крупа), Rainy → Snowy (снег/метель).
|
||
PRECIP_COLD_REMAP: dict[str, str] = {
|
||
"Drizzly": "Flurries (снежная крупа)",
|
||
"Rainy": "Snowy / Blizzard (снег / метель)",
|
||
}
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Таблица ветра [-7 … 19]
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
WINDS_TABLE: dict[int, str] = {
|
||
-7: "Still",
|
||
-6: "Still",
|
||
-5: "Still",
|
||
-4: "Still",
|
||
-3: "Still",
|
||
-2: "Still",
|
||
-1: "Still",
|
||
0: "Still",
|
||
1: "Still",
|
||
2: "Still",
|
||
3: "Still",
|
||
4: "Still",
|
||
5: "Gentle",
|
||
6: "Gentle",
|
||
7: "Moderate",
|
||
8: "Moderate",
|
||
9: "Moderate",
|
||
10: "Strong",
|
||
11: "Strong",
|
||
12: "Windy",
|
||
13: "Windy",
|
||
14: "Stormy",
|
||
15: "Stormy",
|
||
16: "Stormy",
|
||
17: "Stormy",
|
||
18: "Stormy",
|
||
19: "Stormy",
|
||
}
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Климатические модификаторы (Köppen × Season)
|
||
# td — смещение дневного броска температуры
|
||
# tn — дополнительное смещение ночного броска (поверх td)
|
||
# p — смещение броска осадков
|
||
# w — смещение броска ветра
|
||
#
|
||
# Исправлено: p=−0 → p=0 (Cwc Spring)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
CLIMATE_MODS: dict[str, dict[str, dict[str, int]]] = {
|
||
"Af": {
|
||
"Winter": {"td": +3, "tn": 0, "p": -3, "w": +2},
|
||
"Spring": {"td": +2, "tn": 0, "p": -2, "w": 0},
|
||
"Summer": {"td": +2, "tn": 0, "p": -2, "w": 0},
|
||
"Fall": {"td": +1, "tn": 0, "p": -1, "w": 0},
|
||
},
|
||
"Am": {
|
||
"Winter": {"td": +2, "tn": 0, "p": -1, "w": +2},
|
||
"Spring": {"td": +7, "tn": 0, "p": -1, "w": 0},
|
||
"Summer": {"td": +6, "tn": 0, "p": +4, "w": 0},
|
||
"Fall": {"td": +4, "tn": 0, "p": -1, "w": 0},
|
||
},
|
||
"Aw": {
|
||
"Winter": {"td": +5, "tn": 0, "p": -5, "w": +2},
|
||
"Spring": {"td": +4, "tn": 0, "p": -4, "w": 0},
|
||
"Summer": {"td": +3, "tn": 0, "p": -1, "w": 0},
|
||
"Fall": {"td": +3, "tn": 0, "p": -4, "w": 0},
|
||
},
|
||
"As": {
|
||
"Winter": {"td": +4, "tn": 0, "p": -1, "w": +2},
|
||
"Spring": {"td": +4, "tn": 0, "p": -2, "w": 0},
|
||
"Summer": {"td": +4, "tn": 0, "p": -5, "w": 0},
|
||
"Fall": {"td": +4, "tn": 0, "p": -3, "w": 0},
|
||
},
|
||
"BWh": {
|
||
"Winter": {"td": +1, "tn": 0, "p": -5, "w": +2},
|
||
"Spring": {"td": +6, "tn": 0, "p": -5, "w": 0},
|
||
"Summer": {"td": +7, "tn": +2, "p": -5, "w": 0},
|
||
"Fall": {"td": +4, "tn": 0, "p": 0, "w": 0},
|
||
},
|
||
"BWk": {
|
||
"Winter": {"td": 0, "tn": -4, "p": -4, "w": +2},
|
||
"Spring": {"td": 0, "tn": 0, "p": -5, "w": 0},
|
||
"Summer": {"td": +2, "tn": 0, "p": -4, "w": 0},
|
||
"Fall": {"td": 0, "tn": -1, "p": -5, "w": 0},
|
||
},
|
||
"BSh": {
|
||
"Winter": {"td": +6, "tn": 0, "p": -5, "w": +2},
|
||
"Spring": {"td": +7, "tn": +3, "p": -3, "w": 0},
|
||
"Summer": {"td": +6, "tn": +2, "p": -2, "w": 0},
|
||
"Fall": {"td": +6, "tn": +2, "p": -5, "w": 0},
|
||
},
|
||
"BSk": {
|
||
"Winter": {"td": 0, "tn": -1, "p": -4, "w": +2},
|
||
"Spring": {"td": +1, "tn": 0, "p": -4, "w": 0},
|
||
"Summer": {"td": +4, "tn": 0, "p": -5, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -4, "w": 0},
|
||
},
|
||
"Csa": {
|
||
"Winter": {"td": 0, "tn": 0, "p": -4, "w": +2},
|
||
"Spring": {"td": +1, "tn": 0, "p": -4, "w": 0},
|
||
"Summer": {"td": +3, "tn": 0, "p": -4, "w": 0},
|
||
"Fall": {"td": +1, "tn": 0, "p": -3, "w": 0},
|
||
},
|
||
"Csb": {
|
||
"Winter": {"td": 0, "tn": 0, "p": -2, "w": +2},
|
||
"Spring": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
"Summer": {"td": +2, "tn": 0, "p": -4, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -1, "w": 0},
|
||
},
|
||
"Csc": {
|
||
"Winter": {"td": 0, "tn": 0, "p": +2, "w": +2},
|
||
"Spring": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
"Summer": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -2, "w": 0},
|
||
},
|
||
"Cwa": {
|
||
"Winter": {"td": +1, "tn": 0, "p": -3, "w": +2},
|
||
"Spring": {"td": +3, "tn": 0, "p": -2, "w": 0},
|
||
"Summer": {"td": +4, "tn": +2, "p": -1, "w": +2},
|
||
"Fall": {"td": +2, "tn": 0, "p": -3, "w": 0},
|
||
},
|
||
"Cwb": {
|
||
"Winter": {"td": +1, "tn": 0, "p": -3, "w": +2},
|
||
"Spring": {"td": +1, "tn": 0, "p": -2, "w": 0},
|
||
"Summer": {"td": +1, "tn": 0, "p": -1, "w": +2},
|
||
"Fall": {"td": +1, "tn": 0, "p": -5, "w": 0},
|
||
},
|
||
"Cwc": {
|
||
"Winter": {"td": 0, "tn": 0, "p": -3, "w": +2},
|
||
"Spring": {"td": 0, "tn": 0, "p": 0, "w": 0}, # было p=-0
|
||
"Summer": {"td": 0, "tn": 0, "p": +2, "w": +2},
|
||
"Fall": {"td": 0, "tn": 0, "p": -2, "w": 0},
|
||
},
|
||
"Cfa": {
|
||
"Winter": {"td": 0, "tn": 0, "p": -3, "w": +2},
|
||
"Spring": {"td": +2, "tn": 0, "p": -2, "w": 0},
|
||
"Summer": {"td": +4, "tn": +1, "p": -1, "w": 0},
|
||
"Fall": {"td": +1, "tn": 0, "p": -4, "w": 0},
|
||
},
|
||
"Cfb": {
|
||
"Winter": {"td": 0, "tn": 0, "p": -1, "w": +2},
|
||
"Spring": {"td": +1, "tn": 0, "p": -1, "w": 0},
|
||
"Summer": {"td": +3, "tn": +1, "p": -1, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -2, "w": 0},
|
||
},
|
||
"Cfc": {
|
||
"Winter": {"td": 0, "tn": -1, "p": -3, "w": +2},
|
||
"Spring": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
"Summer": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
},
|
||
"Dsa": {
|
||
"Winter": {"td": 0, "tn": -2, "p": -3, "w": +2},
|
||
"Spring": {"td": +3, "tn": +1, "p": -4, "w": 0},
|
||
"Summer": {"td": +5, "tn": +3, "p": -5, "w": 0},
|
||
"Fall": {"td": +2, "tn": +1, "p": -4, "w": 0},
|
||
},
|
||
"Dsb": {
|
||
"Winter": {"td": -2, "tn": -5, "p": -4, "w": +2},
|
||
"Spring": {"td": +1, "tn": -2, "p": -2, "w": 0},
|
||
"Summer": {"td": +3, "tn": +1, "p": -3, "w": 0},
|
||
"Fall": {"td": 0, "tn": -1, "p": -3, "w": 0},
|
||
},
|
||
"Dsc": {
|
||
"Winter": {"td": -2, "tn": -3, "p": -2, "w": +2},
|
||
"Spring": {"td": +1, "tn": -1, "p": -3, "w": 0},
|
||
"Summer": {"td": +2, "tn": -1, "p": -2, "w": 0},
|
||
"Fall": {"td": -1, "tn": -1, "p": -1, "w": 0},
|
||
},
|
||
"Dwa": {
|
||
"Winter": {"td": 0, "tn": -3, "p": -5, "w": +2},
|
||
"Spring": {"td": +1, "tn": 0, "p": -4, "w": 0},
|
||
"Summer": {"td": +3, "tn": 0, "p": -2, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -1, "w": 0},
|
||
},
|
||
"Dwb": {
|
||
"Winter": {"td": -1, "tn": -3, "p": -4, "w": +2},
|
||
"Spring": {"td": +2, "tn": 0, "p": -3, "w": 0},
|
||
"Summer": {"td": +3, "tn": +1, "p": -2, "w": 0},
|
||
"Fall": {"td": 0, "tn": 0, "p": -3, "w": 0},
|
||
},
|
||
"Dwc": {
|
||
"Winter": {"td": -1, "tn": -5, "p": -4, "w": +2},
|
||
"Spring": {"td": 0, "tn": -3, "p": -4, "w": 0},
|
||
"Summer": {"td": +1, "tn": -1, "p": -2, "w": 0},
|
||
"Fall": {"td": 0, "tn": -2, "p": -3, "w": 0},
|
||
},
|
||
"Dwd": {
|
||
"Winter": {"td": -9, "tn": -11, "p": -3, "w": +2},
|
||
"Spring": {"td": 0, "tn": -4, "p": -4, "w": 0},
|
||
"Summer": {"td": +2, "tn": -2, "p": -3, "w": +2},
|
||
"Fall": {"td": -4, "tn": -7, "p": -5, "w": 0},
|
||
},
|
||
"Dfa": {
|
||
"Winter": {"td": -2, "tn": -4, "p": -3, "w": +2},
|
||
"Spring": {"td": +3, "tn": -1, "p": -2, "w": 0},
|
||
"Summer": {"td": +4, "tn": +2, "p": -4, "w": +2},
|
||
"Fall": {"td": +2, "tn": -2, "p": -3, "w": 0},
|
||
},
|
||
"Dfb": {
|
||
"Winter": {"td": -3, "tn": -4, "p": -3, "w": +2},
|
||
"Spring": {"td": +2, "tn": -2, "p": -2, "w": 0},
|
||
"Summer": {"td": +3, "tn": +1, "p": -3, "w": +2},
|
||
"Fall": {"td": 0, "tn": -2, "p": -3, "w": 0},
|
||
},
|
||
"Dfc": {
|
||
"Winter": {"td": -3, "tn": -5, "p": -4, "w": +2},
|
||
"Spring": {"td": +1, "tn": -2, "p": -4, "w": 0},
|
||
"Summer": {"td": +2, "tn": 0, "p": -3, "w": 0},
|
||
"Fall": {"td": -1, "tn": -2, "p": -3, "w": 0},
|
||
},
|
||
"Dfd": {
|
||
"Winter": {"td": -9, "tn": -11, "p": -4, "w": +2},
|
||
"Spring": {"td": 0, "tn": -4, "p": -4, "w": 0},
|
||
"Summer": {"td": +2, "tn": -2, "p": -4, "w": 0},
|
||
"Fall": {"td": -5, "tn": -6, "p": -5, "w": 0},
|
||
},
|
||
"ET": {
|
||
"Winter": {"td": -8, "tn": -11, "p": -2, "w": +2},
|
||
"Spring": {"td": 0, "tn": -1, "p": -3, "w": 0},
|
||
"Summer": {"td": 0, "tn": 0, "p": -2, "w": 0},
|
||
"Fall": {"td": -1, "tn": -2, "p": -1, "w": 0},
|
||
},
|
||
"EF": {
|
||
# Примечание: EF (Антарктида/ледники) даёт td=-11 для всех сезонов.
|
||
# Из-за клэмпинга [-7, 19] большинство бросков упрётся в -7 («Frigid»).
|
||
# Это архитектурное ограничение системы 2d6: вариативность намеренно
|
||
# мала — все дни там одинаково экстремальны.
|
||
"Winter": {"td": -11, "tn": -11, "p": -5, "w": -2},
|
||
"Spring": {"td": -11, "tn": -11, "p": -5, "w": 0},
|
||
"Summer": {"td": -11, "tn": -11, "p": -5, "w": 0},
|
||
"Fall": {"td": -11, "tn": -11, "p": -5, "w": 0},
|
||
},
|
||
}
|
||
|
||
VALID_SEASONS = ("Winter", "Spring", "Summer", "Fall")
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Вспомогательные функции
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def _roll_2d6() -> int:
|
||
"""Бросок 2d6 → [2, 12]."""
|
||
return random.randint(1, 6) + random.randint(1, 6)
|
||
|
||
|
||
def _clamp(value: int, lo: int = -7, hi: int = 19) -> int:
|
||
return max(lo, min(hi, value))
|
||
|
||
|
||
def _lookup_temp(index: int) -> str:
|
||
"""Возвращает описание температуры по индексу (с клэмпингом)."""
|
||
return TEMP_TABLE[_clamp(index)]
|
||
|
||
|
||
def _lookup_precip(index: int, t_index: int) -> str:
|
||
"""
|
||
Возвращает описание осадков с учётом температуры.
|
||
Если t_index < SNOW_THRESHOLD и осадки «тёплые» (Drizzly/Rainy),
|
||
автоматически заменяет их на снежный эквивалент.
|
||
"""
|
||
raw = PRECIPITATION_TABLE[_clamp(index)]
|
||
if t_index < SNOW_THRESHOLD and raw in PRECIP_COLD_REMAP:
|
||
return PRECIP_COLD_REMAP[raw]
|
||
return raw
|
||
|
||
|
||
def _lookup_wind(index: int) -> str:
|
||
return WINDS_TABLE[_clamp(index)]
|
||
|
||
def _f_to_c(fahrenheit: float) -> float:
|
||
"""Конвертация °F → °C."""
|
||
return (fahrenheit - 32) * 5 / 9
|
||
|
||
# Регулярное выражение для замены диапазонов °F на °C в строках таблицы.
|
||
import re
|
||
|
||
def _convert_temp_str_to_celsius(temp_str: str) -> str:
|
||
"""
|
||
Принимает строку вида 'Balmy (69–72 °F)' или 'Frigid (-75 °F или ниже)'
|
||
и возвращает строку с диапазоном в °C.
|
||
|
||
Обрабатывает форматы:
|
||
(X °F или ниже)
|
||
(X до Y °F)
|
||
(X–Y °F)
|
||
(X °F)
|
||
"""
|
||
# «-75 °F или ниже»
|
||
m = re.search(r'\((-?\d+)\s*°F или ниже\)', temp_str)
|
||
if m:
|
||
f = int(m.group(1))
|
||
c = round(_f_to_c(f))
|
||
return re.sub(r'\((-?\d+)\s*°F или ниже\)', f'({c} °C или ниже)', temp_str)
|
||
|
||
# «111 °F или выше»
|
||
m = re.search(r'\((-?\d+)\s*°F или выше\)', temp_str)
|
||
if m:
|
||
f = int(m.group(1))
|
||
c = round(_f_to_c(f))
|
||
return re.sub(r'\((-?\d+)\s*°F или выше\)', f'({c} °C или выше)', temp_str)
|
||
|
||
# «-65 до -74 °F»
|
||
m = re.search(r'\((-?\d+)\s*до\s*(-?\d+)\s*°F\)', temp_str)
|
||
if m:
|
||
c1 = round(_f_to_c(int(m.group(1))))
|
||
c2 = round(_f_to_c(int(m.group(2))))
|
||
return re.sub(r'\((-?\d+)\s*до\s*(-?\d+)\s*°F\)', f'({c1} до {c2} °C)', temp_str)
|
||
|
||
# «69–72 °F»
|
||
m = re.search(r'\((-?\d+)–(-?\d+)\s*°F\)', temp_str)
|
||
if m:
|
||
c1 = round(_f_to_c(int(m.group(1))))
|
||
c2 = round(_f_to_c(int(m.group(2))))
|
||
return re.sub(r'\((-?\d+)–(-?\d+)\s*°F\)', f'({c1}–{c2} °C)', temp_str)
|
||
|
||
# одиночное значение «X °F»
|
||
m = re.search(r'\((-?\d+)\s*°F\)', temp_str)
|
||
if m:
|
||
c = round(_f_to_c(int(m.group(1))))
|
||
return re.sub(r'\((-?\d+)\s*°F\)', f'({c} °C)', temp_str)
|
||
|
||
return temp_str
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Генерация одного дня
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def _generate_single_day(koppen: str, season: str, use_celsius: bool = False) -> str:
|
||
"""
|
||
Генерирует погоду на один день и возвращает Markdown-строку.
|
||
|
||
Механика:
|
||
- Дневная температура: 2d6 + td → клэмп → таблица
|
||
- Ночная температура: (2d6 + td) + tn → клэмп → таблица
|
||
(tn — дополнительная дельта ночи относительно дня)
|
||
- Осадки: 2d6 + p → клэмп → таблица + проверка снега
|
||
- Ветер: 2d6 + w → клэмп → таблица
|
||
"""
|
||
mods = CLIMATE_MODS[koppen][season]
|
||
td = mods["td"]
|
||
tn = mods["tn"] # смещение ночи поверх дневного броска
|
||
p = mods["p"]
|
||
w = mods["w"]
|
||
|
||
# Бросок температуры — один базовый бросок для дня
|
||
base_t_roll = _roll_2d6()
|
||
day_t_index = _clamp(base_t_roll + td)
|
||
# Ночь: тот же базовый бросок + tn (ночная дельта стабильна для климата,
|
||
# но сам бросок дня добавляет случайность)
|
||
night_t_index = _clamp(base_t_roll + td + tn)
|
||
|
||
p_roll = _clamp(_roll_2d6() + p)
|
||
w_roll = _clamp(_roll_2d6() + w)
|
||
|
||
day_temp = _lookup_temp(day_t_index)
|
||
night_temp = _lookup_temp(night_t_index)
|
||
|
||
# Осадки проверяем по ДНЕВНОЙ температуре
|
||
# (если днём уже ниже нуля — всё идёт снегом)
|
||
precip = _lookup_precip(p_roll, day_t_index)
|
||
wind = _lookup_wind(w_roll)
|
||
|
||
# Отладочные индексы (удобно при тестировании)
|
||
debug = (
|
||
f"*(rolls: base_t={base_t_roll}, "
|
||
f"day_idx={day_t_index}, night_idx={night_t_index}, "
|
||
f"p_idx={p_roll}, w_idx={w_roll})*"
|
||
)
|
||
|
||
if use_celsius:
|
||
day_temp = _convert_temp_str_to_celsius(day_temp)
|
||
night_temp = _convert_temp_str_to_celsius(night_temp)
|
||
|
||
return (
|
||
f"- **День:** {day_temp} | "
|
||
f"**Ночь:** {night_temp} | "
|
||
f"**Осадки:** {precip} | "
|
||
f"**Ветер:** {wind} "
|
||
# f"\n {debug}"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# TypedDict для аннотации параметров тула
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
class WeatherConstraint(TypedDict, total=False):
|
||
"""
|
||
Параметры генерации погоды для одной локации/периода.
|
||
|
||
Поля:
|
||
koppen: Köppen-код климата. Допустимые значения:
|
||
Af, Am, Aw, As,
|
||
BWh, BWk, BSh, BSk,
|
||
Csa, Csb, Csc, Cwa, Cwb, Cwc, Cfa, Cfb, Cfc,
|
||
Dsa, Dsb, Dsc, Dwa, Dwb, Dwc, Dwd, Dfa, Dfb, Dfc, Dfd,
|
||
ET, EF.
|
||
|
||
season: Сезон: Winter, Spring, Summer, Fall.
|
||
|
||
count: Количество дней для генерации. По умолчанию 1.
|
||
|
||
use_celsius: default False — если True, выводить °C вместо °F
|
||
"""
|
||
koppen: str # обязательно
|
||
season: str # обязательно
|
||
count: int # default 1
|
||
use_celsius: bool # default False — если True, выводить °C вместо °F
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Публичный метод тула
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def generate_weather(
|
||
debug_description: str,
|
||
constraints: list[WeatherConstraint],
|
||
) -> str:
|
||
"""
|
||
Основной метод, который вызывает тул с фронта.
|
||
|
||
Принимает список объектов WeatherConstraint. Для каждого объекта:
|
||
- валидирует Köppen-код и сезон;
|
||
- генерирует `count` дней погоды.
|
||
|
||
Возвращает Markdown-строку со всеми результатами.
|
||
|
||
Пример вызова:
|
||
generate_weather("тест", [
|
||
{"koppen": "Cfb", "season": "Winter", "count": 3},
|
||
{"koppen": "BWh", "season": "Summer", "count": 1},
|
||
])
|
||
"""
|
||
if not constraints:
|
||
return "❌ Не передано ни одного ограничения."
|
||
|
||
sections: list[str] = []
|
||
|
||
for idx, constraint in enumerate(constraints, start=1):
|
||
koppen = constraint.get("koppen", "").strip()
|
||
season = constraint.get("season", "").strip()
|
||
count = int(constraint.get("count", 1))
|
||
use_celsius = bool(constraint.get("use_celsius", False))
|
||
|
||
# ── Валидация ────────────────────────────────────────────────────────
|
||
errors: list[str] = []
|
||
|
||
if not koppen:
|
||
errors.append("не указан `koppen`")
|
||
elif koppen not in CLIMATE_MODS:
|
||
valid = ", ".join(sorted(CLIMATE_MODS.keys()))
|
||
errors.append(f"неизвестный Köppen-код `{koppen}`. Допустимые: {valid}")
|
||
|
||
if not season:
|
||
errors.append("не указан `season`")
|
||
elif season not in VALID_SEASONS:
|
||
errors.append(
|
||
f"неизвестный сезон `{season}`. "
|
||
f"Допустимые: {', '.join(VALID_SEASONS)}"
|
||
)
|
||
|
||
if errors:
|
||
sections.append(
|
||
f"### Набор {idx} — ошибка\n" + "\n".join(f"- {e}" for e in errors)
|
||
)
|
||
continue
|
||
|
||
count = max(1, min(count, 30)) # не более 30 дней за раз
|
||
|
||
# ── Генерация ────────────────────────────────────────────────────────
|
||
mods = CLIMATE_MODS[koppen][season]
|
||
header = (
|
||
f"### Набор {idx}: {koppen} / {season} ({count} дн.)\n"
|
||
# f"*Модификаторы: td={mods['td']:+d}, tn={mods['tn']:+d}, "
|
||
# f"p={mods['p']:+d}, w={mods['w']:+d}*\n"
|
||
)
|
||
|
||
lines: list[str] = [header]
|
||
for day_num in range(1, count + 1):
|
||
day_line = _generate_single_day(koppen, season, use_celsius)
|
||
lines.append(f"**День {day_num}:** \n{day_line}")
|
||
|
||
sections.append("\n\n".join(lines))
|
||
|
||
return "\n\n---\n\n".join(sections)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Быстрый тест при запуске напрямую
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
result = generate_weather(
|
||
"тест",
|
||
[
|
||
{
|
||
"koppen": "Cfb",
|
||
"season": "Winter",
|
||
"count": 3
|
||
},
|
||
{
|
||
"koppen": "BWh",
|
||
"season": "Summer",
|
||
"count": 2
|
||
},
|
||
{
|
||
"koppen": "EF",
|
||
"season": "Winter",
|
||
"count": 2
|
||
},
|
||
{
|
||
"koppen": "Dwd",
|
||
"season": "Winter",
|
||
"count": 2
|
||
},
|
||
{
|
||
"koppen": "Af",
|
||
"season": "Summer",
|
||
"count": 1
|
||
},
|
||
# Проверка ошибки валидации
|
||
{
|
||
"koppen": "XXX",
|
||
"season": "Winter",
|
||
"count": 1
|
||
},
|
||
])
|
||
print(result)
|