Building funds safe from raids; settlement tiers; population and food on the town square
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3b70c1e3a
commit
3716df7493
6 changed files with 358 additions and 91 deletions
218
game.py
218
game.py
|
|
@ -160,9 +160,9 @@ ACTION_BY_ID = {action["id"]: action for action in ACTIONS}
|
|||
|
||||
BUILDINGS = {
|
||||
"houses": {
|
||||
"name": "Houses", "icon": "🏠", "max": 5, "gold": 200,
|
||||
"name": "Houses", "icon": "🏠", "max": 8, "gold": 200,
|
||||
"base": {"logs": 600, "bricks": 300}, "advanced": {"oak_logs": 200},
|
||||
"effect": "Room for 15 more people per level",
|
||||
"effect": "Room for 20 more people per level",
|
||||
},
|
||||
"walls": {
|
||||
"name": "Walls", "icon": "🏰", "max": 5, "gold": 300,
|
||||
|
|
@ -193,6 +193,18 @@ BUILDINGS = {
|
|||
BUILD_HOURS = 3
|
||||
REPAIR_SHARE = 0.3
|
||||
|
||||
# Population promotes the settlement, permanently. Each tier lifts how high
|
||||
# buildings may go (houses have their own ladder so the next tier is always
|
||||
# reachable) and gives everyone more bank slots.
|
||||
TIERS = [
|
||||
{"name": "Hamlet", "pop": 0, "cap": 1, "houses": 2},
|
||||
{"name": "Village", "pop": 40, "cap": 3, "houses": 4},
|
||||
{"name": "Town", "pop": 90, "cap": 4, "houses": 7},
|
||||
{"name": "City", "pop": 150, "cap": 99, "houses": 8},
|
||||
]
|
||||
TIER_SLOTS = 2
|
||||
GROWTH_PER_DAY = 0.12
|
||||
|
||||
RAID_GROUPS = ["the Red Hand", "the Hollow Crows", "the Blackwater gang", "the Ashen Wolves",
|
||||
"the Gallows Men", "the Thornback raiders"]
|
||||
RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows",
|
||||
|
|
@ -202,9 +214,12 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
|
|||
|
||||
# --- world ---------------------------------------------------------------
|
||||
|
||||
WORLD_VERSION = 2
|
||||
|
||||
|
||||
def new_world(now: float, speed: float = 1.0) -> dict:
|
||||
return {
|
||||
"version": 1,
|
||||
"version": WORLD_VERSION,
|
||||
"created": now,
|
||||
"config": {"speed": speed, "clock_offset": 0},
|
||||
"players": {},
|
||||
|
|
@ -212,8 +227,10 @@ def new_world(now: float, speed: float = 1.0) -> dict:
|
|||
"population": 20.0,
|
||||
"treasury": 0.0,
|
||||
"stockpile": {"bread": 40},
|
||||
"buildings": {key: {"level": 0, "damaged": False} for key in BUILDINGS},
|
||||
"buildings": {key: {"level": 0, "damaged": False, "fund": {}} for key in BUILDINGS},
|
||||
"construction": None,
|
||||
"queue": [],
|
||||
"tier": 0,
|
||||
"last_tick": now,
|
||||
"food_due": 0.0,
|
||||
"starving": False,
|
||||
|
|
@ -227,6 +244,18 @@ def new_world(now: float, speed: float = 1.0) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def migrate(world: dict) -> dict:
|
||||
"""Bring a saved world up to the current shape. Never loses anything."""
|
||||
if world.get("version", 1) < 2:
|
||||
city = world["city"]
|
||||
for building in city["buildings"].values():
|
||||
building.setdefault("fund", {})
|
||||
city.setdefault("queue", [])
|
||||
city.setdefault("tier", 0)
|
||||
world["version"] = 2
|
||||
return world
|
||||
|
||||
|
||||
def speed(world: dict) -> float:
|
||||
return world["config"]["speed"]
|
||||
|
||||
|
|
@ -268,7 +297,29 @@ def repair_cost(key: str, level: int) -> dict:
|
|||
|
||||
|
||||
def housing(world: dict) -> int:
|
||||
return 25 + 15 * building_level(world, "houses")
|
||||
return 25 + 20 * building_level(world, "houses")
|
||||
|
||||
|
||||
def level_cap(world: dict, key: str) -> int:
|
||||
"""The highest level the settlement's tier allows for this building."""
|
||||
tier = TIERS[world["city"]["tier"]]
|
||||
return min(BUILDINGS[key]["max"], tier["houses"] if key == "houses" else tier["cap"])
|
||||
|
||||
|
||||
def fund_target(world: dict, key: str) -> dict | None:
|
||||
"""What the building's fund is collecting for: a repair, the next level, or nothing."""
|
||||
building = world["city"]["buildings"][key]
|
||||
if building["damaged"]:
|
||||
return repair_cost(key, building["level"])
|
||||
if building["level"] >= level_cap(world, key):
|
||||
return None
|
||||
return build_cost(key, building["level"] + 1)
|
||||
|
||||
|
||||
def fund_missing(world: dict, key: str) -> dict:
|
||||
target = fund_target(world, key) or {}
|
||||
fund = world["city"]["buildings"][key]["fund"]
|
||||
return {item: qty - fund.get(item, 0) for item, qty in target.items() if fund.get(item, 0) < qty}
|
||||
|
||||
|
||||
# --- players -------------------------------------------------------------
|
||||
|
|
@ -292,7 +343,8 @@ def skill_level(player: dict, skill: str) -> int:
|
|||
|
||||
|
||||
def bank_slots(world: dict, player: dict) -> int:
|
||||
return BASE_SLOTS + 3 * building_level(world, "storehouse") + player["perks"]["bank_slot"]
|
||||
return (BASE_SLOTS + 3 * building_level(world, "storehouse") + TIER_SLOTS * world["city"]["tier"]
|
||||
+ player["perks"]["bank_slot"])
|
||||
|
||||
|
||||
def stack_size(world: dict) -> int:
|
||||
|
|
@ -549,45 +601,120 @@ def sell_price(world: dict, item: str, now: float) -> float:
|
|||
return price
|
||||
|
||||
|
||||
def commission(world: dict, player: dict, key: str, now: float) -> str | None:
|
||||
def fund(world: dict, player: dict, key: str, item: str, qty: int | str, source: str, now: float) -> str | None:
|
||||
"""Put materials towards a building. Funds are safe from raiders.
|
||||
|
||||
`source` is "bank" (the player's own, earns favour) or "city" (the
|
||||
stockpile, or the treasury for gold). `qty` may be "all": as much as
|
||||
the source has, up to what the fund still needs.
|
||||
"""
|
||||
city = world["city"]
|
||||
if key not in BUILDINGS:
|
||||
return "No such building"
|
||||
if city["construction"]:
|
||||
return "The builders are busy"
|
||||
building = city["buildings"][key]
|
||||
repair = building["damaged"]
|
||||
if not repair and building["level"] >= BUILDINGS[key]["max"]:
|
||||
return "Already at its highest level"
|
||||
cost = repair_cost(key, building["level"]) if repair else build_cost(key, building["level"] + 1)
|
||||
missing = missing_for(city, cost)
|
||||
if missing:
|
||||
return "The stockpile is short: " + ", ".join(missing)
|
||||
for item, qty in cost.items():
|
||||
if item != "gold" and item not in ITEMS:
|
||||
return "No such item"
|
||||
need = fund_missing(world, key).get(item, 0)
|
||||
if not need:
|
||||
return "The fund doesn't need that"
|
||||
if source == "bank":
|
||||
have = player["gold"] if item == "gold" else player["bank"].get(item, 0)
|
||||
elif source == "city":
|
||||
have = int(city["treasury"]) if item == "gold" else city["stockpile"].get(item, 0)
|
||||
else:
|
||||
return "Bad source"
|
||||
if qty == "all":
|
||||
qty = min(have, need)
|
||||
if not isinstance(qty, int) or qty <= 0:
|
||||
return "Nothing to give"
|
||||
if qty > have:
|
||||
return "Not enough to give"
|
||||
qty = min(qty, need)
|
||||
if source == "bank":
|
||||
if item == "gold":
|
||||
city["treasury"] -= qty
|
||||
player["gold"] -= qty
|
||||
else:
|
||||
left = city["stockpile"][item] - qty
|
||||
if left:
|
||||
city["stockpile"][item] = left
|
||||
else:
|
||||
del city["stockpile"][item]
|
||||
hours = BUILD_HOURS * (building["level"] + 1) * (0.25 if repair else 1)
|
||||
city["construction"] = {"building": key, "repair": repair, "start": now,
|
||||
"done_at": now + hours * HOUR / speed(world)}
|
||||
verb = "repairs to" if repair else "work on"
|
||||
log(world, now, "build", f"{player['name']} started {verb} the {BUILDINGS[key]['name'].lower()}", player["id"])
|
||||
bank_take(player, item, qty)
|
||||
gain = qty * (1 if item == "gold" else ITEMS[item]["value"])
|
||||
player["favour"] += gain
|
||||
player["donated"] += gain
|
||||
elif item == "gold":
|
||||
city["treasury"] -= qty
|
||||
else:
|
||||
take_stock(city, item, qty)
|
||||
building = city["buildings"][key]
|
||||
building["fund"][item] = building["fund"].get(item, 0) + qty
|
||||
label = f"{qty:,} gold" if item == "gold" else amount(item, qty)
|
||||
where = "" if source == "bank" else " from the city's stores"
|
||||
log(world, now, "fund", f"{player['name']} put {label}{where} towards the {BUILDINGS[key]['name'].lower()}", player["id"])
|
||||
_queue_if_funded(world, key, now)
|
||||
_start_next(world, now)
|
||||
return None
|
||||
|
||||
|
||||
def missing_for(city: dict, cost: dict) -> list[str]:
|
||||
missing = []
|
||||
for item, qty in cost.items():
|
||||
have = city["treasury"] if item == "gold" else city["stockpile"].get(item, 0)
|
||||
if have < qty:
|
||||
name = "gold" if item == "gold" else ITEMS[item]["name"].lower()
|
||||
missing.append(f"{qty - int(have):,} {name}")
|
||||
return missing
|
||||
def take_stock(city: dict, item: str, qty: int) -> None:
|
||||
left = city["stockpile"][item] - qty
|
||||
if left > 0:
|
||||
city["stockpile"][item] = left
|
||||
else:
|
||||
del city["stockpile"][item]
|
||||
|
||||
|
||||
def _queue_if_funded(world: dict, key: str, now: float) -> None:
|
||||
city = world["city"]
|
||||
if fund_target(world, key) is None or fund_missing(world, key):
|
||||
return
|
||||
busy = city["construction"] and city["construction"]["building"] == key
|
||||
if key not in city["queue"] and not busy:
|
||||
city["queue"].append(key)
|
||||
log(world, now, "build", f"The {BUILDINGS[key]['name'].lower()} are fully funded"
|
||||
if BUILDINGS[key]["name"].endswith("s") else f"The {BUILDINGS[key]['name'].lower()} is fully funded")
|
||||
|
||||
|
||||
def _start_next(world: dict, now: float) -> None:
|
||||
"""Start the next funded building, if the builders are free."""
|
||||
city = world["city"]
|
||||
while not city["construction"] and city["queue"]:
|
||||
key = city["queue"].pop(0)
|
||||
target = fund_target(world, key)
|
||||
if target is None or fund_missing(world, key):
|
||||
continue # its target changed while it waited (e.g. damaged); keep funding
|
||||
building = city["buildings"][key]
|
||||
for item, qty in target.items():
|
||||
left = building["fund"][item] - qty
|
||||
if left > 0:
|
||||
building["fund"][item] = left
|
||||
else:
|
||||
del building["fund"][item]
|
||||
repair = building["damaged"]
|
||||
hours = BUILD_HOURS * (building["level"] + 1) * (0.25 if repair else 1)
|
||||
city["construction"] = {"building": key, "repair": repair, "start": now,
|
||||
"done_at": now + hours * HOUR / speed(world)}
|
||||
verb = "Repairs to" if repair else "Work on"
|
||||
log(world, now, "build", f"{verb} the {BUILDINGS[key]['name'].lower()} began")
|
||||
|
||||
|
||||
def population_view(world: dict) -> dict:
|
||||
"""Population, food and tier, with the numbers that explain them."""
|
||||
city = world["city"]
|
||||
pop = city["population"]
|
||||
room = housing(world)
|
||||
if city["starving"]:
|
||||
growth = -pop * 0.04
|
||||
elif pop < room - 0.01:
|
||||
growth = max(0.5, pop * GROWTH_PER_DAY)
|
||||
elif pop > room:
|
||||
growth = -pop * 0.05
|
||||
else:
|
||||
growth = 0.0
|
||||
food = sum(city["stockpile"].get(item, 0) * ITEMS[item]["food"] for item in FOOD_ORDER)
|
||||
tier = city["tier"]
|
||||
following = TIERS[tier + 1] if tier + 1 < len(TIERS) else None
|
||||
return {
|
||||
"population": int(pop), "housing": room, "growth": round(growth, 1),
|
||||
"food": food, "food_days": round(food / (pop * 0.5), 1) if pop else None,
|
||||
"tier": tier, "tier_name": TIERS[tier]["name"],
|
||||
"next_tier": following and {"name": following["name"], "pop": following["pop"]},
|
||||
}
|
||||
|
||||
|
||||
def defence(world: dict) -> dict:
|
||||
|
|
@ -634,6 +761,16 @@ def tick(world: dict, now: float, rng: random.Random | None = None) -> bool:
|
|||
construction = city["construction"]
|
||||
if construction and construction["done_at"] <= now:
|
||||
_finish_construction(world, construction["done_at"])
|
||||
_start_next(world, construction["done_at"])
|
||||
changed = True
|
||||
while city["tier"] + 1 < len(TIERS) and city["population"] >= TIERS[city["tier"] + 1]["pop"]:
|
||||
city["tier"] += 1
|
||||
tier = TIERS[city["tier"]]
|
||||
log(world, now, "tier", f"The settlement has grown into a {tier['name'].lower()}! "
|
||||
f"Buildings can go higher, and everyone gets {TIER_SLOTS} more bank slots.")
|
||||
for key in BUILDINGS:
|
||||
_queue_if_funded(world, key, now)
|
||||
_start_next(world, now)
|
||||
changed = True
|
||||
if world["next_event_at"] and world["next_event_at"] <= now and not upcoming_event(world):
|
||||
_announce(world, now, rng)
|
||||
|
|
@ -669,7 +806,7 @@ def _city_hour(world: dict, now: float) -> None:
|
|||
else:
|
||||
room = housing(world)
|
||||
if pop < room:
|
||||
city["population"] = min(room, pop + max(0.25, pop * 0.06) / 24)
|
||||
city["population"] = min(room, pop + max(0.5, pop * GROWTH_PER_DAY) / 24)
|
||||
elif pop > room:
|
||||
city["population"] = max(room, pop - pop * 0.05 / 24)
|
||||
city["treasury"] += pop * 0.5 / 24
|
||||
|
|
@ -688,6 +825,8 @@ def _finish_construction(world: dict, now: float) -> None:
|
|||
building["level"] += 1
|
||||
log(world, now, "build", f"{name} reached level {building['level']}")
|
||||
city["construction"] = None
|
||||
# Leftover materials may already cover the next step.
|
||||
_queue_if_funded(world, work["building"], now)
|
||||
|
||||
|
||||
# --- events --------------------------------------------------------------
|
||||
|
|
@ -870,6 +1009,7 @@ def _resolve_raid(world: dict, event: dict, now: float, rng: random.Random) -> N
|
|||
if severity > 0.35 and built:
|
||||
key = rng.choice(built)
|
||||
city["buildings"][key]["damaged"] = True
|
||||
_queue_if_funded(world, key, now)
|
||||
lines.append(f"The {BUILDINGS[key]['name'].lower()} were damaged and won't work until repaired."
|
||||
if BUILDINGS[key]["name"].endswith("s") else
|
||||
f"The {BUILDINGS[key]['name'].lower()} was damaged and won't work until repaired.")
|
||||
|
|
@ -917,5 +1057,5 @@ def content() -> dict:
|
|||
}
|
||||
return {
|
||||
"items": ITEMS, "skills": SKILLS, "classes": CLASSES, "actions": ACTIONS,
|
||||
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP,
|
||||
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue