dnd5-scripts/weather/weather.py
2026-06-17 22:32:48 +03:00

635 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 (615 °F)",
3: "Cold (1625 °F)",
4: "Cold (2632 °F)",
5: "Very Chilly (3336 °F)",
6: "Very Chilly (3740 °F)",
7: "Chilly (4150 °F)",
8: "Chilly (5155 °F)",
9: "Brisk (5660 °F)",
10: "Brisk (6165 °F)",
11: "Balmy (6668 °F)",
12: "Balmy (6972 °F)",
13: "Warm (7376 °F)",
14: "Warm (7780 °F)",
15: "Hot (8186 °F)",
16: "Hot (8792 °F)",
17: "Sweltering (93100 °F)",
18: "Sweltering (101110 °F)",
19: "Sweltering (111 °F или выше)",
}
# Порог температурного индекса, ниже которого осадки — снег/метель,
# а не дождь. Индекс 4 = Cold (2632 °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 (6972 °F)' или 'Frigid (-75 °F или ниже)'
и возвращает строку с диапазоном в °C.
Обрабатывает форматы:
(X °F или ниже)
(X до Y °F)
(XY °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)
# «6972 °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)