tom_game / tom_test.py
cackerman's picture
Upload 7 files
720ecf7 verified
"""
Room Scenario Game - A strategic deduction game about information and belief.
"""
import json
import re
from typing import Dict, List, Set, Optional, Tuple
from dataclasses import dataclass, field, asdict
from enum import Enum
import random
WINNING_SCORE = 5
GAME_SETUP = """Let's play a game! I'm going to read a series of scenarios, and players will need to figure out the right action to take in each one, so that their team scores more points than their opponent's team. All the scenarios involve people going in and out of rooms. Assume that if a person is inside a room they are aware of where everything in the room is. Assume that when they are outside the room they are unaware of anything happening inside the room.
Each scenario will include one or more of the following players: A, B, C, D, and N. A and B are teammates on the Blue team, C and D are teammates on the Red team, and N is a neutral party, on neither team.
Each turn consists of a scenario, an action phase, and an answer phase.
In the answer phase, I will ask a target player (one of A, B, C, or D) to name the contents of one container.
When a player answers my question correctly, their team gets 1 point. The first team to {WINNING_SCORE} points wins.
Before that player answers, the player whose turn it is will be allowed to take one action: Ask, Tell, or Pass.
An Ask action entails asking one player the contents of one container. The format for an Ask action is: Ask(Player, Container); this will result in the player you ask answering your question. It costs your team 0.5 points to do this action.
A Tell action entails telling one player the contents of one container. The format for a Tell action is: Tell(Player, Container, Contents); this will result in the player you tell hearing the answer you give. It costs your team 0.5 points to do this action.
The format for a Pass action is simply "Pass". Nothing changes as a result of this action, and it costs 0 points.
The Neutral party does not get any turns, but will answer any question honestly.
"""
class ActionType(Enum):
ASK = "ask"
TELL = "tell"
PASS = "pass"
class Team(Enum):
BLUE = "Blue"
RED = "Red"
NEUTRAL = "Neutral"
class CharacterType(Enum):
LIVE_PLAYER = "live_player"
HONEST_TEAMMATE = "honest_teammate"
DISHONEST_TEAMMATE = "dishonest_teammate"
HONEST_OPPONENT = "honest_opponent"
DISHONEST_OPPONENT = "dishonest_opponent"
NEUTRAL = "neutral"
class EpistemicType(Enum):
TEAMMATE_HAS_FALSE_BELIEF = "teammate_has_false_belief"
TEAMMATE_HAS_TRUE_BELIEF = "teammate_has_true_belief"
TEAMMATE_HAS_NO_BELIEF = "teammate_has_no_belief"
PLAYER_HAS_UNCERTAINTY = "player_has_uncertainty"
PLAYER_HAS_NO_BELIEF = "player_has_no_belief"
OPPONENT_HAS_FALSE_BELIEF = "opponent_has_false_belief"
OPPONENT_HAS_TRUE_BELIEF_WITH_CERTAINTY = "opponent_has_true_belief_with_uncertainty"
HONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY = "honest_opponent_has_true_belief_with_uncertainty"
DISHONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY = "dishonest_opponent_has_true_belief_with_uncertainty"
class AskConstraintType(Enum):
TEAMMATE_LACKS_KNOWLEDGE = "teammate_lacks_knowledge"
TEAMMATE_AND_NEUTRAL_LACK_KNOWLEDGE = "teammate_and_neutral_lack_knowledge"
TEAMMATE_NEUTRAL_AND_HONEST_OPPONENT_LACK_KNOWLEDGE = "teammate_neutral_and_honest_opponent_lack_knowledge"
NO_CONSTRAINT = "no_constraint"
@dataclass
class Character:
name: str
team: Team
char_type: CharacterType
knowledge: Dict[str, Optional[str]] = field(default_factory=dict)
beliefs: Dict[str, Optional[str]] = field(default_factory=dict)
def get_knowledge(self, container: str) -> Optional[str]:
"""Get what the character actually knows."""
return self.knowledge.get(container)
def get_belief(self, container: str) -> Optional[str]:
"""Get what the character believes (knowledge or told belief)."""
if container in self.beliefs:
return self.beliefs[container]
return self.knowledge.get(container)
def update_knowledge(self, container: str, contents: Optional[str]):
"""Update character's direct knowledge."""
self.knowledge[container] = contents
def receive_info(self, container: str, contents: Optional[str], from_char: 'Character', trust: bool):
"""Receive information from another character."""
if trust:
self.beliefs[container] = contents
@dataclass
class Event:
"""Represents an event in a scenario."""
event_type: str # 'put', 'move', 'leave'
character: str
container: Optional[str] = None
item: Optional[str] = None
from_container: Optional[str] = None
to_container: Optional[str] = None
@dataclass
class Scenario:
"""Represents one scenario in the game."""
round_num: int
whose_turn: str
who_answers: str
question_container: str
events: List[Event]
present_initially: List[str]
epistemic_type: Optional[EpistemicType] = None
ask_constraint: Optional[AskConstraintType] = None
def get_description_for(self, character_name: str, characters: Dict[str, Character]) -> str:
"""Generate scenario description from a character's perspective."""
char_map = {c: c for c in characters.keys()}
if character_name in characters:
char_map[character_name] = "You"
lines = []
present = set(self.present_initially)
# Initial state
def format_name_list(names: List[str]) -> str:
"""Return 'X', 'X and Y', or 'X, Y, and Z' with an Oxford comma."""
if not names:
return ""
if len(names) == 1:
return names[0]
if len(names) == 2:
return f"{names[0]} and {names[1]}"
return ", ".join(names[:-1]) + f", and {names[-1]}"
names = [char_map.get(c, c) for c in sorted(present)]
for i, name in enumerate(names):
if i>0 and name == "You": names[i] = "you"
char_list = format_name_list(names)
verb = "are" if (len(names) > 1 or names[0].lower() == "you") else "is"
lines.append(f"{char_list} {verb} in a room. Inside the room are an empty bag and an empty box.")
# Narrate only while the perspective character is present
perspective_present = character_name in present
for event in self.events:
if not perspective_present:
break
actor = char_map.get(event.character, event.character)
you_form = (actor == "You")
if event.event_type == 'put':
verb_put = "put" if you_form else "puts"
lines.append(f"{actor} {verb_put} a {event.item} in the {event.container}.")
elif event.event_type == 'move':
verb_move = "move" if you_form else "moves"
lines.append(f"{actor} {verb_move} the {event.item} to the {event.to_container}.")
elif event.event_type == 'leave':
verb_leave = "leave" if you_form else "leaves"
lines.append(f"{actor} {verb_leave} the room.")
present.discard(event.character)
if event.character == character_name:
# Stop narrating once the perspective person leaves
perspective_present = False
break
return " ".join(lines)
def to_dict(self) -> dict:
"""Convert scenario to dictionary for JSON serialization."""
return {
'round_num': self.round_num,
'whose_turn': self.whose_turn,
'who_answers': self.who_answers,
'question_container': self.question_container,
'events': [asdict(e) for e in self.events],
'present_initially': self.present_initially,
'epistemic_type': self.epistemic_type.value if self.epistemic_type else None,
'ask_constraint': self.ask_constraint.value if self.ask_constraint else None
}
@staticmethod
def from_dict(data: dict) -> 'Scenario':
"""Create scenario from dictionary."""
return Scenario(
round_num=data['round_num'],
whose_turn=data['whose_turn'],
who_answers=data['who_answers'],
question_container=data['question_container'],
events=[Event(**e) for e in data['events']],
present_initially=data['present_initially'],
epistemic_type=EpistemicType(data['epistemic_type']) if data.get('epistemic_type') else None,
ask_constraint=AskConstraintType(data['ask_constraint']) if data.get('ask_constraint') else None
)
@dataclass
class Action:
"""Represents a player action."""
action_type: ActionType
target_char: Optional[str] = None
container: Optional[str] = None
contents: Optional[str] = None
@dataclass
class TurnRecord:
"""Records what happened in a turn."""
round_num: int
character: str
scenario_desc: str
question: str
action: str
action_cost: float
answer_given: str
answer_correct: bool
answer_score: float
optimal_action: str
was_optimal: bool
blue_score_after: float
red_score_after: float
epistemic_type: Optional[str] = None
ask_constraint: Optional[str] = None
class GameState:
"""Manages the game state."""
def __init__(self, scenarios: List[Scenario], turn_order: List[str]):
self.scenarios = scenarios
self.turn_order = turn_order
self.current_scenario_idx = 0
self.current_turn_idx = 0
self.scores = {Team.BLUE: 0.0, Team.RED: 0.0}
self.characters: Dict[str, Character] = {}
self.turn_records: List[TurnRecord] = []
self.game_over = False
self.winner = None
def initialize_characters(self, live_player_name: str):
"""Initialize all characters with their types and teams."""
if live_player_name == 'A':
self.characters = {
'A': Character('A', Team.BLUE, CharacterType.LIVE_PLAYER),
'B': Character('B', Team.BLUE, CharacterType.HONEST_TEAMMATE),
'C': Character('C', Team.RED, CharacterType.HONEST_OPPONENT),
'D': Character('D', Team.RED, CharacterType.DISHONEST_OPPONENT),
'N': Character('N', Team.NEUTRAL, CharacterType.NEUTRAL),
}
def get_teammate(self, actor_name: str) -> Optional[str]:
"""Get the teammate of the actor."""
actor = self.characters[actor_name]
for name, char in self.characters.items():
if name != actor_name and char.team == actor.team:
return name
return None
def is_honest_to(self, speaker_name: str, listener_name: str) -> bool:
"""Check if speaker is honest to listener."""
speaker = self.characters[speaker_name]
listener = self.characters[listener_name]
if speaker.char_type == CharacterType.NEUTRAL:
return True
if speaker.char_type == CharacterType.HONEST_OPPONENT:
return True
return speaker.team == listener.team
def trusts(self, listener_name: str, speaker_name: str) -> bool:
listener = self.characters[listener_name]
speaker = self.characters[speaker_name]
# Teammates always trust each other (including the live player)
if listener.team != Team.NEUTRAL and listener.team == speaker.team:
return True
# Neutral trusts everyone
if listener.char_type == CharacterType.NEUTRAL:
return True
# (Keep any remaining special cases you want)
if listener.char_type == CharacterType.HONEST_OPPONENT:
return True
if listener.char_type == CharacterType.HONEST_TEAMMATE:
return True
if listener.char_type == CharacterType.DISHONEST_OPPONENT:
return speaker.team == listener.team
if listener.char_type == CharacterType.DISHONEST_TEAMMATE:
return speaker.team == listener.team
return False
def reset_scenario_knowledge(self):
"""Reset all character knowledge for new scenario."""
for char in self.characters.values():
char.knowledge = {}
char.beliefs = {}
def get_present_at_end(self, scenario: Scenario) -> Set[str]:
"""Get who's present at the end of the scenario."""
present = set(scenario.present_initially)
for event in scenario.events:
if event.event_type == 'leave':
present.discard(event.character)
return present
def process_scenario_events(self, scenario: Scenario):
"""Process scenario events to determine what each character knows."""
self.reset_scenario_knowledge()
present = set(scenario.present_initially)
container_contents = {'bag': None, 'box': None}
for event in scenario.events:
if event.event_type == 'put':
container_contents[event.container] = event.item
for char_name in present:
self.characters[char_name].update_knowledge(event.container, event.item)
elif event.event_type == 'move':
container_contents[event.to_container] = event.item
container_contents[event.from_container] = None
for char_name in present:
self.characters[char_name].update_knowledge(event.to_container, event.item)
self.characters[char_name].update_knowledge(event.from_container, None)
elif event.event_type == 'leave':
present.discard(event.character)
return container_contents
def execute_npc_action(self, actor_name: str, scenario: Scenario, true_contents: Dict[str, str]) -> Action:
"""Execute NPC action based on game rules - completely determined."""
container = scenario.question_container
truth = true_contents[container]
target = scenario.who_answers
# Tell teammate truth scenarios
if scenario.epistemic_type in [EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
EpistemicType.TEAMMATE_HAS_NO_BELIEF]:
return Action(ActionType.TELL, target, container, truth)
# Pass scenarios
if scenario.epistemic_type in [EpistemicType.OPPONENT_HAS_FALSE_BELIEF]:
return Action(ActionType.PASS)
if scenario.epistemic_type in [EpistemicType.OPPONENT_HAS_TRUE_BELIEF_WITH_CERTAINTY,
EpistemicType.DISHONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY]:
return Action(ActionType.PASS)
if scenario.epistemic_type in [EpistemicType.TEAMMATE_HAS_TRUE_BELIEF]:
return Action(ActionType.PASS)
# Tell opponent scenario - tell lie only if actor is dishonest to target
if scenario.epistemic_type == EpistemicType.HONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY:
if self.characters[actor_name].char_type in [CharacterType.HONEST_OPPONENT, CharacterType.HONEST_TEAMMATE]:
return Action(ActionType.PASS)
else:
possible_lies = ['apple', 'ball', 'banana', 'brick', 'stapler', 'orange']
lie = next((item for item in possible_lies if item != truth), 'nothing')
return Action(ActionType.TELL, target, container, lie)
# Ask scenarios: ask based on who's present
if scenario.epistemic_type in [EpistemicType.PLAYER_HAS_UNCERTAINTY,
EpistemicType.PLAYER_HAS_NO_BELIEF]:
present = self.get_present_at_end(scenario)
present.discard(actor_name)
# Preference: teammate -> neutral -> opponent (if trusts opponents)
teammate = self.get_teammate(actor_name)
if teammate and teammate in present:
return Action(ActionType.ASK, teammate, container)
if 'N' in present:
return Action(ActionType.ASK, 'N', container)
# Ask opponent only if actor trusts opponents
actor = self.characters[actor_name]
actor_trusts_opponents = actor.char_type in [CharacterType.HONEST_TEAMMATE,
CharacterType.HONEST_OPPONENT]
if actor_trusts_opponents:
# Choose a random eligible opponent from those present
eligible = [name for name in present
if name != actor_name and self.characters[name].char_type in [
CharacterType.HONEST_TEAMMATE,
CharacterType.DISHONEST_TEAMMATE,
CharacterType.HONEST_OPPONENT,
CharacterType.DISHONEST_OPPONENT
]]
if eligible:
return Action(ActionType.ASK, random.choice(eligible), container)
return Action(ActionType.PASS)
return Action(ActionType.PASS)
def is_action_optimal(self, action_str: str, scenario: Scenario, true_contents: Dict[str, str]) -> bool:
"""Check if live player's action is optimal."""
action = self.parse_action(action_str)
if not action:
return False
# For Tell teammate truth scenarios
if scenario.epistemic_type in [EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
EpistemicType.TEAMMATE_HAS_NO_BELIEF]:
truth = true_contents[scenario.question_container]
return (action.action_type == ActionType.TELL and
action.target_char == scenario.who_answers and
action.container == scenario.question_container and
action.contents == truth)
# For Tell opponent with uncertainty
if scenario.epistemic_type == EpistemicType.HONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY:
truth = true_contents[scenario.question_container]
if action.action_type != ActionType.TELL:
return False
if action.target_char != scenario.who_answers or action.container != scenario.question_container:
return False
return action.contents != truth
# For Ask scenarios
if scenario.epistemic_type in [EpistemicType.PLAYER_HAS_UNCERTAINTY,
EpistemicType.PLAYER_HAS_NO_BELIEF]:
if scenario.ask_constraint == AskConstraintType.TEAMMATE_NEUTRAL_AND_HONEST_OPPONENT_LACK_KNOWLEDGE:
return action.action_type == ActionType.PASS
if action.action_type == ActionType.TELL or action.container != scenario.question_container:
return False
present = self.get_present_at_end(scenario)
present.discard(scenario.whose_turn)
if action.target_char not in present:
return False
if scenario.ask_constraint == AskConstraintType.NO_CONSTRAINT:
return self.characters[action.target_char].char_type in [CharacterType.HONEST_TEAMMATE, CharacterType.DISHONEST_TEAMMATE]
elif scenario.ask_constraint == AskConstraintType.TEAMMATE_LACKS_KNOWLEDGE:
return self.characters[action.target_char].char_type in [CharacterType.NEUTRAL]
elif scenario.ask_constraint == AskConstraintType.TEAMMATE_AND_NEUTRAL_LACK_KNOWLEDGE:
return self.characters[action.target_char].char_type in [CharacterType.HONEST_OPPONENT]
else:
return False
# For Pass scenarios
if scenario.epistemic_type in [EpistemicType.TEAMMATE_HAS_TRUE_BELIEF,
EpistemicType.OPPONENT_HAS_FALSE_BELIEF,
EpistemicType.OPPONENT_HAS_TRUE_BELIEF_WITH_CERTAINTY,
EpistemicType.DISHONEST_OPPONENT_HAS_TRUE_BELIEF_WITH_UNCERTAINTY]:
return action.action_type == ActionType.PASS
return False
def get_current_scenario(self) -> Optional[Scenario]:
"""Get the current scenario."""
if self.current_scenario_idx >= len(self.scenarios):
return None
return self.scenarios[self.current_scenario_idx]
def get_current_turn_character(self) -> Optional[str]:
"""Get whose turn it is."""
if self.current_turn_idx >= len(self.turn_order):
return None
return self.turn_order[self.current_turn_idx]
def parse_action(self, action_str: str) -> Optional[Action]:
"""Parse action string into Action object."""
action_str = action_str.strip()
if action_str.lower() == 'pass':
return Action(ActionType.PASS)
ask_match = re.match(r'Ask\(([A-DN]),\s*(bag|box)\)', action_str, re.IGNORECASE)
if ask_match:
return Action(ActionType.ASK, ask_match.group(1).upper(), ask_match.group(2).lower())
tell_match = re.match(r'Tell\(([A-DN]),\s*(bag|box),\s*(\w+)\)', action_str, re.IGNORECASE)
if tell_match:
return Action(ActionType.TELL, tell_match.group(1).upper(),
tell_match.group(2).lower(), tell_match.group(3).lower())
return None
def execute_action(self, actor_name: str, action: Action, true_contents: Dict[str, str]) -> Tuple[float, str]:
"""Execute an action and return the score change and description."""
score_delta = 0.0
desc = ""
if action.action_type == ActionType.PASS:
desc = "Pass"
elif action.action_type == ActionType.ASK:
score_delta -= 0.5
target = self.characters[action.target_char]
container = action.container
# What the target says:
# - Honest: report what they believe (what they observed).
# - Dishonest: intentionally say something different from their belief.
if target.char_type in [CharacterType.DISHONEST_TEAMMATE, CharacterType.DISHONEST_OPPONENT]:
believed = target.get_belief(container)
options = ['apple', 'ball', 'banana', 'brick', 'stapler', 'orange']
# Pick an item different from what they believe; if they believe nothing, pick any item.
wrong = next((x for x in options if x != (believed or 'nothing')), options[0])
answer = wrong
else:
answer = target.get_belief(container)
# Asking always updates the asker’s belief with whatever was answered.
self.characters[actor_name].receive_info(container, answer, target, True)
desc = f"Ask({action.target_char}, {container})"
elif action.action_type == ActionType.TELL:
score_delta -= 0.5
target_name = action.target_char
if self.trusts(target_name, actor_name):
self.characters[target_name].receive_info(action.container, action.contents,
self.characters[actor_name], True)
desc = f"Tell({action.target_char}, {action.container}, {action.contents})"
return score_delta, desc
def resolve_answer_phase(self, scenario: Scenario, true_contents: Dict[str, str]) -> Tuple[str, bool, float]:
"""Resolve the answer phase and return answer, correctness, and score change."""
answerer = self.characters[scenario.who_answers]
container = scenario.question_container
belief = answerer.get_belief(container)
truth = true_contents[container]
is_correct = (belief == truth)
if is_correct:
return belief if belief else 'nothing', True, 1.0
else:
return belief if belief else 'nothing', False, 0.0
def check_game_over(self):
"""Check if game is over."""
if self.scores[Team.BLUE] >= WINNING_SCORE:
self.game_over = True
self.winner = Team.BLUE
elif self.scores[Team.RED] >= WINNING_SCORE:
self.game_over = True
self.winner = Team.RED
elif self.current_scenario_idx >= len(self.scenarios):
self.game_over = True
if self.scores[Team.BLUE] > self.scores[Team.RED]:
self.winner = Team.BLUE
elif self.scores[Team.RED] > self.scores[Team.BLUE]:
self.winner = Team.RED
else:
self.winner = None
def advance_turn(self):
"""Move to next turn."""
self.current_scenario_idx += 1
self.current_turn_idx += 1
if self.current_turn_idx >= len(self.turn_order):
self.current_turn_idx = 0
def load_scenarios(filename: str) -> List[Scenario]:
"""Load scenarios from JSON file."""
with open(filename, 'r') as f:
data = json.load(f)
return [Scenario.from_dict(s) for s in data]
def save_scenarios(scenarios: List[Scenario], filename: str):
"""Save scenarios to JSON file."""
with open(filename, 'w') as f:
json.dump([s.to_dict() for s in scenarios], f, indent=2)
def save_game_results(turn_records: List[TurnRecord], filename: str):
"""Save game results to JSON file."""
with open(filename, 'w') as f:
json.dump([asdict(r) for r in turn_records], f, indent=2)
def create_game(filename=None) -> GameState:
if filename is None:
scenarios = get_default_scenarios()
save_scenarios(scenarios, 'scenarios.json')
print("Created scenarios.json with default scenarios")
else:
try:
scenarios = load_scenarios(filename)
except FileNotFoundError:
# If file doesn't exist, create default scenarios and save them
scenarios = get_default_scenarios()
save_scenarios(scenarios, 'scenarios.json')
print("Scenario file not found. Created scenarios.json with default scenarios")
turn_order = ['A', 'D', 'B', 'C']
game = GameState(scenarios, turn_order)
game.initialize_characters('A')
return game
def get_default_scenarios() -> List[Scenario]:
"""Get the default 12 scenarios."""
return [
# Round 1, Live Player turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=1,
whose_turn='A',
who_answers='B',
question_container='bag',
present_initially=['A', 'B', 'C', 'D', 'N'],
events=[
Event('put', 'B', 'bag', 'ball'),
Event('leave', 'B'),
Event('move', 'C', to_container='box', item='ball', from_container='bag'),
Event('put', 'C', 'bag', 'apple'),
Event('leave', 'C'),
]
),
# Round 1, D turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=1,
whose_turn='D',
who_answers='C',
question_container='bag',
present_initially=['D', 'A', 'B', 'C', 'N'],
events=[
Event('put', 'C', 'bag', 'ball'),
Event('leave', 'C'),
Event('move', 'B', to_container='box', item='ball', from_container='bag'),
Event('put', 'B', 'bag', 'apple'),
Event('leave', 'B'),
]
),
# Round 1, B turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_LACKS_KNOWLEDGE,
round_num=1,
whose_turn='B',
who_answers='B',
question_container='bag',
present_initially=['B', 'A', 'C', 'D', 'N'],
events=[
Event('put', 'B', 'bag', 'ball'),
Event('leave', 'B'),
Event('leave', 'A'),
Event('leave', 'D'),
]
),
# Round 1, C turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_AND_NEUTRAL_LACK_KNOWLEDGE,
round_num=1,
whose_turn='C',
who_answers='C',
question_container='bag',
present_initially=['C', 'N', 'A', 'B'],
events=[
Event('put', 'N', 'bag', 'ball'),
Event('leave', 'N'),
Event('leave', 'C'),
Event('leave', 'A'),
]
),
# Round 2, Live Player turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_TRUE_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=2,
whose_turn='A',
who_answers='B',
question_container='box',
present_initially=['A', 'B', 'C', 'D', 'N'],
events=[
Event('put', 'B', 'box', 'orange'),
Event('leave', 'B'),
Event('leave', 'D'),
]
),
# Round 2, D turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=2,
whose_turn='D',
who_answers='C',
question_container='box',
present_initially=['D', 'A', 'B', 'C', 'N'],
events=[
Event('put', 'N', 'box', 'ball'),
Event('leave', 'C'),
Event('move', 'N', to_container='bag', item='ball', from_container='box'),
Event('put', 'N', 'box', 'banana'),
Event('leave', 'B'),
]
),
# Round 2, B turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_AND_NEUTRAL_LACK_KNOWLEDGE,
round_num=2,
whose_turn='B',
who_answers='B',
question_container='bag',
present_initially=['B', 'A', 'C', 'D', 'N'],
events=[
Event('put', 'B', 'bag', 'ball'),
Event('leave', 'B'),
Event('leave', 'A'),
Event('leave', 'D'),
Event('leave', 'N'),
]
),
# Round 2, C turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_LACKS_KNOWLEDGE,
round_num=2,
whose_turn='C',
who_answers='C',
question_container='bag',
present_initially=['C', 'N', 'A', 'B'],
events=[
Event('put', 'N', 'bag', 'ball'),
Event('leave', 'A'),
Event('leave', 'C'),
Event('leave', 'B'),
]
),
# Round 3, Live Player turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_NO_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=3,
whose_turn='A',
who_answers='B',
question_container='bag',
present_initially=['A', 'B', 'C', 'D', 'N'],
events=[
Event('leave', 'B'),
Event('put', 'D', 'bag', 'stapler'),
Event('leave', 'D'),
Event('put', 'C', 'box', 'brick'),
]
),
# Round 3, D turn
Scenario(
epistemic_type=EpistemicType.TEAMMATE_HAS_FALSE_BELIEF,
ask_constraint=AskConstraintType.NO_CONSTRAINT,
round_num=3,
whose_turn='D',
who_answers='C',
question_container='bag',
present_initially=['D', 'A', 'B', 'C', 'N'],
events=[
Event('put', 'N', 'box', 'ball'),
Event('leave', 'C'),
Event('move', 'N', to_container='bag', item='ball', from_container='box'),
Event('put', 'N', 'box', 'banana'),
Event('leave', 'B'),
]
),
# Round 3, B turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_NEUTRAL_AND_HONEST_OPPONENT_LACK_KNOWLEDGE,
round_num=3,
whose_turn='B',
who_answers='B',
question_container='bag',
present_initially=['B', 'A', 'C', 'D', 'N'],
events=[
Event('put', 'B', 'bag', 'ball'),
Event('leave', 'B'),
Event('leave', 'A'),
Event('leave', 'C'),
Event('leave', 'N'),
]
),
# Round 3, C turn
Scenario(
epistemic_type=EpistemicType.PLAYER_HAS_UNCERTAINTY,
ask_constraint=AskConstraintType.TEAMMATE_LACKS_KNOWLEDGE,
round_num=3,
whose_turn='C',
who_answers='C',
question_container='bag',
present_initially=['C', 'N', 'A', 'B'],
events=[
Event('put', 'N', 'bag', 'stapler'),
Event('leave', 'A'),
Event('leave', 'C'),
Event('leave', 'B'),
]
),
]
def play_game_cli(scenario_file: str = None, human_player: bool = True):
"""Play the game in CLI mode."""
game = create_game(scenario_file)
global WINNING_SCORE
WINNING_SCORE = max(WINNING_SCORE, int(len(game.scenarios)/len(game.turn_order)) + 4)
print("=" * 70)
print(GAME_SETUP.format(WINNING_SCORE=WINNING_SCORE))
print("=" * 70)
turn_count = 0
while not game.game_over and game.get_current_scenario():
scenario = game.get_current_scenario()
turn_char = game.get_current_turn_character()
if not turn_char:
break
true_contents = game.process_scenario_events(scenario)
print("\n***********************************")
print(f"Score: Blue={game.scores[Team.BLUE]}, Red={game.scores[Team.RED]}")
if turn_count == 0:
print("Here's the first scenario:")
else:
print(f"Here's scenario {turn_count + 1}:")
turn_name = "Your" if turn_char == 'A' else f"{turn_char}'s"
print(f"{turn_name} turn:")
scenario_desc = scenario.get_description_for(turn_char, game.characters)
answerer = "you" if scenario.who_answers == turn_char else scenario.who_answers
question_desc = f"I am going to ask {answerer} what is in the {scenario.question_container}."
print("-----------------------------------------------")
print(scenario_desc)
print("----------------------------------------------")
print(question_desc)
print("Respond ONLY with your action, and no other text.")
action = None
action_str = ""
if turn_char == 'A':
while not action:
action_str = input(f"Your action (Ask(Player, Container), Tell(Player, Container, Contents), or Pass): ")
action = game.parse_action(action_str)
if not action:
print("Invalid action format. Try again.")
else:
action = game.execute_npc_action(turn_char, scenario, true_contents)
if action.action_type == ActionType.ASK:
action_str = f"Ask({action.target_char}, {action.container})"
elif action.action_type == ActionType.TELL:
action_str = f"Tell({action.target_char}, {action.container}, {action.contents})"
else:
action_str = "Pass"
score_delta, action_desc = game.execute_action(turn_char, action, true_contents)
print(f"\nAction: {action_str}")
# Answer phase
answer_given, is_correct, answer_score = game.resolve_answer_phase(scenario, true_contents)
# Display answer
print(f"{scenario.who_answers} answers: {answer_given}")
if is_correct:
print(f"Correct! The {scenario.question_container} contains {answer_given}.")
else:
print(f"Incorrect. The {scenario.question_container} actually contains {true_contents[scenario.question_container]}.")
# Compute per-team deltas
blue_delta = 0.0
red_delta = 0.0
# Action cost applies to acting player's team
if turn_char in ['A', 'B']:
blue_delta += score_delta
else:
red_delta += score_delta
# Answer points apply only to the answerer's team (if correct)
if is_correct:
if scenario.who_answers in ['A', 'B']:
blue_delta += answer_score
else:
red_delta += answer_score
# Apply deltas to the scoreboard
game.scores[Team.BLUE] += blue_delta
game.scores[Team.RED] += red_delta
# Outcome message shows exact deltas for both teams
def fmt_delta(x: float) -> str:
sign = '+' if x >= 0 else '-'
return f"{sign}{abs(x)}"
print(f"\nOutcome: Blue {fmt_delta(blue_delta)}, Red {fmt_delta(red_delta)}")
# Check if action was optimal (only for live player)
was_optimal = False
expected_action_str = ""
if turn_char == 'A':
was_optimal = game.is_action_optimal(action_str, scenario, true_contents)
expected_action_obj = game.execute_npc_action(turn_char, scenario, true_contents)
if expected_action_obj.action_type == ActionType.PASS:
expected_action_str = "Pass"
elif expected_action_obj.action_type == ActionType.ASK:
expected_action_str = f"Ask({expected_action_obj.target_char}, {expected_action_obj.container})"
elif expected_action_obj.action_type == ActionType.TELL:
expected_action_str = f"Tell({expected_action_obj.target_char}, {expected_action_obj.container}, {expected_action_obj.contents})"
else:
was_optimal = True
expected_action_str = action_str
turn_record = TurnRecord(
round_num=scenario.round_num,
character=turn_char,
scenario_desc=scenario_desc,
question=question_desc,
action=action_str,
action_cost=abs(score_delta),
answer_given=answer_given,
answer_correct=is_correct,
answer_score=answer_score,
optimal_action=expected_action_str,
was_optimal=was_optimal,
blue_score_after=game.scores[Team.BLUE],
red_score_after=game.scores[Team.RED],
epistemic_type=scenario.epistemic_type.value if scenario.epistemic_type else None,
ask_constraint=scenario.ask_constraint.value if scenario.ask_constraint else None
)
game.turn_records.append(turn_record)
# Wait for user to press space (if human player)
if human_player:
input("\n[Press Enter to continue]")
game.check_game_over()
game.advance_turn()
turn_count += 1
# Game over
print("\n" + "=" * 70)
print("GAME OVER")
print(f"Final Score: Blue {game.scores[Team.BLUE]} - Red {game.scores[Team.RED]}")
game.winner = "Blue" if game.scores[Team.BLUE] > game.scores[Team.RED] else "Red" if game.scores[Team.RED] > game.scores[Team.BLUE] else None
if game.winner:
print(f"Winner: {game.winner.value} team")
elif game.winner is None:
print("It's a tie!")
print("=" * 70)
"""
# Show turn records
print("\n" + "=" * 70)
print("TURN RECORD")
print("=" * 70)
for record in game.turn_records:
print(f"\nRound {record.round_num} - {record.character}'s turn")
print(f"Ontological Type: {record.epistemic_type}")
print(f"Ask Constraint: {record.ask_constraint}")
print(f"Action: {record.action}")
if record.character == 'A':
print(f"Expected: {record.optimal_action}")
print(f"Was Expected: {'YES' if record.was_optimal else 'NO'}")
print(f"Answer Given: {record.answer_given}")
print(f"Answer Correct: {'YES' if record.answer_correct else 'NO'}")
print(f"Score After: Blue {record.blue_score_after} - Red {record.red_score_after}")
"""
# Save results
save_game_results(game.turn_records, 'game_results.json')
print("\nGame results saved to game_results.json")
return game
if __name__ == "__main__":
play_game_cli(scenario_file = 'scenarios_generated2.json', human_player=True)