188 lines
5.5 KiB
Python
188 lines
5.5 KiB
Python
"""Frogger: guide a frog across busy traffic lanes to reach the far side.
|
||
|
||
The frog starts at the bottom row and must cross to the top. Traffic lanes
|
||
alternate direction; safe lanes (no cars) appear every few rows as rest zones.
|
||
The frog scores 100 each time it reaches the top and respawns at the bottom.
|
||
It earns +1 reward for each upward step and +100 for crossing, but the game
|
||
ends immediately if a car hits it.
|
||
"""
|
||
|
||
from retro.game import Game
|
||
|
||
BOARD_WIDTH = 20
|
||
BOARD_HEIGHT = 12
|
||
FRAMERATE = 8
|
||
GAME_DURATION = 60 * FRAMERATE # ~60 seconds
|
||
|
||
# One entry per traffic row (rows 1–10). None marks a safe lane with no cars.
|
||
# Each traffic entry is (direction, speed, num_cars, car_length).
|
||
# direction: 1 = right, -1 = left
|
||
# speed: steps between moves (higher = slower)
|
||
# num_cars: how many cars in the lane
|
||
# car_length: how many cells each car occupies
|
||
LANES = [
|
||
( 1, 2, 2, 2), # row 1 fast
|
||
(-1, 4, 3, 2), # row 2 slow, crowded
|
||
( 1, 1, 1, 3), # row 3 very fast
|
||
None, # row 4 safe zone
|
||
(-1, 3, 2, 2), # row 5 medium
|
||
( 1, 2, 2, 3), # row 6 fast, long cars
|
||
(-1, 5, 3, 2), # row 7 slow, crowded
|
||
None, # row 8 safe zone
|
||
( 1, 1, 2, 2), # row 9 very fast
|
||
(-1, 3, 2, 3), # row 10 medium, long cars
|
||
]
|
||
|
||
|
||
class Frog:
|
||
name = "Frog"
|
||
character = 'O'
|
||
color = "green_on_black"
|
||
position = (0, 0)
|
||
|
||
UP = (0, -1)
|
||
DOWN = (0, 1)
|
||
LEFT = (-1, 0)
|
||
RIGHT = (1, 0)
|
||
|
||
def __init__(self):
|
||
self._pending_direction = None
|
||
self._frames_remaining = GAME_DURATION
|
||
|
||
def handle_keystroke(self, keystroke, game):
|
||
if keystroke.name == "KEY_UP":
|
||
self._pending_direction = self.UP
|
||
elif keystroke.name == "KEY_DOWN":
|
||
self._pending_direction = self.DOWN
|
||
elif keystroke.name == "KEY_LEFT":
|
||
self._pending_direction = self.LEFT
|
||
elif keystroke.name == "KEY_RIGHT":
|
||
self._pending_direction = self.RIGHT
|
||
|
||
def play_turn(self, game):
|
||
self._frames_remaining -= 1
|
||
game.state['time_left'] = self._frames_remaining // FRAMERATE
|
||
if self._frames_remaining <= 0:
|
||
game.end()
|
||
return
|
||
|
||
if self._pending_direction is None:
|
||
return
|
||
dx, dy = self._pending_direction
|
||
self._pending_direction = None
|
||
|
||
bw, bh = game.board_size
|
||
x, y = self.position
|
||
nx, ny = x + dx, y + dy
|
||
|
||
if not (0 <= nx < bw) or ny >= bh:
|
||
game.state['reward'] -= 5
|
||
return
|
||
|
||
if ny < 0:
|
||
game.state['score'] += 100
|
||
game.state['reward'] += 100
|
||
self._respawn(game)
|
||
return
|
||
|
||
self.position = (nx, ny)
|
||
|
||
if ny < y:
|
||
game.state['reward'] += 1
|
||
|
||
for agent in game.agents:
|
||
if hasattr(agent, '_is_car') and agent.position == self.position:
|
||
game.state['reward'] -= 10
|
||
game.end()
|
||
return
|
||
|
||
def _respawn(self, game):
|
||
bw, bh = game.board_size
|
||
self.position = (bw // 2, bh - 1)
|
||
self._pending_direction = None
|
||
|
||
|
||
class CarSegment:
|
||
"""One cell of a multi-cell car. Moved by the lead Car each turn."""
|
||
_is_car = True
|
||
character = 'X'
|
||
color = "red_on_black"
|
||
|
||
def __init__(self, name, position):
|
||
self.name = name
|
||
self.position = position
|
||
|
||
|
||
class Car:
|
||
"""Lead cell of a multi-cell car. Moves itself and all trailing segments."""
|
||
_is_car = True
|
||
character = 'X'
|
||
color = "red_on_black"
|
||
|
||
def __init__(self, name, lane, speed, direction, start_x, board_width, segments):
|
||
self.name = name
|
||
self._lane = lane
|
||
self._speed = speed
|
||
self._direction = direction
|
||
self._board_width = board_width
|
||
self._step = 0
|
||
self.position = (start_x, lane)
|
||
self.segments = segments
|
||
|
||
def play_turn(self, game):
|
||
self._step += 1
|
||
if self._step < self._speed:
|
||
return
|
||
self._step = 0
|
||
bw = self._board_width
|
||
x, y = self.position
|
||
self.position = ((x + self._direction) % bw, y)
|
||
for seg in self.segments:
|
||
sx, sy = seg.position
|
||
seg.position = ((sx + self._direction) % bw, sy)
|
||
|
||
frog = game.get_agent_by_name("Frog")
|
||
car_cells = {self.position} | {s.position for s in self.segments}
|
||
if frog.position in car_cells:
|
||
game.state['reward'] -= 10
|
||
game.end()
|
||
|
||
|
||
def create_game():
|
||
bw, bh = BOARD_WIDTH, BOARD_HEIGHT
|
||
frog = Frog()
|
||
frog.position = (bw // 2, bh - 1)
|
||
agents = [frog]
|
||
|
||
for lane_idx, row in enumerate(range(1, bh - 1)):
|
||
spec = LANES[lane_idx] if lane_idx < len(LANES) else None
|
||
if spec is None:
|
||
continue
|
||
direction, speed, num_cars, car_length = spec
|
||
spacing = bw // num_cars
|
||
for i in range(num_cars):
|
||
start_x = (i * spacing + lane_idx * 3) % bw
|
||
segments = []
|
||
for j in range(1, car_length):
|
||
seg_x = (start_x - direction * j) % bw
|
||
seg = CarSegment(f"Car_{row}_{i}_seg{j}", (seg_x, row))
|
||
segments.append(seg)
|
||
agents.append(seg)
|
||
agents.append(Car(f"Car_{row}_{i}", row, speed, direction, start_x, bw, segments))
|
||
|
||
return Game(
|
||
agents,
|
||
{
|
||
'score': 0,
|
||
'reward': 0.0,
|
||
'time_left': GAME_DURATION // FRAMERATE,
|
||
},
|
||
board_size=(bw, bh),
|
||
framerate=FRAMERATE,
|
||
show_state=['score', 'time_left'],
|
||
)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
create_game().play()
|