Weather changes on time; repeat rolls lengthen the spell
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5bcf26f379
commit
0b7e4a722f
3 changed files with 41 additions and 8 deletions
31
game.py
31
game.py
|
|
@ -1563,6 +1563,7 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
||||||
_happenings_hour(world, city["last_tick"], rng)
|
_happenings_hour(world, city["last_tick"], rng)
|
||||||
_city_hour(world, city["last_tick"])
|
_city_hour(world, city["last_tick"])
|
||||||
changed = True
|
changed = True
|
||||||
|
changed = advance_weather(world, now, rng) or changed
|
||||||
changed = _happenings_tracks(world, now, rng) or changed
|
changed = _happenings_tracks(world, now, rng) or changed
|
||||||
construction = city["construction"]
|
construction = city["construction"]
|
||||||
if construction and construction["labour"] >= construction["needed"]:
|
if construction and construction["labour"] >= construction["needed"]:
|
||||||
|
|
@ -2134,6 +2135,30 @@ def _roll_weather(world: dict, now: float, rng: random.Random, current: str | No
|
||||||
return _pick(SEASON_WEATHER[season(world, now)["index"]], rng)
|
return _pick(SEASON_WEATHER[season(world, now)["index"]], rng)
|
||||||
|
|
||||||
|
|
||||||
|
def advance_weather(world: dict, now: float, rng: random.Random) -> bool:
|
||||||
|
"""Change the weather when its spell ends. Returns True if it changed."""
|
||||||
|
weather = world["city"]["weather"]
|
||||||
|
if now < weather["until"]:
|
||||||
|
return False
|
||||||
|
while now >= weather["until"]:
|
||||||
|
weather["kind"] = weather["next"]
|
||||||
|
_plan_spell(world, weather, rng)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_spell(world: dict, weather: dict, rng: random.Random) -> None:
|
||||||
|
"""Set how long the current weather lasts and what follows. Rolls that keep the
|
||||||
|
same weather lengthen the spell, so `until` is when it really changes."""
|
||||||
|
for _ in range(3):
|
||||||
|
weather["until"] += rng.uniform(4, 8) * HOUR / speed(world)
|
||||||
|
weather["next"] = _roll_weather(world, weather["until"], rng, weather["kind"])
|
||||||
|
if weather["next"] != weather["kind"]:
|
||||||
|
return
|
||||||
|
# Three spells of the same is long enough: something else comes next.
|
||||||
|
weights = SEASON_WEATHER[season(world, weather["until"])["index"]]
|
||||||
|
weather["next"] = _pick({kind: w for kind, w in weights.items() if kind != weather["kind"]}, rng)
|
||||||
|
|
||||||
|
|
||||||
def _modifier_sources(world: dict) -> list[dict]:
|
def _modifier_sources(world: dict) -> list[dict]:
|
||||||
city = world["city"]
|
city = world["city"]
|
||||||
sources = [WEATHER[city["weather"]["kind"]]] + city["effects"]
|
sources = [WEATHER[city["weather"]["kind"]]] + city["effects"]
|
||||||
|
|
@ -2437,11 +2462,7 @@ def hire_mercenaries(world: dict, player: dict, now: float) -> str | None:
|
||||||
def _happenings_hour(world: dict, now: float, rng: random.Random) -> None:
|
def _happenings_hour(world: dict, now: float, rng: random.Random) -> None:
|
||||||
"""Weather, firewood, thieves and expiries, once per game hour."""
|
"""Weather, firewood, thieves and expiries, once per game hour."""
|
||||||
city = world["city"]
|
city = world["city"]
|
||||||
weather = city["weather"]
|
advance_weather(world, now, rng)
|
||||||
if now >= weather["until"]:
|
|
||||||
weather["kind"] = weather["next"]
|
|
||||||
weather["until"] = now + rng.uniform(4, 8) * HOUR / speed(world)
|
|
||||||
weather["next"] = _roll_weather(world, weather["until"], rng, weather["kind"])
|
|
||||||
city["effects"] = [e for e in city["effects"] if e["until"] > now]
|
city["effects"] = [e for e in city["effects"] if e["until"] > now]
|
||||||
for problem in list(city["problems"]):
|
for problem in list(city["problems"]):
|
||||||
if problem.get("until") and problem["until"] <= now:
|
if problem.get("until") and problem["until"] <= now:
|
||||||
|
|
|
||||||
|
|
@ -681,15 +681,14 @@ function siteBanner() {
|
||||||
|
|
||||||
// "Rain: farming 10% faster. Clear skies in 3h 20m."
|
// "Rain: farming 10% faster. Clear skies in 3h 20m."
|
||||||
function weatherTip(w) {
|
function weatherTip(w) {
|
||||||
const next = w.next === w.kind ? `Staying ${w.name.toLowerCase()}` : w.next_name;
|
return `${w.name}: ${w.text.charAt(0).toLowerCase()}${w.text.slice(1)}. ${w.next_name} in ${dur(w.until - serverNow())}.`;
|
||||||
return `${w.name}: ${w.text.charAt(0).toLowerCase()}${w.text.slice(1)}. ${next} in ${dur(w.until - serverNow())}.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function weatherCard() {
|
function weatherCard() {
|
||||||
const h = S.happenings, w = h.weather;
|
const h = S.happenings, w = h.weather;
|
||||||
return `<div class="stat" title="${esc(`${weatherTip(w)} Weather changes every few hours; each season lasts a week.`)}"><div class="k">${h.season.icon} ${h.season.name}, day ${h.season.day}</div>
|
return `<div class="stat" title="${esc(`${weatherTip(w)} Weather changes every few hours; each season lasts a week.`)}"><div class="k">${h.season.icon} ${h.season.name}, day ${h.season.day}</div>
|
||||||
<div class="v">${w.icon} <small>${esc(w.name)}</small></div>
|
<div class="v">${w.icon} <small>${esc(w.name)}</small></div>
|
||||||
<div class="note">${w.next === w.kind ? `Staying ${esc(w.name.toLowerCase())}` : `${w.next_icon} ${esc(w.next_name)}`} in ${countdown(w.until)}</div></div>`;
|
<div class="note">${w.next_icon} ${esc(w.next_name)} in ${countdown(w.until)}</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything going on in the city right now, and what anyone can do about it.
|
// Everything going on in the city right now, and what anyone can do about it.
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,7 @@ class CityTest(unittest.TestCase):
|
||||||
world = world_with()
|
world = world_with()
|
||||||
city = world["city"]
|
city = world["city"]
|
||||||
city["stockpile"] = {"bread": 5}
|
city["stockpile"] = {"bread": 5}
|
||||||
|
world["next_fortune_at"] = None # no lucky wagon of bread
|
||||||
game.tick(world, T0 + 4 * game.DAY, random.Random(1))
|
game.tick(world, T0 + 4 * game.DAY, random.Random(1))
|
||||||
self.assertTrue(city["starving"])
|
self.assertTrue(city["starving"])
|
||||||
self.assertNotIn("bread", city["stockpile"])
|
self.assertNotIn("bread", city["stockpile"])
|
||||||
|
|
@ -953,6 +954,18 @@ class HappeningsTest(unittest.TestCase):
|
||||||
self.assertEqual(game.season(self.world, T0 + 8 * game.DAY)["name"], "Summer")
|
self.assertEqual(game.season(self.world, T0 + 8 * game.DAY)["name"], "Summer")
|
||||||
self.assertEqual(game.season(self.world, T0 + 22 * game.DAY)["name"], "Winter")
|
self.assertEqual(game.season(self.world, T0 + 22 * game.DAY)["name"], "Winter")
|
||||||
|
|
||||||
|
def test_weather_changes_on_time_and_always_to_something_new(self):
|
||||||
|
weather = self.city["weather"]
|
||||||
|
weather.update(kind="rain", next="clear", until=T0 + 10 * 60)
|
||||||
|
self.city["last_tick"] = T0
|
||||||
|
game.tick(self.world, T0 + 11 * 60, self.rng) # well inside the hour
|
||||||
|
self.assertEqual(weather["kind"], "clear")
|
||||||
|
for _ in range(50):
|
||||||
|
spell = weather["until"]
|
||||||
|
game.tick(self.world, spell, self.rng)
|
||||||
|
self.assertNotEqual(weather["next"], weather["kind"])
|
||||||
|
self.assertGreater(weather["until"], spell)
|
||||||
|
|
||||||
def test_days_count_midnights_not_24_hour_stretches(self):
|
def test_days_count_midnights_not_24_hour_stretches(self):
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
late = datetime(2026, 10, 18, 23, 0).timestamp()
|
late = datetime(2026, 10, 18, 23, 0).timestamp()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue