need to commit more
19
cards.py
@@ -378,3 +378,22 @@ class Duke(Card):
|
|||||||
return cls(duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
|
return cls(duke_id, name, gold_mult, strength_mult, magic_mult, shadow_mult, holy_mult, soldier_mult,
|
||||||
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult,
|
worker_mult, monster_mult, citizen_mult, domain_mult, boss_mult, minion_mult, beast_mult,
|
||||||
titan_mult, expansion)
|
titan_mult, expansion)
|
||||||
|
|
||||||
|
|
||||||
|
class Exhausted(Card):
|
||||||
|
def __init__(self, exhausted_id):
|
||||||
|
super().__init__()
|
||||||
|
self.exhausted_id = exhausted_id
|
||||||
|
self.name = "Exhausted"
|
||||||
|
self.toggle_visibility(True)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
**super().to_dict(),
|
||||||
|
"exhausted_id": self.exhausted_id,
|
||||||
|
"name": self.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
return cls(d["exhausted_id"])
|
||||||
|
|||||||
181
docs/effect-strings.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Effect String Syntax
|
||||||
|
|
||||||
|
This document covers how effect strings work across the three card tables (citizens, domains, monsters), where the syntax currently diverges, and the proposed unified grammar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current state by table
|
||||||
|
|
||||||
|
### Citizens — `special_payout_on_turn` / `special_payout_off_turn`
|
||||||
|
|
||||||
|
| Card | String | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| Merchant | `choose g 2 m 2` | Pick one: +2g or +2m |
|
||||||
|
| Mercenary | `exchange s 1 g 2` | Pay 1s, gain 2g |
|
||||||
|
| Champion | `exchange g 1 s 4` | Pay 1g, gain 4s |
|
||||||
|
| Paladin | `exchange s 1 m 3` | Pay 1s, gain 3m |
|
||||||
|
| Butcher | `count owned_worker g 2` | Gain 2g per owned Worker citizen |
|
||||||
|
|
||||||
|
### Domains — `activation_effect`
|
||||||
|
|
||||||
|
| Card | String | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| Ancient Tomb | `action.modify_monster_strength +3` | Prompt: add 3 to a monster's strength cost |
|
||||||
|
| Pretorius Conclave | `choose <citizens>` | Prompt: take any citizen from the board |
|
||||||
|
| Cursed Cavern | `m 4 + concurrent_flip_one_citizen` | Gain 4m; all players flip a citizen |
|
||||||
|
| Darktide Harbour | `choose <citizens where role==shadow>` | Prompt: take a shadow citizen |
|
||||||
|
| Cloudrider's Camp | `s 3 + choose <citizens where role==soldier and gold_cost<=2>` | Gain 3s; prompt: take a soldier citizen worth ≤2g |
|
||||||
|
| Wisborg | `manipulate_resources mode=self_convert pay=g:3 gain=v:3 optional=true` | Optionally pay 3g to gain 3vp |
|
||||||
|
|
||||||
|
### Domains — `passive_effect`
|
||||||
|
|
||||||
|
| Card | String | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| Jousting Field | `harvest.gain_per_owned_citizen_name Knight g 1` | Harvest phase: gain 1g per Knight owned |
|
||||||
|
| Foxgrove Palisade | `roll.set_one_die target=6 cost=g:2` | Roll phase: pay 2g to set a die to 6 |
|
||||||
|
| The Desert Orchid | `roll.set_one_die target=1 cost=g_per_owned_role:holy_citizen` | Roll phase: pay 1g per holy citizen to set a die to 1 |
|
||||||
|
| Emerald Stronghold | `effect.add action.emeraldstronghold` | Flag: ignore + when buying citizens |
|
||||||
|
| Pratchett's Plateau | `effect.add action.pratchettsplateau` | Flag: domains cost 1g less |
|
||||||
|
| Shelley Commons | `action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true` | End of action: optionally pay 1g to a player for 1vp |
|
||||||
|
| Cathedral of St Aquila | `action.end manipulate_resources mode=take_from_player take=g:1 optional=true` | End of action: optionally take 1g from a player |
|
||||||
|
| King Tower | `action.end manipulate_resources mode=pay_to_player gain=v:1 pay=m:1 optional=true` | End of action: optionally pay 1m to a player for 1vp |
|
||||||
|
| The Orb of Urdr | `action.end manipulate_resources mode=take_from_player take=m:1 optional=true` | End of action: optionally take 1m from a player |
|
||||||
|
|
||||||
|
### Monsters — `special_reward`
|
||||||
|
|
||||||
|
| Card | String | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| Goblin Mage | `choose g 1 m 1` | Pick one: +1g or +1m |
|
||||||
|
| Goblin Bomber | `choose g 2 m 2 s 2` | Pick one: +2g, +2m, or +2s |
|
||||||
|
| Goblin King | `count area Hills g 1` | Gain 1g per Hills monster slain |
|
||||||
|
| Skeleton King | `count area Ruins g 2` | Gain 2g per Ruins monster slain |
|
||||||
|
| Bane Spider | `choose g 3 <citizens where name==Knight>` | Pick one: +3g or take a Knight citizen |
|
||||||
|
| Ettercap | `choose <citizens where gold_cost<=2>` | Take a citizen worth ≤2g |
|
||||||
|
| Spider Queen | `choose <count area Forest g 2> <citizens + v 1>` | Pick one: 2g per Forest monster slain, or take a citizen and gain 1vp |
|
||||||
|
| Satyr Mage | `choose g 5 m 5 s 5` | Pick one: +5g, +5m, or +5s |
|
||||||
|
| Troll | `count area Valley m 2` | Gain 2m per Valley monster slain |
|
||||||
|
| Dire Bear | `choose g 2 m 2` | Pick one: +2g or +2m |
|
||||||
|
| Orc Warrior | `choose <citizens where gold_cost<=3>` | Take a citizen worth ≤3g |
|
||||||
|
| Orc Batrider | `choose <citizens>` | Take any citizen |
|
||||||
|
| Orc Chieftain | `count area Mountain g 2` | Gain 2g per Mountain monster slain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where the syntax diverges
|
||||||
|
|
||||||
|
### 1. Resource notation — two forms
|
||||||
|
|
||||||
|
Citizens and monsters use positional shorthand; domain KV pairs use colon notation:
|
||||||
|
|
||||||
|
```
|
||||||
|
# positional (citizens, monsters)
|
||||||
|
choose g 2 m 2
|
||||||
|
count owned_worker g 2
|
||||||
|
|
||||||
|
# colon inside KV values (domain passives)
|
||||||
|
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1
|
||||||
|
manipulate_resources mode=self_convert pay=g:3 gain=v:3
|
||||||
|
roll.set_one_die cost=g:2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `count` — same structure, different second word
|
||||||
|
|
||||||
|
The two count patterns are syntactically parallel but semantically distinct. No unification needed beyond being aware they share a parser.
|
||||||
|
|
||||||
|
```
|
||||||
|
count owned_worker g 2 # count by citizen role owned
|
||||||
|
count area Hills g 1 # count by monster area slain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `choose` — brackets sometimes, not always
|
||||||
|
|
||||||
|
The bracket vs no-bracket distinction does carry real meaning and is worth keeping:
|
||||||
|
|
||||||
|
```
|
||||||
|
choose g 1 m 1 # pick one of these resource amounts
|
||||||
|
choose g 3 <citizens where name==Knight> # pick a resource amount OR an entity
|
||||||
|
choose <citizens where gold_cost<=2> # pick an entity from a filtered set
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. `exchange` — only exists in citizens
|
||||||
|
|
||||||
|
No equivalent pattern in domains or monsters. Could be expressed as a compound but `exchange` is readable:
|
||||||
|
|
||||||
|
```
|
||||||
|
exchange s 1 g 2 # pay 1s, receive 2g
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. `.` is doing three different jobs
|
||||||
|
|
||||||
|
```
|
||||||
|
harvest.gain_per_owned_citizen_name ... # dot = phase separator (phase.verb)
|
||||||
|
roll.set_one_die ... # dot = phase separator (phase.verb)
|
||||||
|
action.end manipulate_resources ... # dot = phase separator, then space, then verb
|
||||||
|
action.modify_monster_strength +3 # dot = namespace separator, not timing
|
||||||
|
effect.add action.emeraldstronghold # dot = verb separator, then dot = namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. `manipulate_resources` wrapper verbosity
|
||||||
|
|
||||||
|
The `mode=` value is doing the same work as a first-word verb. The wrapper adds noise:
|
||||||
|
|
||||||
|
```
|
||||||
|
# current
|
||||||
|
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true
|
||||||
|
|
||||||
|
# without the wrapper — same information
|
||||||
|
action.end pay_to_player g 1 v 1 optional
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed unified grammar
|
||||||
|
|
||||||
|
### Core rules
|
||||||
|
|
||||||
|
1. **`.` means phase prefix only.** The left side is always a timing trigger (`harvest`, `roll`, `action.end`). Bare verbs have no dot.
|
||||||
|
2. **Resource amounts are always positional: `g N`.** Colon notation (`g:N`) only appears inside `=` assignments in KV strings where a space would be ambiguous.
|
||||||
|
3. **`choose` uses brackets for entity picks, bare words for resource picks.** Mixed is allowed: `choose g 3 <citizens where name==Knight>`.
|
||||||
|
4. **Compound effects use ` + `.** Each leg is a self-contained effect: `m 4 + concurrent_flip_one_citizen`.
|
||||||
|
|
||||||
|
### Proposed rewrites
|
||||||
|
|
||||||
|
**Domain activation:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# before → after
|
||||||
|
action.modify_monster_strength +3 → modify_monster_strength 3
|
||||||
|
m 4 + concurrent_flip_one_citizen → no change
|
||||||
|
manipulate_resources mode=self_convert pay=g:3 gain=v:3 ... → self_convert g 3 v 3 optional
|
||||||
|
choose <citizens where role==shadow> → no change
|
||||||
|
s 3 + choose <citizens where role==soldier and gold_cost<=2> → no change
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain passive:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# before → after
|
||||||
|
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=g:1 optional=true → action.end pay_to_player g 1 v 1 optional
|
||||||
|
action.end manipulate_resources mode=take_from_player take=g:1 optional=true → action.end take_from_player g 1 optional
|
||||||
|
action.end manipulate_resources mode=pay_to_player gain=v:1 pay=m:1 optional=true → action.end pay_to_player m 1 v 1 optional
|
||||||
|
action.end manipulate_resources mode=take_from_player take=m:1 optional=true → action.end take_from_player m 1 optional
|
||||||
|
|
||||||
|
harvest.gain_per_owned_citizen_name Knight g 1 → no change
|
||||||
|
roll.set_one_die target=6 cost=g:2 → no change
|
||||||
|
effect.add action.emeraldstronghold → no change
|
||||||
|
```
|
||||||
|
|
||||||
|
**Citizens and monsters:** no changes needed — syntax is already consistent within each table and the proposed rules codify what they already do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What stays as parsed strings vs opaque keys
|
||||||
|
|
||||||
|
All effects in the tables above are **parsed strings** — a new card with different numbers works without any code change.
|
||||||
|
|
||||||
|
The only candidates for opaque keys are effects with branching prompt logic unique to a single card that cannot be generalized with different parameters:
|
||||||
|
|
||||||
|
- `concurrent_flip_one_citizen` (Cursed Cavern) — multi-player concurrent event
|
||||||
|
- `modify_monster_strength 3` (Ancient Tomb) — board-state mutation prompt
|
||||||
|
|
||||||
|
Even these stay as strings under the unified grammar; they just dispatch to named functions rather than an inline parsing branch. The DB string is the key; the function is the implementation.
|
||||||
285
game.py
@@ -93,6 +93,28 @@ def _validate_monster_slay_payment(player, strength_cost, magic_min, gp, sp, mp)
|
|||||||
raise ValueError("Insufficient resources.")
|
raise ValueError("Insufficient resources.")
|
||||||
|
|
||||||
|
|
||||||
|
def _player_resource_balances(player):
|
||||||
|
if not player:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"g": int(getattr(player, "gold_score", 0)),
|
||||||
|
"s": int(getattr(player, "strength_score", 0)),
|
||||||
|
"m": int(getattr(player, "magic_score", 0)),
|
||||||
|
"v": int(getattr(player, "victory_score", 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _balances_allow_payout(balances, payout_vec):
|
||||||
|
"""balances: dict g,s,m,v; payout_vec: [dg, ds, dm, dv]."""
|
||||||
|
if not balances:
|
||||||
|
return False
|
||||||
|
keys = ("g", "s", "m", "v")
|
||||||
|
for i, k in enumerate(keys):
|
||||||
|
if int(balances.get(k, 0)) + int(payout_vec[i]) < 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Concurrent (non-ordered) action subsystem.
|
# Concurrent (non-ordered) action subsystem.
|
||||||
#
|
#
|
||||||
@@ -216,6 +238,9 @@ class Game:
|
|||||||
self.rolled_die_two = game_state.get('rolled_die_two', self.die_two)
|
self.rolled_die_two = game_state.get('rolled_die_two', self.die_two)
|
||||||
self.rolled_die_sum = game_state.get('rolled_die_sum', self.die_sum)
|
self.rolled_die_sum = game_state.get('rolled_die_sum', self.die_sum)
|
||||||
self.exhausted_count = game_state['exhausted_count']
|
self.exhausted_count = game_state['exhausted_count']
|
||||||
|
self.exhausted_stack = list(game_state.get('exhausted_stack') or [])
|
||||||
|
self.end_game_triggered = game_state.get('end_game_triggered', False)
|
||||||
|
self.final_scores = game_state.get('final_scores', None)
|
||||||
self.effects = game_state['effects']
|
self.effects = game_state['effects']
|
||||||
self.action_required = game_state['action_required']
|
self.action_required = game_state['action_required']
|
||||||
# Concurrent (non-ordered) prompt: all listed players must respond before progression.
|
# Concurrent (non-ordered) prompt: all listed players must respond before progression.
|
||||||
@@ -237,6 +262,8 @@ class Game:
|
|||||||
self.last_active_time = 0
|
self.last_active_time = 0
|
||||||
self.game_log = list(game_state.get('game_log') or [])
|
self.game_log = list(game_state.get('game_log') or [])
|
||||||
self.pending_action_end_queue = list(game_state.get("pending_action_end_queue") or [])
|
self.pending_action_end_queue = list(game_state.get("pending_action_end_queue") or [])
|
||||||
|
self.pending_required_choice = game_state.get("pending_required_choice")
|
||||||
|
self._silent_harvest_batch = False
|
||||||
# Between roll and harvest we allow a small "finalization window" where effects (or dev rigging)
|
# Between roll and harvest we allow a small "finalization window" where effects (or dev rigging)
|
||||||
# may legally change the dice. When present, the engine blocks in roll_pending until finalized.
|
# may legally change the dice. When present, the engine blocks in roll_pending until finalized.
|
||||||
self.pending_roll = game_state.get('pending_roll') or None
|
self.pending_roll = game_state.get('pending_roll') or None
|
||||||
@@ -285,6 +312,9 @@ class Game:
|
|||||||
Advance the game by one deterministic tick.
|
Advance the game by one deterministic tick.
|
||||||
This is intentionally small-grained so the server can call it implicitly.
|
This is intentionally small-grained so the server can call it implicitly.
|
||||||
"""
|
"""
|
||||||
|
if self.phase == 'game_over':
|
||||||
|
return False
|
||||||
|
|
||||||
# Block on any active concurrent (non-ordered) prompt first.
|
# Block on any active concurrent (non-ordered) prompt first.
|
||||||
if self.is_blocked_on_concurrent_action():
|
if self.is_blocked_on_concurrent_action():
|
||||||
return False
|
return False
|
||||||
@@ -292,8 +322,15 @@ class Game:
|
|||||||
# Block only on required player choices (not on standard action prompts)
|
# Block only on required player choices (not on standard action prompts)
|
||||||
if self.action_required and self.action_required.get("id") and self.action_required.get("id") != self.game_id:
|
if self.action_required and self.action_required.get("id") and self.action_required.get("id") != self.game_id:
|
||||||
aa = str(self.action_required.get("action", "") or "")
|
aa = str(self.action_required.get("action", "") or "")
|
||||||
if self.action_required.get("action") == "bonus_resource_choice" or aa.startswith("choose ") or aa.startswith(
|
if (
|
||||||
"choose_player") or aa.startswith("choose_monster") or aa == "domain_self_convert":
|
self.action_required.get("action") == "bonus_resource_choice"
|
||||||
|
or aa == "manual_harvest"
|
||||||
|
or aa == "harvest_optional_exchange"
|
||||||
|
or aa.startswith("choose ")
|
||||||
|
or aa.startswith("choose_player")
|
||||||
|
or aa.startswith("choose_monster")
|
||||||
|
or aa == "domain_self_convert"
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.phase == "setup":
|
if self.phase == "setup":
|
||||||
@@ -337,8 +374,17 @@ class Game:
|
|||||||
if self.pending_action_end_queue:
|
if self.pending_action_end_queue:
|
||||||
return False
|
return False
|
||||||
finisher = self._player_label(self.current_player_id())
|
finisher = self._player_label(self.current_player_id())
|
||||||
|
if not self.end_game_triggered:
|
||||||
|
reason = self._check_end_game_condition()
|
||||||
|
if reason:
|
||||||
|
self.end_game_triggered = True
|
||||||
|
self._log_game_event(f"End-game condition met ({reason}); finishing this round.")
|
||||||
self.turn_index = (self.turn_index + 1) % max(1, len(self.player_list))
|
self.turn_index = (self.turn_index + 1) % max(1, len(self.player_list))
|
||||||
self.turn_number = int(self.turn_number) + 1
|
self.turn_number = int(self.turn_number) + 1
|
||||||
|
if self.end_game_triggered and self.player_list[self.turn_index].is_first:
|
||||||
|
self._log_game_event(f"{finisher} ended their turn.")
|
||||||
|
self._finalize_game()
|
||||||
|
return True
|
||||||
self.phase = 'roll'
|
self.phase = 'roll'
|
||||||
self.actions_remaining = 0
|
self.actions_remaining = 0
|
||||||
self.action_required["id"] = self.game_id
|
self.action_required["id"] = self.game_id
|
||||||
@@ -401,8 +447,17 @@ class Game:
|
|||||||
if aid and aid != self.game_id and aact and aact != "standard_action":
|
if aid and aid != self.game_id and aact and aact != "standard_action":
|
||||||
return False
|
return False
|
||||||
finisher = self._player_label(self.current_player_id())
|
finisher = self._player_label(self.current_player_id())
|
||||||
|
if not self.end_game_triggered:
|
||||||
|
reason = self._check_end_game_condition()
|
||||||
|
if reason:
|
||||||
|
self.end_game_triggered = True
|
||||||
|
self._log_game_event(f"End-game condition met ({reason}); finishing this round.")
|
||||||
self.turn_index = (self.turn_index + 1) % max(1, len(self.player_list))
|
self.turn_index = (self.turn_index + 1) % max(1, len(self.player_list))
|
||||||
self.turn_number = int(self.turn_number) + 1
|
self.turn_number = int(self.turn_number) + 1
|
||||||
|
if self.end_game_triggered and self.player_list[self.turn_index].is_first:
|
||||||
|
self._log_game_event(f"{finisher} ended their turn.")
|
||||||
|
self._finalize_game()
|
||||||
|
return True
|
||||||
self.phase = 'roll'
|
self.phase = 'roll'
|
||||||
self.actions_remaining = 0
|
self.actions_remaining = 0
|
||||||
# Leaving action phase: clear the standard action prompt.
|
# Leaving action phase: clear the standard action prompt.
|
||||||
@@ -447,8 +502,13 @@ class Game:
|
|||||||
# If we're blocked on a required choice, no standard actions can be taken.
|
# If we're blocked on a required choice, no standard actions can be taken.
|
||||||
if self.action_required and self.action_required.get("id") and self.action_required.get("id") != self.game_id:
|
if self.action_required and self.action_required.get("id") and self.action_required.get("id") != self.game_id:
|
||||||
aa = str(self.action_required.get("action", "") or "")
|
aa = str(self.action_required.get("action", "") or "")
|
||||||
if self.action_required.get("action") in ("bonus_resource_choice", "manual_harvest") or aa.startswith(
|
if self.action_required.get("action") in (
|
||||||
"choose ") or aa.startswith("choose_player") or aa.startswith("choose_monster") or aa == "domain_self_convert":
|
"bonus_resource_choice",
|
||||||
|
"manual_harvest",
|
||||||
|
"harvest_optional_exchange",
|
||||||
|
) or aa.startswith("choose ") or aa.startswith("choose_player") or aa.startswith(
|
||||||
|
"choose_monster"
|
||||||
|
) or aa == "domain_self_convert":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if player_id != self.current_player_id():
|
if player_id != self.current_player_id():
|
||||||
@@ -970,7 +1030,7 @@ class Game:
|
|||||||
if not aid or aid == self.game_id:
|
if not aid or aid == self.game_id:
|
||||||
return False
|
return False
|
||||||
aa = self.action_required.get("action") or ""
|
aa = self.action_required.get("action") or ""
|
||||||
if aa in ("bonus_resource_choice", "manual_harvest"):
|
if aa in ("bonus_resource_choice", "manual_harvest", "harvest_optional_exchange"):
|
||||||
return True
|
return True
|
||||||
if str(aa).startswith("choose ") or str(aa).startswith("choose_player") or str(aa).startswith("choose_monster"):
|
if str(aa).startswith("choose ") or str(aa).startswith("choose_player") or str(aa).startswith("choose_monster"):
|
||||||
return True
|
return True
|
||||||
@@ -1099,6 +1159,8 @@ class Game:
|
|||||||
p.harvest_delta = {"gold": 0, "strength": 0, "magic": 0, "victory": 0}
|
p.harvest_delta = {"gold": 0, "strength": 0, "magic": 0, "victory": 0}
|
||||||
active = self._player_by_id(self.current_player_id())
|
active = self._player_by_id(self.current_player_id())
|
||||||
self._apply_harvest_jousting_passive(active)
|
self._apply_harvest_jousting_passive(active)
|
||||||
|
self._silent_harvest_batch = True
|
||||||
|
try:
|
||||||
order = self._harvest_player_id_order_starting_active()
|
order = self._harvest_player_id_order_starting_active()
|
||||||
for pid in order:
|
for pid in order:
|
||||||
player = self._player_by_id(pid)
|
player = self._player_by_id(pid)
|
||||||
@@ -1114,6 +1176,8 @@ class Game:
|
|||||||
for slot in slots:
|
for slot in slots:
|
||||||
self._apply_harvest_activation(player, slot["_obj"], slot["kind"], on_turn)
|
self._apply_harvest_activation(player, slot["_obj"], slot["kind"], on_turn)
|
||||||
consumed.append(slot["slot_key"])
|
consumed.append(slot["slot_key"])
|
||||||
|
finally:
|
||||||
|
self._silent_harvest_batch = False
|
||||||
for player in self.player_list:
|
for player in self.player_list:
|
||||||
print(f"Player {player.name}: {player.gold_score} G, {player.strength_score} S, {player.magic_score} M,"
|
print(f"Player {player.name}: {player.gold_score} G, {player.strength_score} S, {player.magic_score} M,"
|
||||||
f" {player.victory_score} VP, Monsters: {len(player.owned_monsters)}, "
|
f" {player.victory_score} VP, Monsters: {len(player.owned_monsters)}, "
|
||||||
@@ -1128,12 +1192,37 @@ class Game:
|
|||||||
return
|
return
|
||||||
self._harvest_run_automation_until_blocked()
|
self._harvest_run_automation_until_blocked()
|
||||||
|
|
||||||
def _execute_compound_payout(self, compound_command, player_id, auto_apply_single_choice=True):
|
def _want_harvest_optional_exchange_prompt(self, raw_command):
|
||||||
|
"""
|
||||||
|
During interactive harvest only: pure \"exchange pay gain\" specials pause for confirm/skip.
|
||||||
|
Batch harvest_phase() sets _silent_harvest_batch so exchanges auto-resolve when affordable.
|
||||||
|
"""
|
||||||
|
if getattr(self, "phase", None) != "harvest":
|
||||||
|
return False
|
||||||
|
if getattr(self, "_silent_harvest_batch", False):
|
||||||
|
return False
|
||||||
|
rc = (raw_command or "").strip()
|
||||||
|
if " + " in rc:
|
||||||
|
return False
|
||||||
|
parts = rc.split()
|
||||||
|
if len(parts) < 5:
|
||||||
|
return False
|
||||||
|
return parts[0].lower() == "exchange"
|
||||||
|
|
||||||
|
def _execute_compound_payout(
|
||||||
|
self,
|
||||||
|
compound_command,
|
||||||
|
player_id,
|
||||||
|
auto_apply_single_choice=True,
|
||||||
|
balance_hint=None,
|
||||||
|
suppress_exchange_optional_prompt=False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Execute multiple commands separated by +.
|
Execute multiple commands separated by +.
|
||||||
e.g. "s 3 + choose <citizens where role==soldier and gold_cost<=2>"
|
e.g. "s 3 + choose <citizens where role==soldier and gold_cost<=2>"
|
||||||
Non-choice commands are executed immediately and return [result].
|
Non-choice commands are executed immediately and return [result].
|
||||||
Choice commands set action_required and return [0,0,0,0].
|
Choice commands set action_required and return [0,0,0,0].
|
||||||
|
balance_hint: optional dict g,s,m,v carried across segments so exchange affordability sees prior legs.
|
||||||
"""
|
"""
|
||||||
parts = [p.strip() for p in (compound_command or "").split(" + ")]
|
parts = [p.strip() for p in (compound_command or "").split(" + ")]
|
||||||
if not parts:
|
if not parts:
|
||||||
@@ -1144,6 +1233,9 @@ class Game:
|
|||||||
return [-9999, 0, 0, 0]
|
return [-9999, 0, 0, 0]
|
||||||
prior_action = (self.action_required or {}).get("action", "")
|
prior_action = (self.action_required or {}).get("action", "")
|
||||||
prior_concurrent = getattr(self, "concurrent_action", None)
|
prior_concurrent = getattr(self, "concurrent_action", None)
|
||||||
|
bal = dict(balance_hint) if balance_hint is not None else _player_resource_balances(player)
|
||||||
|
if not bal:
|
||||||
|
return [-9999, 0, 0, 0]
|
||||||
for cmd in parts:
|
for cmd in parts:
|
||||||
if not cmd:
|
if not cmd:
|
||||||
continue
|
continue
|
||||||
@@ -1151,6 +1243,8 @@ class Game:
|
|||||||
cmd,
|
cmd,
|
||||||
player_id,
|
player_id,
|
||||||
auto_apply_single_choice=auto_apply_single_choice,
|
auto_apply_single_choice=auto_apply_single_choice,
|
||||||
|
balance_hint=bal,
|
||||||
|
suppress_exchange_optional_prompt=suppress_exchange_optional_prompt,
|
||||||
)
|
)
|
||||||
new_action = (self.action_required or {}).get("action", "")
|
new_action = (self.action_required or {}).get("action", "")
|
||||||
new_concurrent = getattr(self, "concurrent_action", None)
|
new_concurrent = getattr(self, "concurrent_action", None)
|
||||||
@@ -1165,13 +1259,24 @@ class Game:
|
|||||||
if prior_empty:
|
if prior_empty:
|
||||||
return payout
|
return payout
|
||||||
return total_payout
|
return total_payout
|
||||||
|
bal["g"] = int(bal["g"]) + int(payout[0])
|
||||||
|
bal["s"] = int(bal["s"]) + int(payout[1])
|
||||||
|
bal["m"] = int(bal["m"]) + int(payout[2])
|
||||||
|
bal["v"] = int(bal["v"]) + int(payout[3])
|
||||||
total_payout[0] += payout[0]
|
total_payout[0] += payout[0]
|
||||||
total_payout[1] += payout[1]
|
total_payout[1] += payout[1]
|
||||||
total_payout[2] += payout[2]
|
total_payout[2] += payout[2]
|
||||||
total_payout[3] += payout[3]
|
total_payout[3] += payout[3]
|
||||||
return total_payout
|
return total_payout
|
||||||
|
|
||||||
def execute_special_payout(self, command, player_id, auto_apply_single_choice=True):
|
def execute_special_payout(
|
||||||
|
self,
|
||||||
|
command,
|
||||||
|
player_id,
|
||||||
|
auto_apply_single_choice=True,
|
||||||
|
balance_hint=None,
|
||||||
|
suppress_exchange_optional_prompt=False,
|
||||||
|
):
|
||||||
print("executing special payout")
|
print("executing special payout")
|
||||||
raw = (command or "").strip()
|
raw = (command or "").strip()
|
||||||
low = raw.lower()
|
low = raw.lower()
|
||||||
@@ -1185,6 +1290,8 @@ class Game:
|
|||||||
raw,
|
raw,
|
||||||
player_id,
|
player_id,
|
||||||
auto_apply_single_choice=auto_apply_single_choice,
|
auto_apply_single_choice=auto_apply_single_choice,
|
||||||
|
balance_hint=balance_hint,
|
||||||
|
suppress_exchange_optional_prompt=suppress_exchange_optional_prompt,
|
||||||
)
|
)
|
||||||
payout = [0, 0, 0, 0] # gp, sp, mp, vp, todo: citizen, monster, domain
|
payout = [0, 0, 0, 0] # gp, sp, mp, vp, todo: citizen, monster, domain
|
||||||
split_command = (command or "").split()
|
split_command = (command or "").split()
|
||||||
@@ -1246,6 +1353,11 @@ class Game:
|
|||||||
case _:
|
case _:
|
||||||
payout[0] = -9999
|
payout[0] = -9999
|
||||||
case "exchange":
|
case "exchange":
|
||||||
|
player_x = self._player_by_id(player_id)
|
||||||
|
if not player_x:
|
||||||
|
payout[0] = -9999
|
||||||
|
print(payout)
|
||||||
|
return payout
|
||||||
match second_word:
|
match second_word:
|
||||||
case 'g':
|
case 'g':
|
||||||
payout[0] = payout[0] - int(third_word)
|
payout[0] = payout[0] - int(third_word)
|
||||||
@@ -1268,6 +1380,27 @@ class Game:
|
|||||||
payout[3] = payout[3] + int(split_command[4])
|
payout[3] = payout[3] + int(split_command[4])
|
||||||
case _:
|
case _:
|
||||||
payout[0] = -9999
|
payout[0] = -9999
|
||||||
|
if payout[0] == -9999:
|
||||||
|
print(payout)
|
||||||
|
return payout
|
||||||
|
bal_x = balance_hint if balance_hint is not None else _player_resource_balances(player_x)
|
||||||
|
if not _balances_allow_payout(bal_x, payout):
|
||||||
|
return [0, 0, 0, 0]
|
||||||
|
if (
|
||||||
|
not suppress_exchange_optional_prompt
|
||||||
|
and balance_hint is None
|
||||||
|
and self._want_harvest_optional_exchange_prompt(raw)
|
||||||
|
):
|
||||||
|
self.pending_required_choice = {
|
||||||
|
"kind": "harvest_optional_exchange",
|
||||||
|
"player_id": player_id,
|
||||||
|
"command": raw,
|
||||||
|
}
|
||||||
|
self.action_required["id"] = player_id
|
||||||
|
self.action_required["action"] = "harvest_optional_exchange"
|
||||||
|
return [0, 0, 0, 0]
|
||||||
|
print(payout)
|
||||||
|
return payout
|
||||||
case "choose":
|
case "choose":
|
||||||
normalized, options = self._normalize_choose_command(command)
|
normalized, options = self._normalize_choose_command(command)
|
||||||
options = self._filter_unavailable_choose_options(options)
|
options = self._filter_unavailable_choose_options(options)
|
||||||
@@ -2282,6 +2415,47 @@ class Game:
|
|||||||
self.action_required['id'] = self.game_id
|
self.action_required['id'] = self.game_id
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if current_required == "harvest_optional_exchange":
|
||||||
|
prc_h = getattr(self, "pending_required_choice", None) or {}
|
||||||
|
if prc_h.get("kind") != "harvest_optional_exchange" or prc_h.get("player_id") != player_id:
|
||||||
|
return
|
||||||
|
act_h = (action or "").strip().lower()
|
||||||
|
if act_h not in ("confirm_harvest_exchange", "skip_harvest_exchange"):
|
||||||
|
return
|
||||||
|
cmd_h = (prc_h.get("command") or "").strip()
|
||||||
|
target_h = self._player_by_id(player_id)
|
||||||
|
self.pending_required_choice = None
|
||||||
|
self.action_required["action"] = ""
|
||||||
|
self.action_required["id"] = self.game_id
|
||||||
|
if not target_h or not cmd_h:
|
||||||
|
self._maybe_resume_harvest_prompt()
|
||||||
|
return
|
||||||
|
before_h = self._player_scores_line(target_h)
|
||||||
|
if act_h == "skip_harvest_exchange":
|
||||||
|
self._log_game_event(
|
||||||
|
f"{self._player_label(player_id)} skipped optional harvest exchange ({cmd_h}); "
|
||||||
|
f"scores unchanged ({before_h})."
|
||||||
|
)
|
||||||
|
self._maybe_resume_harvest_prompt()
|
||||||
|
return
|
||||||
|
payout_h = self.execute_special_payout(
|
||||||
|
cmd_h,
|
||||||
|
player_id,
|
||||||
|
suppress_exchange_optional_prompt=True,
|
||||||
|
)
|
||||||
|
if isinstance(payout_h, list) and len(payout_h) >= 4 and payout_h[0] != -9999:
|
||||||
|
target_h.gold_score = int(target_h.gold_score) + int(payout_h[0])
|
||||||
|
target_h.strength_score = int(target_h.strength_score) + int(payout_h[1])
|
||||||
|
target_h.magic_score = int(target_h.magic_score) + int(payout_h[2])
|
||||||
|
target_h.victory_score = int(getattr(target_h, "victory_score", 0)) + int(payout_h[3])
|
||||||
|
self._bump_harvest_delta(target_h, payout_h[0], payout_h[1], payout_h[2], payout_h[3])
|
||||||
|
after_h = self._player_scores_line(target_h)
|
||||||
|
self._log_game_event(
|
||||||
|
f"{self._player_label(player_id)} took harvest exchange ({cmd_h}); scores {before_h} -> {after_h}"
|
||||||
|
)
|
||||||
|
self._maybe_resume_harvest_prompt()
|
||||||
|
return
|
||||||
|
|
||||||
prc0 = getattr(self, "pending_required_choice", None) or {}
|
prc0 = getattr(self, "pending_required_choice", None) or {}
|
||||||
if prc0.get("kind") == "domain_boost_monster" and str(current_required).strip() == "choose_monster_strength":
|
if prc0.get("kind") == "domain_boost_monster" and str(current_required).strip() == "choose_monster_strength":
|
||||||
act = (action or "").strip().lower()
|
act = (action or "").strip().lower()
|
||||||
@@ -2455,12 +2629,18 @@ class Game:
|
|||||||
self._log_game_event(f"All players finished: {ca.get('kind')}.")
|
self._log_game_event(f"All players finished: {ca.get('kind')}.")
|
||||||
handler.finalize(self)
|
handler.finalize(self)
|
||||||
self.concurrent_action = None
|
self.concurrent_action = None
|
||||||
# If we were stalled in setup, drive the engine forward so the
|
# Drive the engine forward after the concurrent action resolves.
|
||||||
# next state the client polls is something actionable.
|
|
||||||
if self.phase == "setup":
|
if self.phase == "setup":
|
||||||
|
# Setup stall: advance until the first actionable state.
|
||||||
while self.advance_tick():
|
while self.advance_tick():
|
||||||
if self.phase == "action":
|
if self.phase == "action":
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
# Mid-game concurrent action (e.g. Cursed Cavern flip during action phase):
|
||||||
|
# if the active player spent their last action before the concurrent prompt,
|
||||||
|
# finish_turn_if_no_actions_remaining will advance the turn now that the
|
||||||
|
# block is cleared. If they still have actions, this is a no-op.
|
||||||
|
self.finish_turn_if_no_actions_remaining()
|
||||||
|
|
||||||
def update_payout_for_role(self, role_name, player_id, payout, split_command):
|
def update_payout_for_role(self, role_name, player_id, payout, split_command):
|
||||||
role_count = 0
|
role_count = 0
|
||||||
@@ -2560,6 +2740,10 @@ class Game:
|
|||||||
|
|
||||||
if citizen_stack:
|
if citizen_stack:
|
||||||
citizen_stack[-1].toggle_accessibility(True)
|
citizen_stack[-1].toggle_accessibility(True)
|
||||||
|
elif self.exhausted_stack:
|
||||||
|
exhausted = self.exhausted_stack.pop()
|
||||||
|
citizen_stack.append(exhausted)
|
||||||
|
self.exhausted_count = int(self.exhausted_count) + 1
|
||||||
after = self._player_scores_line(player)
|
after = self._player_scores_line(player)
|
||||||
pay = self._format_resource_payment(gp, sp, mp)
|
pay = self._format_resource_payment(gp, sp, mp)
|
||||||
self._log_game_event(
|
self._log_game_event(
|
||||||
@@ -2688,6 +2872,10 @@ class Game:
|
|||||||
if domain_stack:
|
if domain_stack:
|
||||||
domain_stack[-1].toggle_visibility(True)
|
domain_stack[-1].toggle_visibility(True)
|
||||||
domain_stack[-1].toggle_accessibility(True)
|
domain_stack[-1].toggle_accessibility(True)
|
||||||
|
elif self.exhausted_stack:
|
||||||
|
exhausted = self.exhausted_stack.pop()
|
||||||
|
domain_stack.append(exhausted)
|
||||||
|
self.exhausted_count = int(self.exhausted_count) + 1
|
||||||
self._apply_domain_activation_effect(player, bought)
|
self._apply_domain_activation_effect(player, bought)
|
||||||
after = self._player_scores_line(player)
|
after = self._player_scores_line(player)
|
||||||
pay = self._format_resource_payment(gp, sp, mp)
|
pay = self._format_resource_payment(gp, sp, mp)
|
||||||
@@ -2736,6 +2924,85 @@ class Game:
|
|||||||
self.harvest_phase()
|
self.harvest_phase()
|
||||||
self.action_phase()
|
self.action_phase()
|
||||||
|
|
||||||
|
def _check_end_game_condition(self):
|
||||||
|
"""Returns a reason string if any end condition is met, else None."""
|
||||||
|
from cards import Exhausted
|
||||||
|
if all(not stack for stack in self.monster_grid):
|
||||||
|
return "all monsters slain"
|
||||||
|
if all(not stack for stack in self.domain_grid):
|
||||||
|
return "all domains built"
|
||||||
|
if int(self.exhausted_count) >= len(self.player_list) * 2:
|
||||||
|
return "exhausted stacks filled"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _calculate_final_scores(self):
|
||||||
|
"""Compute final VP for each player including Duke multipliers. Returns ranked list."""
|
||||||
|
self.unflip_all_citizens_for_final_scoring()
|
||||||
|
scores = []
|
||||||
|
for player in self.player_list:
|
||||||
|
duke_vp = 0
|
||||||
|
if player.owned_dukes:
|
||||||
|
duke = player.owned_dukes[0]
|
||||||
|
roles = player.calc_roles()
|
||||||
|
monster_attrs = self.owned_monster_attributes(player.player_id)
|
||||||
|
|
||||||
|
def _res(score, divisor):
|
||||||
|
d = int(divisor or 0)
|
||||||
|
return int(score) // d if d > 0 else 0
|
||||||
|
|
||||||
|
def _cnt(count, multiplier):
|
||||||
|
return int(count) * int(multiplier or 0)
|
||||||
|
|
||||||
|
duke_vp = (
|
||||||
|
_res(player.gold_score, duke.gold_multiplier)
|
||||||
|
+ _res(player.strength_score, duke.strength_multiplier)
|
||||||
|
+ _res(player.magic_score, duke.magic_multiplier)
|
||||||
|
+ _cnt(roles["shadow_count"], duke.shadow_multiplier)
|
||||||
|
+ _cnt(roles["holy_count"], duke.holy_multiplier)
|
||||||
|
+ _cnt(roles["soldier_count"], duke.soldier_multiplier)
|
||||||
|
+ _cnt(roles["worker_count"], duke.worker_multiplier)
|
||||||
|
+ _cnt(len(player.owned_monsters), duke.monster_multiplier)
|
||||||
|
+ _cnt(len(player.owned_citizens), duke.citizen_multiplier)
|
||||||
|
+ _cnt(len(player.owned_domains), duke.domain_multiplier)
|
||||||
|
+ _cnt(monster_attrs.get("Boss", 0), duke.boss_multiplier)
|
||||||
|
+ _cnt(monster_attrs.get("Minion", 0), duke.minion_multiplier)
|
||||||
|
+ _cnt(monster_attrs.get("Beast", 0), duke.beast_multiplier)
|
||||||
|
+ _cnt(monster_attrs.get("Titan", 0), duke.titan_multiplier)
|
||||||
|
)
|
||||||
|
total_vp = int(player.victory_score) + duke_vp
|
||||||
|
tableau_size = (
|
||||||
|
len(player.owned_starters)
|
||||||
|
+ len(player.owned_citizens)
|
||||||
|
+ len(player.owned_domains)
|
||||||
|
+ len(player.owned_monsters)
|
||||||
|
+ len(player.owned_dukes)
|
||||||
|
)
|
||||||
|
scores.append({
|
||||||
|
"player_id": player.player_id,
|
||||||
|
"name": player.name,
|
||||||
|
"base_vp": int(player.victory_score),
|
||||||
|
"duke_vp": duke_vp,
|
||||||
|
"total_vp": total_vp,
|
||||||
|
"tableau_size": tableau_size,
|
||||||
|
})
|
||||||
|
scores.sort(key=lambda s: (-s["total_vp"], s["tableau_size"]))
|
||||||
|
for rank, s in enumerate(scores):
|
||||||
|
s["rank"] = rank + 1
|
||||||
|
return scores
|
||||||
|
|
||||||
|
def _finalize_game(self):
|
||||||
|
"""Compute final scores, set phase to game_over, and log the result."""
|
||||||
|
self.final_scores = self._calculate_final_scores()
|
||||||
|
self.phase = "game_over"
|
||||||
|
if self.final_scores:
|
||||||
|
for s in self.final_scores:
|
||||||
|
place = {1: "1st", 2: "2nd", 3: "3rd"}.get(s["rank"], f"{s['rank']}th")
|
||||||
|
self._log_game_event(
|
||||||
|
f"{place}: {s['name']} — {s['total_vp']} VP "
|
||||||
|
f"({s['base_vp']} base + {s['duke_vp']} Duke)."
|
||||||
|
)
|
||||||
|
self._log_game_event(f"Game over! {self.final_scores[0]['name']} wins!")
|
||||||
|
|
||||||
def end_check(self):
|
def end_check(self):
|
||||||
if self.exhausted_count <= (len(self.player_list) * 2):
|
if self.exhausted_count <= (len(self.player_list) * 2):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
|
||||||
from cards import Citizen, Domain, Duke, Monster, Starter
|
from cards import Citizen, Domain, Duke, Exhausted, Monster, Starter
|
||||||
from game_models import GameMember, LobbyMember, Player
|
from game_models import GameMember, LobbyMember, Player
|
||||||
|
|
||||||
|
|
||||||
@@ -72,6 +72,8 @@ class GameObjectEncoder(JSONEncoder):
|
|||||||
return obj.to_dict()
|
return obj.to_dict()
|
||||||
if isinstance(obj, Domain):
|
if isinstance(obj, Domain):
|
||||||
return obj.to_dict()
|
return obj.to_dict()
|
||||||
|
if isinstance(obj, Exhausted):
|
||||||
|
return obj.to_dict()
|
||||||
if hasattr(obj, "game_id") and hasattr(obj, "player_list") and hasattr(obj, "monster_grid"):
|
if hasattr(obj, "game_id") and hasattr(obj, "player_list") and hasattr(obj, "monster_grid"):
|
||||||
ca_raw = getattr(obj, "concurrent_action", None)
|
ca_raw = getattr(obj, "concurrent_action", None)
|
||||||
ca_enc = ca_raw
|
ca_enc = ca_raw
|
||||||
@@ -91,6 +93,9 @@ class GameObjectEncoder(JSONEncoder):
|
|||||||
"rolled_die_sum": getattr(obj, "rolled_die_sum", obj.die_sum),
|
"rolled_die_sum": getattr(obj, "rolled_die_sum", obj.die_sum),
|
||||||
"pending_roll": getattr(obj, "pending_roll", None),
|
"pending_roll": getattr(obj, "pending_roll", None),
|
||||||
"exhausted_count": obj.exhausted_count,
|
"exhausted_count": obj.exhausted_count,
|
||||||
|
"exhausted_stack_size": len(getattr(obj, "exhausted_stack", None) or []),
|
||||||
|
"end_game_triggered": getattr(obj, "end_game_triggered", False),
|
||||||
|
"final_scores": getattr(obj, "final_scores", None),
|
||||||
"effects": obj.effects,
|
"effects": obj.effects,
|
||||||
"action_required": obj.action_required,
|
"action_required": obj.action_required,
|
||||||
"pending_required_choice": getattr(obj, "pending_required_choice", None),
|
"pending_required_choice": getattr(obj, "pending_required_choice", None),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from cards import Citizen, Domain, Duke, Monster, Starter
|
from cards import Citizen, Domain, Duke, Exhausted, Monster, Starter
|
||||||
from game_models import Player
|
from game_models import Player
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ def load_game_data(game_id, preset, player_list_from_lobby, debug_starting_resou
|
|||||||
domain_stack = []
|
domain_stack = []
|
||||||
duke_query = "select_random_dukes"
|
duke_query = "select_random_dukes"
|
||||||
duke_stack = []
|
duke_stack = []
|
||||||
|
exhausted_stack = [Exhausted(i) for i in range(len(player_list_from_lobby) * 2)]
|
||||||
starter_query = "SELECT * FROM starters"
|
starter_query = "SELECT * FROM starters"
|
||||||
starter_stack = []
|
starter_stack = []
|
||||||
player_list = []
|
player_list = []
|
||||||
@@ -256,6 +257,7 @@ def load_game_data(game_id, preset, player_list_from_lobby, debug_starting_resou
|
|||||||
|
|
||||||
game_state = {
|
game_state = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
|
"pending_required_choice": None,
|
||||||
"player_list": player_list,
|
"player_list": player_list,
|
||||||
"monster_grid": monster_grid,
|
"monster_grid": monster_grid,
|
||||||
"citizen_grid": citizen_grid,
|
"citizen_grid": citizen_grid,
|
||||||
@@ -264,6 +266,9 @@ def load_game_data(game_id, preset, player_list_from_lobby, debug_starting_resou
|
|||||||
"die_two": die_two,
|
"die_two": die_two,
|
||||||
"die_sum": die_sum,
|
"die_sum": die_sum,
|
||||||
"exhausted_count": exhausted_count,
|
"exhausted_count": exhausted_count,
|
||||||
|
"exhausted_stack": exhausted_stack,
|
||||||
|
"end_game_triggered": False,
|
||||||
|
"final_scores": None,
|
||||||
"effects": effects,
|
"effects": effects,
|
||||||
"action_required": action_required,
|
"action_required": action_required,
|
||||||
"concurrent_action": None,
|
"concurrent_action": None,
|
||||||
|
|||||||
BIN
images/.DS_Store
vendored
Normal file
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 546 KiB After Width: | Height: | Size: 546 KiB |
BIN
images/domains/domain_01_jousting_field.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/domains/domain_02_ancient_tomb.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
images/domains/domain_03_foxgrove_palisade.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
images/domains/domain_04_the_desert_orchid.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/domains/domain_05_pretorius_conclave.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
images/domains/domain_06_emerald_stronghold.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/domains/domain_07_pratchetts_plateau.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
images/domains/domain_08_shelley_commons.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/domains/domain_09_cathedral_of_st_aquila.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
images/domains/domain_10_cursed_cavern.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/domains/domain_11_darktide_harbor.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/domains/domain_12_king_tower.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
images/domains/domain_13_cloudriders_camp.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
images/domains/domain_14_the_orb_of_urdr.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/domains/domain_15_wisborg.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 509 KiB |
BIN
images/domains/domain_r01c01.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/domains/domain_r01c03.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/domains/domain_r01c04.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/domains/domain_r01c05.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/domains/domain_r01c07.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
images/domains/domain_r01c09.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/domains/domain_r01c10.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/domains/domain_r02c01.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
images/domains/domain_r02c02.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/domains/domain_r02c03.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
images/domains/domain_r02c05.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
images/domains/domain_r02c06.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
images/domains/domain_r03c01.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
images/domains/domain_r03c02.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/domains/domain_r03c03.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/domains/domain_r03c04.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/domains/domain_r03c05.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
images/domains/domain_r03c06.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/domains/domain_r03c07.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/domains/domain_r03c09.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
images/domains/domain_r03c10.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
images/domains/domain_r04c01.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
images/domains/domain_r04c02.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
images/domains/domain_r04c03.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/domains/domain_r04c04.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/domains/domain_r04c05.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
images/domains/domain_r04c06.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/domains/domain_r04c07.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
images/domains/domain_r04c08.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
images/domains/domain_r04c09.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
images/domains/domain_r04c10.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
images/domains/domain_r05c01.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
images/domains/domain_r05c02.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/domains/domain_r05c03.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/domains/domain_r05c04.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/domains/domain_r05c05.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/domains/domain_r05c06.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/domains/domain_r05c07.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
images/domains/domain_r05c08.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/domains/domain_r05c10.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
images/domains/domain_r06c01.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/domains/domain_r06c02.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/domains/domain_r06c03.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/domains/domain_r06c05.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
images/domains/domain_r06c06.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
images/domains/domain_r06c07.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/domains/domain_r06c09.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
images/domains/domain_r06c10.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
images/domains/domain_r07c01.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
images/domains/domain_r07c02.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
images/domains/domain_r07c03.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
images/domains/domain_r07c04.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
images/domains/domain_r07c06.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/domains/domain_r07c08.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
images/domains/domain_r07c09.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/dukes/duke_01_elisium_the_allsmith.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/dukes/duke_02_reese_the_firebrand.jpg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
images/dukes/duke_03_tsoukalos_the_conspirator.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
images/dukes/duke_04_cornelius_the_dreamer.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
images/dukes/duke_05_mico_the_monster_slayer.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/dukes/duke_06_waybright_the_wise.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
images/dukes/duke_07_mulholland_the_brave.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/dukes/duke_08_hrothgar_the_conqueror.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/dukes/duke_09_isabella_the_righteous.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
images/dukes/duke_10_elsyn_saint_of_shadows.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
images/dukes/duke_11_waryn_lord_of_rogues.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |