Last active
March 8, 2020 15:34
-
-
Save seansawyer/f19a6d25ab255bc3b7a34569e9b995c2 to your computer and use it in GitHub Desktop.
Roguelike Development in Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def main(): | |
state_handlers = { | |
State.MAP: MapStateHandler, | |
State.ENDGAME: EndgameStateHandler, | |
} | |
run_fsm(state_handlers, State.MAP, build_game()) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from dataclasses import dataclass | |
from enum import Enum | |
class State(Enum): | |
MAP = 'map' | |
ENDGAME = 'endgame' | |
@dataclass | |
class Game: | |
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@dataclass | |
class Game: | |
root_console: tcod.console.Console | |
draw_console: tcod.console.Console |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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' | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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]: | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def draw_map(game: Game) -> None: | |
# ... | |
if game.fov_map.fov[game.exit_y][game.exit_x]: | |
# Draw the exit. | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
# ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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