|
|
""" |
|
|
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 |
|
|
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) |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
if listener.team != Team.NEUTRAL and listener.team == speaker.team: |
|
|
return True |
|
|
|
|
|
|
|
|
if listener.char_type == CharacterType.NEUTRAL: |
|
|
return True |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
if scenario.epistemic_type in [EpistemicType.TEAMMATE_HAS_FALSE_BELIEF, |
|
|
EpistemicType.TEAMMATE_HAS_NO_BELIEF]: |
|
|
return Action(ActionType.TELL, target, container, truth) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
actor = self.characters[actor_name] |
|
|
actor_trusts_opponents = actor.char_type in [CharacterType.HONEST_TEAMMATE, |
|
|
CharacterType.HONEST_OPPONENT] |
|
|
if actor_trusts_opponents: |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if target.char_type in [CharacterType.DISHONEST_TEAMMATE, CharacterType.DISHONEST_OPPONENT]: |
|
|
believed = target.get_belief(container) |
|
|
options = ['apple', 'ball', 'banana', 'brick', 'stapler', 'orange'] |
|
|
|
|
|
wrong = next((x for x in options if x != (believed or 'nothing')), options[0]) |
|
|
answer = wrong |
|
|
else: |
|
|
answer = target.get_belief(container) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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 [ |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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'), |
|
|
] |
|
|
), |
|
|
|
|
|
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_given, is_correct, answer_score = game.resolve_answer_phase(scenario, true_contents) |
|
|
|
|
|
|
|
|
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]}.") |
|
|
|
|
|
|
|
|
blue_delta = 0.0 |
|
|
red_delta = 0.0 |
|
|
|
|
|
|
|
|
if turn_char in ['A', 'B']: |
|
|
blue_delta += score_delta |
|
|
else: |
|
|
red_delta += score_delta |
|
|
|
|
|
|
|
|
if is_correct: |
|
|
if scenario.who_answers in ['A', 'B']: |
|
|
blue_delta += answer_score |
|
|
else: |
|
|
red_delta += answer_score |
|
|
|
|
|
|
|
|
game.scores[Team.BLUE] += blue_delta |
|
|
game.scores[Team.RED] += red_delta |
|
|
|
|
|
|
|
|
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)}") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if human_player: |
|
|
input("\n[Press Enter to continue]") |
|
|
|
|
|
game.check_game_over() |
|
|
game.advance_turn() |
|
|
turn_count += 1 |
|
|
|
|
|
|
|
|
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_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) |