Skip to content

Commit 609a92e

Browse files
authored
Add Brain + intrigue on location support (#17)
* Add brain ability * Support for intrigue on locations * Fix for 3.11
1 parent a8931f0 commit 609a92e

File tree

9 files changed

+83
-35
lines changed

9 files changed

+83
-35
lines changed

app/models/api_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def from_game_state(game_id: str, game_state: GameState) -> "GameStateResponse":
3636
characters=[
3737
CharacterState(
3838
name=c.name,
39-
location=c.location.name,
39+
location=c.location.location_type,
4040
paranoia=c.paranoia,
4141
goodwill=c.goodwill,
4242
intrigue=c.intrigue,

engine/ability_effects.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
from .models import Character, RoleType
1+
from .models import Character, Location, RoleType
22
from .state import GameState
33

4-
def resolve_conspiracy_theorist_ability(source_char: Character, target_char: Character, game_state: GameState):
5-
"""
6-
The Conspiracy Theorist can use ADD_PARANOIA on anyone at their location.
7-
"""
4+
def resolve_conspiracy_theorist_ability(source: Character, target: Character | Location):
5+
assert isinstance(target, Character)
86
# Find characters at the same location (excluding themself)
9-
if source_char.location != target_char.location:
7+
if source.location != target.location:
108
raise ValueError(f"Error resolving conspiracy theorist ability: target at different location")
119
else:
12-
target_char.paranoia += 1
10+
target.paranoia += 1
11+
12+
def resolve_brain_ability(source: Character, target: Character | Location):
13+
if isinstance(target, Character) and source.location != target.location:
14+
raise ValueError(f"Error resolving conspiracy theorist ability: target at different location")
15+
else:
16+
target.intrigue += 1
17+
1318

1419
# The main registry for all ability logic
1520
ABILITY_LOGIC = {
1621
RoleType.CONSPIRACY_THEORIST: resolve_conspiracy_theorist_ability,
22+
RoleType.BRAIN: resolve_brain_ability,
1723
# Add other roles with abilities here
1824
}

engine/engine.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
from engine.ability_effects import ABILITY_LOGIC
44
from .incident_effects import INCIDENT_EFFECTS
5-
from .models import Action, Character, Location, ActionType, ALL_LOCATIONS, RoleType, TurnData
5+
from .models import Action, Character, Location, ActionType, ALL_LOCATIONS, LocationType, RoleType, TurnData
66
from .role_effects import ROLE_EFFECTS
77
from .state import GameState
88
from collections import defaultdict
99

1010
# Define the layout as a 2x2 grid
1111
LOCATION_GRID = [
12-
[Location.HOSPITAL, Location.SHRINE], # Top row
13-
[Location.CITY, Location.SCHOOL] # Bottom row
12+
[LocationType.HOSPITAL, LocationType.SHRINE], # Top row
13+
[LocationType.CITY, LocationType.SCHOOL] # Bottom row
1414
]
1515

16-
def find_location_coords(location: Location) -> tuple[int, int]:
16+
def find_location_coords(location: LocationType) -> tuple[int, int]:
1717
for row_idx, row in enumerate(LOCATION_GRID):
1818
for col_idx, loc in enumerate(row):
1919
if loc == location:
@@ -80,7 +80,7 @@ def resolve_actions(game_state: GameState, turn_data: TurnData):
8080
# Resolve actions targeting characters
8181
for char, action_list in char_to_actions.items():
8282
for action in action_list:
83-
resolve_action(char, action.type) # type: ignore
83+
resolve_action(char, action.type)
8484

8585
# TODO: Resolve location-based actions (not yet implemented)
8686
# for loc, action_list in loc_to_actions.items():
@@ -98,18 +98,22 @@ def resolve_abilities(game_state: GameState, turn_data: TurnData):
9898

9999
for choice in turn_data.ability_actions:
100100
source_char = next((c for c in game_state.characters if c.role == choice.source), None)
101-
target_char = next((c for c in game_state.characters if c.name == choice.target), None)
101+
if choice.target.lower() in {l.value for l in LocationType}:
102+
target_loc_enum = LocationType(choice.target.lower())
103+
target_obj = game_state.location_states[target_loc_enum]
104+
else:
105+
target_obj = next((c for c in game_state.characters if c.name == choice.target), None)
102106

103-
if not source_char or not target_char:
104-
print(f"Warning: Invalid character in ability choice, skipping.")
107+
if not source_char or not target_obj:
108+
print(f"Warning: Invalid character or location in ability choice, skipping.")
105109
continue
106110

107111
ability_fn = ABILITY_LOGIC.get(source_char.role)
108112
if not ability_fn:
109113
print(f"Warning: {source_char.name} ({source_char.role.name}) has no defined ability, skipping.")
110114
continue
111115

112-
ability_fn(source_char, target_char, game_state)
116+
ability_fn(source_char, target_obj)
113117

114118

115119
def resolve_roles(game_state: GameState):

engine/models.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ class RoleType(str, Enum):
2020
WITCH = "witch"
2121

2222

23-
class Location(str, Enum):
23+
class LocationType(str, Enum):
2424
CITY = "city"
2525
HOSPITAL = "hospital"
2626
SCHOOL = "school"
2727
SHRINE = "shrine"
2828

2929

30-
ALL_LOCATIONS = list(Location)
30+
ALL_LOCATIONS = list(LocationType)
3131

3232

3333
class ActionType(str, Enum):
@@ -58,9 +58,9 @@ class IncidentType(str, Enum):
5858

5959
class Character(BaseModel):
6060
name: str
61-
location: Location
62-
starting_location: Location
63-
disallowed_locations: List[Location]
61+
location: LocationType
62+
starting_location: LocationType
63+
disallowed_locations: List[LocationType]
6464
paranoia: int = 0
6565
paranoia_limit: int = 0
6666
goodwill: int = 0
@@ -77,6 +77,11 @@ def __eq__(self, other):
7777
return self.name == other.name
7878

7979

80+
class Location(BaseModel):
81+
location_type: LocationType
82+
intrigue: int = 0
83+
84+
8085
class Incident(BaseModel):
8186
type: IncidentType
8287
day: int
@@ -117,4 +122,4 @@ class TurnData(BaseModel):
117122
incident_choices: Optional[List[IncidentChoice]] = None
118123
ability_actions: Optional[List[AbilityChoice]] = None
119124

120-
AllActionsByDay = Dict[int, TurnData]
125+
AllActionsByDay = Dict[int, TurnData]

engine/simulation.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import argparse
2+
3+
from engine.models import Location, LocationType
24
from .state import GameState
35
from .victory_checker import check_victory
46
from .yaml_loader import load_actions_from_yaml, load_script_from_yaml
@@ -8,6 +10,9 @@ def create_starting_game_state(script_path: str, actions_path: str) -> GameState
810
day = 1
911
script = load_script_from_yaml(script_path)
1012
actions = load_actions_from_yaml(actions_path)
13+
location_states = {
14+
location_type: Location(location_type=location_type) for location_type in LocationType
15+
}
1116

1217
return GameState(
1318
day=day,
@@ -16,7 +21,8 @@ def create_starting_game_state(script_path: str, actions_path: str) -> GameState
1621
max_loops=script.max_loops,
1722
characters=script.characters,
1823
incidents=script.incidents,
19-
actions=actions
24+
actions=actions,
25+
location_states=location_states
2026
)
2127

2228
def simulate_day(game_state: GameState):

engine/state.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass, field
2-
from typing import List, Optional, Set
2+
from typing import Dict, List, Optional, Set
33

4-
from .models import Character, Incident, AllActionsByDay, RoleType
4+
from .models import Character, Incident, AllActionsByDay, Location, LocationType, RoleType
55

66

77
@dataclass
@@ -13,13 +13,14 @@ class GameState:
1313
characters: List[Character]
1414
incidents: List[Incident]
1515
actions: AllActionsByDay
16+
location_states: Dict[LocationType, Location]
1617
game_result: Optional[str] = None # "protagonists_win", "mastermind_win", or None
1718
revealed_roles: Set[RoleType] = field(default_factory=set)
1819

1920

2021
def print_characters(self):
2122
for char in self.characters:
22-
message = f"{char.name} is at {char.location}, paranoia={char.paranoia}, goodwill={char.goodwill}, intrigue={char.intrigue}"
23+
message = f"{char.name} is at {char.location.name}, paranoia={char.paranoia}, goodwill={char.goodwill}, intrigue={char.intrigue}"
2324
if not char.alive:
2425
message += ", status=DEAD"
2526
print(message)

engine/yaml_loader.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import yaml
22
from .models import (
33
AbilityChoice, Action, ActionSource, ActionTargetType, ActionType,
4-
AllActionsByDay, Character, IncidentChoice, IncidentType, Location,
4+
AllActionsByDay, Character, IncidentChoice, IncidentType, Location, LocationType,
55
RoleType, Incident, Script, TurnData
66
)
77

@@ -13,9 +13,9 @@ def load_script_from_yaml(path: str) -> Script:
1313
for entry in data["characters"]:
1414
character = Character(
1515
name=entry["name"],
16-
location=Location[entry["starting_location"]],
17-
starting_location=Location[entry["starting_location"]],
18-
disallowed_locations=[Location[loc] for loc in entry["disallowed_locations"]],
16+
location=LocationType[entry["starting_location"]],
17+
starting_location=LocationType[entry["starting_location"]],
18+
disallowed_locations=[LocationType[loc] for loc in entry["disallowed_locations"]],
1919
paranoia_limit=entry["paranoia_limit"],
2020
role=RoleType[entry["role"]]
2121
)
@@ -56,7 +56,7 @@ def parse_actions(source: ActionSource, actions_raw: list, day: int) -> list[Act
5656

5757
target_str = action_dict["target"]
5858
# Infer target_type based on name
59-
if target_str.lower() in {loc.value for loc in Location}:
59+
if target_str.lower() in {loc.value for loc in LocationType}:
6060
target_type = ActionTargetType.LOCATION
6161
else:
6262
target_type = ActionTargetType.CHARACTER

scripts/the_first_script/actions_4.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
type: ADD_INTRIGUE
99

1010
- target: Doctor
11-
type: ADD_INTRIGUE
11+
type: MOVE_VERTICAL
1212

1313
protagonist:
1414
- target: Shrine Maiden
@@ -20,6 +20,10 @@
2020
- target: Doctor
2121
type: ADD_GOODWILL
2222

23+
ability_actions:
24+
- source: BRAIN
25+
target: Office Worker
26+
2327
- day: 2
2428
mastermind:
2529
- target: Girl Student
@@ -41,6 +45,10 @@
4145
- target: Doctor
4246
type: ADD_GOODWILL
4347

48+
ability_actions:
49+
- source: BRAIN
50+
target: Office Worker
51+
4452
- day: 3
4553
mastermind:
4654
- target: Girl Student
@@ -62,6 +70,10 @@
6270
- target: Doctor
6371
type: ADD_GOODWILL
6472

73+
ability_actions:
74+
- source: BRAIN
75+
target: Office Worker
76+
6577
- day: 4
6678
mastermind:
6779
- target: Girl Student
@@ -82,3 +94,7 @@
8294

8395
- target: Doctor
8496
type: ADD_GOODWILL
97+
98+
ability_actions:
99+
- source: BRAIN
100+
target: CITY

tests/test_scenarios.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from engine.simulation import run_full_simulation
2-
from engine.models import RoleType
2+
from engine.models import LocationType, RoleType
33

44
def test_friend_dies_at_end_of_loop():
55
"""
@@ -58,4 +58,14 @@ def test_conspiracy_theorist_ability():
5858
final_state = run_full_simulation(script_path, actions_path)
5959
assert final_state.game_result == "mastermind_win"
6060
key_person = next(c for c in final_state.characters if c.role == RoleType.KEY_PERSON)
61-
assert not key_person.alive
61+
assert not key_person.alive
62+
63+
def test_brain_ability():
64+
script_path = "scripts/the_first_script/script.yaml"
65+
actions_path = "scripts/the_first_script/actions_4.yaml"
66+
final_state = run_full_simulation(script_path, actions_path)
67+
assert final_state.game_result == "protagonist_win"
68+
assert all(char.alive for char in final_state.characters)
69+
office_worker = next(c for c in final_state.characters if c.name == "Office Worker")
70+
assert office_worker.intrigue == 7
71+
assert final_state.location_states[LocationType.CITY].intrigue == 1

0 commit comments

Comments
 (0)