Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 326 additions & 64 deletions agent.py

Large diffs are not rendered by default.

157 changes: 65 additions & 92 deletions case_closed_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,24 @@
from enum import Enum
from typing import Optional

EMPTY = 0
AGENT = 1
# top-level constants
EMPTY = 0
AGENT1 = 1
AGENT2 = 2

"""
GameBoard class manages the game board.

Handles the 2D grid, state of each cell, and provides torus (wraparound)
functionality for all coordinate-based operations.
"""
class GameBoard:
def __init__(self, height: int = 18, width: int = 20):
self.height = height
self.width = width
self.grid = [[EMPTY for _ in range(width)] for _ in range(height)]

def _torus_check(self, position: tuple[int, int]) -> tuple[int, int]:
x, y = position
normalized_x = x % self.width
normalized_y = y % self.height
return (normalized_x, normalized_y)

def get_cell_state(self, position: tuple[int, int]) -> int:
x, y = self._torus_check(position)
x, y = position
return self.grid[y][x]

def set_cell_state(self, position: tuple[int, int], state: int):
x, y = self._torus_check(position)
x, y = position
self.grid[y][x] = state

def get_random_empty_cell(self) -> tuple[int, int] | None:
Expand All @@ -38,14 +29,13 @@ def get_random_empty_cell(self) -> tuple[int, int] | None:
for x in range(self.width):
if self.grid[y][x] == EMPTY:
empty_cells.append((x, y))

if not empty_cells:
return None

return random.choice(empty_cells)

# <-- OUTDENTED: this was nested before; now it's a proper method
def __str__(self) -> str:
chars = {EMPTY: '.', AGENT: 'A'}
chars = {EMPTY: '.', AGENT1: 'A', AGENT2: 'B'}
board_str = ""
for y in range(self.height):
for x in range(self.width):
Expand All @@ -54,109 +44,94 @@ def __str__(self) -> str:
return board_str


UP = (0, -1)
DOWN = (0, 1)
RIGHT = (1, 0)
LEFT = (-1, 0)

class Direction(Enum):
UP = (0, -1)
DOWN = (0, 1)
RIGHT = (1, 0)
LEFT = (-1, 0)
UP = (0, -1)
DOWN = (0, 1)
RIGHT = (1, 0)
LEFT = (-1, 0)


class GameResult(Enum):
AGENT1_WIN = 1
AGENT2_WIN = 2
DRAW = 3
DRAW = 3


class Agent:
'''This class represents an agent in the game. It manages the agent's trail using a deque.'''
def __init__(self, agent_id: str, start_pos: tuple[int, int], start_dir: Direction, board: GameBoard):
"""Represents an agent; manages its trail and movement."""
def __init__(self, agent_id: str, start_pos: tuple[int, int],
start_dir: Direction, board: GameBoard, mark: int):
self.agent_id = agent_id
self.mark = mark # <— accept and store mark
second = (start_pos[0] + start_dir.value[0], start_pos[1] + start_dir.value[1])
self.trail = deque([start_pos, second]) # Trail of positions
self.trail = deque([start_pos, second])
self.direction = start_dir
self.board = board
self.alive = True
self.length = 2 # Initial length of the trail
self.boosts_remaining = 3 # Each agent gets 3 speed boosts
self.length = 2
self.boosts_remaining = 3

self.board.set_cell_state(start_pos, AGENT)
self.board.set_cell_state(second, AGENT)
# paint initial cells with THIS agent's mark
self.board.set_cell_state(start_pos, self.mark)
self.board.set_cell_state(second, self.mark)

def is_head(self, position: tuple[int, int]) -> bool:
return position == self.trail[-1]

def move(self, direction: Direction, other_agent: Optional['Agent'] = None, use_boost: bool = False) -> bool:
"""
Moves the agent in the given direction and handles collisions.
Agents leave a permanent trail behind them.

Args:
direction: Direction enum indicating where to move
other_agent: The other agent on the board (for collision detection)
use_boost: If True and boosts available, moves twice instead of once

Returns:
True if the agent survives the move, False if it dies
"""
def move(self, direction: Direction, other_agent: Optional['Agent'] = None,
use_boost: bool = False) -> bool:
if not self.alive:
return False

if use_boost and self.boosts_remaining <= 0:
print(f'Agent {self.agent_id} tried to boost but has no boosts remaining')
# no boosts left; ignore
use_boost = False

num_moves = 2 if use_boost else 1

if use_boost:
self.boosts_remaining -= 1
print(f'Agent {self.agent_id} used boost! ({self.boosts_remaining} remaining)')

for move_num in range(num_moves):
for _ in range(num_moves):
# prevent immediate reversal
cur_dx, cur_dy = self.direction.value
req_dx, req_dy = direction.value
if (req_dx, req_dy) == (-cur_dx, -cur_dy):
print('invalid move')
continue # Skip this move if invalid direction
print('invalid move (reverse); skipping this step')
continue

head = self.trail[-1]
dx, dy = direction.value
new_head = (head[0] + dx, head[1] + dy)

new_head = self.board._torus_check(new_head)
# out of bounds => death
if not (0 <= new_head[0] < self.board.width and 0 <= new_head[1] < self.board.height):
self.alive = False
return False

cell_state = self.board.get_cell_state(new_head)

self.direction = direction
# Handle collision with agent trail
if cell_state == AGENT:
# Check if it's our own trail (any part of our trail)

# collision with ANY trail (A or B)
if cell_state != EMPTY:
# own trail?
if new_head in self.trail:
# Hit our own trail
self.alive = False
return False

# Check collision with the other agent
# opponent trail?
if other_agent and other_agent.alive and new_head in other_agent.trail:
# Check for head-on collision
if other_agent.is_head(new_head):
# Head-on collision: always a draw (both agents die)
if other_agent.is_head(new_head): # head-on -> both die (draw)
self.alive = False
other_agent.alive = False
return False
else:
# Hit other agent's trail (not head-on)
else: # hit opponent body
self.alive = False
return False

# Normal move (empty cell) - leave trail behind
# Add new head, trail keeps growing

# safe move – lay down THIS agent's mark
self.trail.append(new_head)
self.length += 1
self.board.set_cell_state(new_head, AGENT)
self.board.set_cell_state(new_head, self.mark)

return True

Expand All @@ -167,42 +142,40 @@ def get_trail_positions(self) -> list[tuple[int, int]]:
class Game:
def __init__(self):
self.board = GameBoard()
self.agent1 = Agent(agent_id=1, start_pos=(1, 2), start_dir=Direction.RIGHT, board=self.board)
self.agent2 = Agent(agent_id=2, start_pos=(17, 15), start_dir=Direction.LEFT, board=self.board)
self.agent1 = Agent(agent_id="1", start_pos=(1, 2),
start_dir=Direction.RIGHT, board=self.board, mark=AGENT1)
self.agent2 = Agent(agent_id="2", start_pos=(17, 15),
start_dir=Direction.LEFT, board=self.board, mark=AGENT2)
self.turns = 0

def reset(self):
"""Resets the game to the initial state."""
self.board = GameBoard()
self.agent1 = Agent(agent_id=1, start_pos=(1, 2), start_dir=Direction.RIGHT, board=self.board)
self.agent2 = Agent(agent_id=2, start_pos=(17, 15), start_dir=Direction.LEFT, board=self.board)
self.agent1 = Agent(agent_id="1", start_pos=(1, 2),
start_dir=Direction.RIGHT, board=self.board, mark=AGENT1)
self.agent2 = Agent(agent_id="2", start_pos=(17, 15),
start_dir=Direction.LEFT, board=self.board, mark=AGENT2)
self.turns = 0
def step(self, dir1: Direction, dir2: Direction, boost1: bool = False, boost2: bool = False):
"""Advances the game by one step, moving both agents."""

def step(self, dir1: Direction, dir2: Direction,
boost1: bool = False, boost2: bool = False):
if self.turns >= 200:
print("Max turns reached. Checking trail lengths...")
# length tiebreak
if self.agent1.length > self.agent2.length:
print(f"Agent 1 wins with trail length {self.agent1.length} vs {self.agent2.length}")
return GameResult.AGENT1_WIN
elif self.agent2.length > self.agent1.length:
print(f"Agent 2 wins with trail length {self.agent2.length} vs {self.agent1.length}")
return GameResult.AGENT2_WIN
else:
print(f"Draw - both agents have trail length {self.agent1.length}")
return GameResult.DRAW

agent_one_alive = self.agent1.move(dir1, other_agent=self.agent2, use_boost=boost1)
agent_two_alive = self.agent2.move(dir2, other_agent=self.agent1, use_boost=boost2)
a1_alive = self.agent1.move(dir1, other_agent=self.agent2, use_boost=boost1)
a2_alive = self.agent2.move(dir2, other_agent=self.agent1, use_boost=boost2)

if not agent_one_alive and not agent_two_alive:
print("Both agents have crashed.")
if not a1_alive and not a2_alive:
return GameResult.DRAW
elif not agent_one_alive:
print("Agent 1 has crashed.")
elif not a1_alive:
return GameResult.AGENT2_WIN
elif not agent_two_alive:
print("Agent 2 has crashed.")
elif not a2_alive:
return GameResult.AGENT1_WIN

self.turns += 1
return None # game continues
Binary file added model.pth
Binary file not shown.
File renamed without changes.
Loading