Skip to content

Instantly share code, notes, and snippets.

@seansawyer
Last active March 8, 2020 15:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seansawyer/f19a6d25ab255bc3b7a34569e9b995c2 to your computer and use it in GitHub Desktop.
Save seansawyer/f19a6d25ab255bc3b7a34569e9b995c2 to your computer and use it in GitHub Desktop.
Roguelike Development in Python
import tcod
import tcod.event
tcod.console_set_custom_font(
'arial10x10.png',
tcod.FONT_LAYOUT_TCOD | tcod.FONT_TYPE_GREYSCALE,
)
with tcod.console_init_root(
CONSOLE_WIDTH,
CONSOLE_HEIGHT,
order='F',
renderer=tcod.RENDERER_SDL2,
title='FSM Game',
vsync=True
) as root_console:
draw_console = tcod.console.Console(CONSOLE_WIDTH, CONSOLE_HEIGHT, order='F')
loop(root_console, draw_console)
import random
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Optional, Tuple
class State(Enum):
MAP = 'map'
ENDGAME = 'endgame'
@dataclass
class Game:
pass
class StateHandler:
def __init__(self, next_state: State, game: Game):
self.next_state = next_state
self.game = game
def handle(self) -> Tuple[Optional[State], Game]:
"""
Override this to handle the state. Returning `None` will cause the
state machine to terminate. Otherwise, return the next state and the
game data to use in that state.
"""
print(f'{self.__class__} -> {self.next_state}, {self.game}')
return self.next_state, self.game
class MapStateHandler(StateHandler):
pass
class EndgameStateHandler(StateHandler):
pass
def run_fsm(
state_handlers: Dict[State, StateHandler],
state: State,
game: Game
) -> None:
while state is not None:
handler_class = state_handlers[state]
handler = handler_class(state, game)
state, game = handler.handle()
STATE_HANDLERS = {
State.MAP: MapStateHandler,
State.ENDGAME: EndgameStateHandler,
}
if __name__ == '__main__':
run_fsm(STATE_HANDLERS, State.MAP, build_game())
class StateHandler:
def __init__(self, next_state: State, game: Game):
self.next_state = next_state
self.game = game
def handle(self) -> Tuple[Optional[State], Game]:
"""
Override this to handle the state. Returning `None` will cause the
state machine to terminate. Otherwise, return the next state and the
game data to use in that state.
"""
print(f'{self.__class__} -> {self.next_state}, {self.game}')
return self.next_state, self.game
class MapStateHandler(StateHandler):
pass
class EndgameStateHandler(StateHandler):
pass
def main():
state_handlers = {
State.MAP: MapStateHandler,
State.ENDGAME: EndgameStateHandler,
}
run_fsm(state_handlers, State.MAP, build_game())
if __name__ == '__main__':
main()
def run_fsm(
state_handlers: Dict[State, StateHandler],
state: State,
game: Game
) -> None:
while state is not None:
handler_class = state_handlers[state]
handler = handler_class(state, game)
state, game = handler.handle()
from dataclasses import dataclass
from enum import Enum
class State(Enum):
MAP = 'map'
ENDGAME = 'endgame'
@dataclass
class Game:
pass
class StateHandler:
# ...
def draw(self) -> None:
"""Override this to draw the screen for this state."""
pass
def on_enter_state(self) -> None:
self.draw()
to_console = self.game.root_console
from_console = self.game.draw_console
to_console.blit(
from_console,
width=from_console.width,
height=from_console.height
)
tcod.console_flush()
def on_reenter_state(self) -> None:
# same as on_enter_state()
class MapStateHandler(StateHandler):
# ...
def draw(self):
draw_map(self.game)
def draw_map(game: Game) -> None:
game.draw_console.clear()
# Draw the player in the middle of the screen.
game.draw_console.draw_rect(
game.map_width // 2,
game.map_height // 2,
1,
1,
ord('@'),
fg=tcod.yellow
)
class StateHandler(tcod.event.EventDispatch):
# ...
def ev_quit(self, event):
self.next_state = None
def ev_keydown(self, event):
pass
def handle(self) -> Tuple[Optional[State], Game]:
"""
Dispatch pending input events to handler methods, and then return the
next state and a game instance to use in that state.
"""
for event in tcod.event.wait():
self.dispatch(event)
return self.next_state, self.game
def run_fsm(
state_handlers: Dict[State, StateHandler],
state: State,
game: Game
) -> None:
last_state = None
while state is not None:
handler_class = state_handlers[state]
handler = handler_class(state, game)
if state == last_state:
handler.on_reenter_state()
else:
handler.on_enter_state()
last_state = state
state, game = handler.handle()
@dataclass
class Game:
root_console: tcod.console.Console
draw_console: tcod.console.Console
class MapStateHandler(StateHandler):
# ...
def ev_keydown(self, event):
if event.scancode == tcod.event.SCANCODE_F:
fullscreen = not tcod.console_is_fullscreen()
tcod.console_set_fullscreen(fullscreen)
elif event.scancode == tcod.event.SCANCODE_Q:
self.next_state = None # quit
elif event.scancode == tcod.event.SCANCODE_W:
self.next_state = State.ENDGAME # win
@dataclass
class Game:
# ...
player_x: int
player_y: int
class MapStateHandler(StateHandler):
def ev_keydown(self, event):
# ...
elif event.scancode == tcod.event.SCANCODE_H:
self.handle_move(-1, 0) # left
elif event.scancode == tcod.event.SCANCODE_J:
self.handle_move(0, 1) # down
elif event.scancode == tcod.event.SCANCODE_K:
self.handle_move(0, -1) # up
elif event.scancode == tcod.event.SCANCODE_L:
self.handle_move(1, 0) # right
class MapStateHandler(StateHandler):
def handle_move(self, dx, dy):
# Insert tedious logic to make sure the player stays on-screen here.
# ...
# Move the player and record their new position.
self.game.player_x = self.game.player_x + dx
self.game.player_y = self.game.player_y + dy
class MapStateHandler(StateHandler):
# ...
def ev_keydown(self, event):
# ...
elif event.scancode == tcod.event.SCANCODE_H:
self.handle_move(-1, 0) # left
elif event.scancode == tcod.event.SCANCODE_J:
self.handle_move(0, 1) # down
elif event.scancode == tcod.event.SCANCODE_K:
self.handle_move(0, -1) # up
elif event.scancode == tcod.event.SCANCODE_L:
self.handle_move(1, 0) # right
def handle_move(self, dx, dy):
# Insert tedious logic to make sure the player stays on-screen here.
# ...
# Move the player and record their new position.
self.game.player_x = self.game.player_x + dx
self.game.player_y = self.game.player_y + dy
def build_map(width: int, height: int) -> List[List[str]]:
"""Generate a map by carving out a random walk."""
# Start with all walls.
map_tiles = [['#'] * width for y in range(height)]
# Choose a random starting point.
x = random.randint(1, width - 2)
y = random.randint(1, height - 2)
# Walk in a random direction.
possible_moves = [(0, -1), (0, 1), (-1, 0), (1, 0)]
map_tiles[y][x] = '.'
for i in range(10000):
dx, dy= random.choice(possible_moves)
if 0 < x + dx < width - 1 and 0 < y + dy < height - 1:
x = x + dx
y = y + dy
map_tiles[y][x] = '.'
return map_tiles
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
map_tiles = build_map(CONSOLE_WIDTH, CONSOLE_HEIGHT)
occupied_coords = set()
exit_x, exit_y = place_randomly(map_tiles, occupied_coords)
player_x, player_y = place_randomly(map_tiles, occupied_coords)
return Game(
# ...
map_tiles=map_tiles,
occupied_coords=occupied_coords,
exit_x=exit_x,
exit_y=exit_y
)
def draw_map(game: Game) -> None:
# ...
for y, row in enumerate(game.map_tiles):
for x, tile in enumerate(row):
game.draw_console.draw_rect(x, y, 1, 1, ord(tile), fg=tcod.white)
# ...
def draw_map(game: Game) -> None:
# ...
for y, row in enumerate(game.map_tiles):
for x, tile in enumerate(row):
game.draw_console.draw_rect(x, y, 1, 1, ord(tile), fg=tcod.white)
# ...
@dataclass
class Mob:
hp: int
@dataclass
class Game:
# ...
mobs: Dict[Tuple[int, int], Mob]
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
# ...
mobs = {}
for i in range(25):
mob_coords = place_randomly(map_tiles, occupied_coords)
mobs[mob_coords] = Mob(5)
return Game(
# ...
mobs=mobs
)
class MapStateHandler(StateHandler):
def check_move(
self,
from_x: int,
from_y: int,
dx: int,
dy: int
) -> Tuple[Tuple[int, int], Optional[str]]:
# ...
if not is_wall(x, y, self.game.map_tiles) and coords not in self.game.occupied_coords:
return coords, 'move'
# ...
@dataclass
class Game:
# ...
player_hp: int
won: Optional[bool] = None # True if won, False if lost, None if in progress
class MapStateHandler(StateHandler):
def check_move(
self,
from_x: int,
from_y: int,
dx: int,
dy: int,
allow_attack: bool = False
) -> Tuple[Tuple[int, int], Optional[str], Optional[Mob]]:
# ...
if allow_attack:
attack_target = self.game.mobs.get(coords)
if attack_target:
return coords, 'attack', attack_target
# ...
class MapStateHandler(StateHandler):
def handle_attack(self, coords: Tuple[int, int], mob: Mob):
# We let the player strike first, then check if the mob is dead prior
# to counterattack. This gives the player a slight advantage.
mob.hp -= 1
if mob.hp <= 0:
self.game.mobs.pop(coords)
self.game.occupied_coords.remove(coords)
else:
self.game.player_hp -= 1
# If the player's hit points reach zero, they lose of course!
if self.game.player_hp <= 0:
self.game.won = False
self.next_state = State.ENDGAME
class MapStateHandler(StateHandler):
def maybe_move(self, dx, dy):
coords, action_type, action_target = self.check_move(
self.player_x,
self.player_y,
dx,
dy,
allow_attack=True
)
# ...
if action_type == 'attack':
self.handle_attack(coords, action_target)
# ...
@dataclass
class Game:
# ...
fov_map: tcod.map.Map
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
# ...
fov_map = tcod.map.Map(map_width, map_height)
# Transparent tiles are everything except the walls.
for y, row in enumerate(map_tiles):
for x, tile in enumerate(row):
if tile != '#':
fov_map.transparent[y][x] = True
fov_map.compute_fov(player_x, player_y, 10)
return Game(
# ...
fov_map=fov_map
)
class MapStateHandler(StateHandler):
def on_reenter_state(self) -> None:
self.game.fov_map.compute_fov(self.game.player_x, self.game.player_y, 10)
super().on_reenter_state()
# ...
def draw_map(game: Game) -> None:
# ...
if game.fov_map.fov[game.exit_y][game.exit_x]:
# ...
def draw_map(game: Game) -> None:
# ...
if game.fov_map.fov[game.exit_y][game.exit_x]:
# Draw the exit.
# ...
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
# ...
fov_map = tcod.map.Map(map_width, map_height)
# Transparent tiles are everything except the walls.
for y, row in enumerate(map_tiles):
for x, tile in enumerate(row):
if tile != '#':
fov_map.transparent[y][x] = True
fov_map.compute_fov(player_x, player_y, 10)
return Game(
# ...
fov_map=fov_map
)
class MapStateHandler(StateHandler):
# ...
def on_reenter_state(self) -> None:
self.game.fov_map.compute_fov(self.game.player_x, self.game.player_y, 10)
super().on_reenter_state()
@dataclass
class Game:
# ...
memory: np.ndarray
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
# ...
return Game(
# ...
memory=np.copy(fov_map.fov)
)
class MapStateHandler(StateHandler):
def on_reenter_state(self) -> None:
self.game.fov_map.compute_fov(self.game.player_x, self.game.player_y, 10)
self.game.memory |= self.game.fov_map.fov
super().on_reenter_state()
# ...
def draw_map(game: Game) -> None:
# ...
visible = game.fov_map.fov[game.exit_y][game.exit_x]
memorized = game.memory[game.exit_y][game.exit_x]
if visible or memorized:
# draw the exit
# ...
def draw_map(game: Game) -> None:
# ...
visible = game.fov_map.fov[game.exit_y][game.exit_x]
memorized = game.memory[game.exit_y][game.exit_x]
if visible or memorized:
# draw the exit
# ...
import numpy as np
def build_game(
root_console: tcod.console.Console,
draw_console: tcod.console.Console
) -> Game:
# ...
return Game(
# ...
memory=np.copy(fov_map.fov)
)
class MapStateHandler(StateHandler):
# ...
def on_reenter_state(self) -> None:
self.game.fov_map.compute_fov(self.game.player_x, self.game.player_y, 10)
self.game.memory |= self.game.fov_map.fov
super().on_reenter_state()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment