"""Game engine for PyScoundrel."""
from typing import TYPE_CHECKING, Optional
from ..dungeon import Dungeon
from ..models import Card, CardType, Deck, Player, Room, Weapon
from .actions import ActionResult
from .state import GamePhase, GameState
if TYPE_CHECKING:
pass
[docs]
class GameEngine:
"""
The game engine handles all game logic and rules enforcement.
This class processes player actions and updates game state accordingly.
"""
def __init__(
self,
state: Optional[GameState] = None,
seed: Optional[int] = None,
dungeon: Optional[Dungeon] = None,
):
"""
Initialize the game engine.
Args:
state: Optional existing game state to continue from
seed: Optional random seed for deck shuffling
dungeon: Optional dungeon configuration (loads default if not provided)
"""
if state is None:
# Always load dungeon - use default if not specified
if dungeon is None:
dungeon = Dungeon() # Loads default_dungeon.yaml
# Create deck from dungeon
deck = Deck(dungeon, shuffle=True, seed=seed)
player = Player()
state = GameState(player=player, deck=deck)
self.state = state
[docs]
def start_game(self) -> ActionResult:
"""Start a new game."""
self.state.phase = GamePhase.DRAW_ROOM
return ActionResult(success=True, message="Game started! Draw your first room.")
[docs]
def draw_room(self) -> ActionResult:
"""
Draw 4 cards to form a room.
If there's a leftover card from previous room, use it as the first card.
"""
if self.state.phase not in (GamePhase.DRAW_ROOM, GamePhase.TURN_COMPLETE):
return ActionResult(success=False, message="Cannot draw room in current phase.")
# Create new room
room = Room()
# Add leftover card from previous room if exists
if self.state.current_room and self.state.current_room.is_complete:
leftover = self.state.current_room.get_remaining_card()
if leftover:
room.add_card(leftover)
# Draw remaining cards to make 4
cards_needed = 4 - len(room.cards)
drawn_cards = self.state.deck.draw_multiple(cards_needed)
for card in drawn_cards:
room.add_card(card)
if not room.is_full:
# Deck ran out - game over
self.state.check_game_over()
return ActionResult(
success=True,
message="Dungeon complete! You survived!",
metadata={"game_over": True, "victory": True},
)
self.state.current_room = room
self.state.start_new_turn()
self.state.phase = GamePhase.DECIDE_AVOID
return ActionResult(success=True, message=f"Room drawn: {room}", metadata={"room": room})
[docs]
def avoid_room(self) -> ActionResult:
"""
Avoid the current room by placing all 4 cards at bottom of deck.
Can only avoid if haven't avoided the previous room.
"""
if not self.state.can_avoid_room:
return ActionResult(success=False, message="Cannot avoid two rooms in a row!")
if not self.state.current_room or not self.state.current_room.is_full:
return ActionResult(success=False, message="No complete room to avoid!")
# Put all 4 cards at bottom of deck
self.state.deck.add_to_bottom(self.state.current_room.cards)
self.state.rooms_avoided_consecutively += 1
self.state.current_room = None
self.state.phase = GamePhase.DRAW_ROOM
return ActionResult(
success=True, message="Room avoided. Cards placed at bottom of dungeon."
)
[docs]
def face_card(self, card_index: int) -> ActionResult:
"""
Face a card from the current room.
Args:
card_index: Index of card to face (0-3)
Returns:
Result indicating what happened
"""
if self.state.phase not in (GamePhase.DECIDE_AVOID, GamePhase.FACE_CARDS):
return ActionResult(success=False, message="Cannot face card in current phase.")
room = self.state.current_room
if not room:
return ActionResult(success=False, message="No room available!")
# Update phase to facing cards
if self.state.phase == GamePhase.DECIDE_AVOID:
self.state.phase = GamePhase.FACE_CARDS
self.state.rooms_avoided_consecutively = 0
try:
card = room.face_card(card_index)
except (IndexError, ValueError) as e:
return ActionResult(success=False, message=str(e))
# Handle the card based on its type
if card.card_type == CardType.WEAPON:
return self._handle_weapon(card)
elif card.card_type == CardType.HEALTH_POTION:
return self._handle_potion(card)
elif card.card_type == CardType.MONSTER:
return self._handle_monster_encounter(card)
return ActionResult(success=False, message="Unknown card type!")
[docs]
def fight_monster_barehanded(self, monster: Card) -> ActionResult:
"""
Fight a monster without a weapon (take full damage).
Args:
monster: The monster to fight
Returns:
Result of the combat
"""
damage = monster.value
self.state.player.take_damage(damage)
self.state.discard([monster])
game_over = self.state.check_game_over()
self._check_turn_complete()
return ActionResult(
success=True,
message=f"Fought {monster} barehanded!",
damage_taken=damage,
metadata={"player_died": game_over and not self.state.victory},
)
[docs]
def fight_monster_with_weapon(self, monster: Card) -> ActionResult:
"""
Fight a monster with the equipped weapon.
Args:
monster: The monster to fight
Returns:
Result of the combat
"""
weapon = self.state.player.equipped_weapon
if not weapon:
return ActionResult(success=False, message="No weapon equipped!")
if not weapon.can_kill(monster):
return ActionResult(
success=False,
message=f"Weapon cannot kill {monster} (last kill: {weapon.last_kill_value})!",
)
damage = weapon.attack(monster)
self.state.player.take_damage(damage)
game_over = self.state.check_game_over()
self._check_turn_complete()
message = f"Used {weapon.card} against {monster}!"
if damage == 0:
message += " No damage taken!"
return ActionResult(
success=True,
message=message,
damage_taken=damage,
metadata={"player_died": game_over and not self.state.victory},
)
def _handle_weapon(self, weapon_card: Card) -> ActionResult:
"""Handle picking up a weapon."""
new_weapon = Weapon(card=weapon_card)
# Discard old weapon and any monsters on it
old_weapon = self.state.player.equip_weapon(new_weapon)
if old_weapon:
cards_to_discard = [old_weapon.card] + old_weapon.slain_monsters
self.state.discard(cards_to_discard)
message = f"Equipped {weapon_card}! Discarded old weapon."
else:
message = f"Equipped {weapon_card}!"
self._check_turn_complete()
return ActionResult(success=True, message=message, metadata={"weapon": new_weapon})
def _handle_potion(self, potion: Card) -> ActionResult:
"""Handle using a health potion."""
# Can only use one potion per turn
if self.state.player.potions_used_this_turn >= 1:
self.state.discard([potion])
self._check_turn_complete()
return ActionResult(
success=True, message=f"Discarded {potion} (already used a potion this turn)."
)
# Heal the player
heal_amount = self.state.player.heal(potion.value)
self.state.player.potions_used_this_turn += 1
self.state.last_card_was_potion = True
self.state.discard([potion])
self._check_turn_complete()
return ActionResult(success=True, message=f"Used {potion}!", health_gained=heal_amount)
def _handle_monster_encounter(self, monster: Card) -> ActionResult:
"""
Handle encountering a monster.
Returns a result indicating the monster was encountered.
Player must choose how to fight it.
"""
weapon = self.state.player.equipped_weapon
can_use_weapon = weapon and weapon.can_kill(monster)
metadata = {"monster": monster, "can_use_weapon": can_use_weapon, "weapon": weapon}
if can_use_weapon:
message = f"Encountered {monster}! (Weapon available)"
else:
if weapon:
message = f"Encountered {monster}! (Weapon cannot be used - must fight barehanded)"
else:
message = f"Encountered {monster}! (No weapon - must fight barehanded)"
return ActionResult(success=True, message=message, metadata=metadata)
def _check_turn_complete(self) -> None:
"""Check if the current turn is complete (3 cards faced)."""
if self.state.current_room and self.state.current_room.is_complete:
self.state.end_turn()
@property
def is_game_over(self) -> bool:
"""Check if the game is over."""
return self.state.game_over
@property
def score(self) -> int:
"""Get the current score."""
return self.state.score