1
0

Basic save/load functionality

This commit is contained in:
Timothy Warren 2022-01-18 14:37:48 -05:00
parent e82d2e5b49
commit 5d0915b82b
6 changed files with 175 additions and 41 deletions

3
.gitignore vendored
View File

@ -264,3 +264,6 @@ dmypy.json
cython_debug/
# End of https://www.toptal.com/developers/gitignore/api/python,jetbrains+all,macos
# Don't save game saves to git!
savegame.sav

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import lzma
import pickle
from typing import TYPE_CHECKING
from tcod.console import Console
@ -54,3 +56,9 @@ class Engine:
)
render_names_at_mouse_location(console, x=21, y=44, engine=self)
def save_as(self, filename: str) -> None:
"""Save this Engine instance as a compressed file."""
save_data = lzma.compress(pickle.dumps(self))
with open(filename, "wb") as f:
f.write(save_data)

View File

@ -89,6 +89,33 @@ class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
raise SystemExit()
class PopupMessage(BaseEventHandler):
"""Display a popup text window."""
def __init__(self, parent_handler: BaseEventHandler, text: str):
self.parent = parent_handler
self.text = text
def on_render(self, console: tcod.Console) -> None:
"""Render the parent and dim the result, then print the message on top."""
self.parent.on_render(console)
console.tiles_rgb["fg"] //= 8
console.tiles_rgb["bg"] //= 8
console.print(
console.width // 2,
console.height // 2,
self.text,
fg=color.white,
bg=color.black,
alignment=tcod.CENTER,
)
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseEventHandler]:
"""Any key returns to the parent handler."""
return self.parent
class EventHandler(BaseEventHandler):
def __init__(self, engine: Engine):
self.engine = engine

59
main.py
View File

@ -1,31 +1,25 @@
#!/usr/bin/env python3
import copy
import traceback
import tcod
import color
from engine import Engine
import entity_factories
import exceptions
import input_handlers
from procgen import generate_dungeon
import setup_game
def save_game(handler: input_handlers.BaseEventHandler, filename: str) -> None:
"""If the current event handler has an active Engine then save it."""
if isinstance(handler, input_handlers.EventHandler):
handler.engine.save_as(filename)
print("Game saved.")
def main() -> None:
screen_width = 80
screen_height = 50
map_width = 80
map_height = 43
room_max_size = 10
room_min_size = 6
max_rooms = 30
max_monsters_per_room = 2
max_items_per_room = 2
tileset = tcod.tileset.load_tilesheet(
"dejavu10x10_gs_tc.png",
32,
@ -33,28 +27,7 @@ def main() -> None:
tcod.tileset.CHARMAP_TCOD
)
player = copy.deepcopy(entity_factories.player)
engine = Engine(player)
engine.game_map = generate_dungeon(
max_rooms,
room_min_size,
room_max_size,
map_width,
map_height,
max_monsters_per_room,
max_items_per_room,
engine,
)
engine.update_fov()
engine.message_log.add_message(
"Hello and welcome, adventurer, to yet another dungeon!",
color.welcome_text
)
handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
handler: input_handlers.BaseEventHandler = setup_game.MainMenu()
with tcod.context.new_terminal(
screen_width,
@ -67,25 +40,29 @@ def main() -> None:
try:
while True:
root_console.clear()
engine.event_handler.on_render(console=root_console)
handler.on_render(console=root_console)
context.present(root_console)
try:
for event in tcod.event.wait():
context.convert_event(event)
engine.event_handler.handle_events(event)
handler = handler.handle_events(event)
except Exception: # Handle exceptions in game.
traceback.print_exc() # Print error to stderr.
# Then print the error to the message log.
engine.message_log.add_message(traceback.format_exc(), color.error)
if isinstance(handler, input_handlers.EventHandler):
handler.engine.message_log.add_message(
traceback.format_exc(),
color.error,
)
except exceptions.QuitWithoutSaving:
raise
except SystemExit: # Save and quit.
# TODO: Add the save function here
save_game(handler, "savegame.sav")
raise
except BaseException: # Save on any other unexpected exception.
# TODO: Add the save function here
save_game(handler, "savegame.sav")
raise

BIN
menu_background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

119
setup_game.py Normal file
View File

@ -0,0 +1,119 @@
"""Handle the loading and initialization of game sessions."""
from __future__ import annotations
import copy
import lzma
import pickle
import traceback
from typing import Optional
import tcod
import color
from engine import Engine
import entity_factories
import input_handlers
from procgen import generate_dungeon
# Load the background image and remove the alpha channel.
background_image = tcod.image.load("menu_background.png")[:, :, :3]
def new_game() -> Engine:
"""Return a brand new game session as an Engine instance."""
map_width = 80
map_height = 43
room_max_size = 10
room_min_size = 6
max_rooms = 30
max_monsters_per_room = 2
max_items_per_room = 2
player = copy.deepcopy(entity_factories.player)
engine = Engine(player)
engine.game_map = generate_dungeon(
max_rooms=max_rooms,
room_min_size=room_min_size,
room_max_size=room_max_size,
map_width=map_width,
map_height=map_height,
max_monsters_per_room=max_monsters_per_room,
max_items_per_room=max_items_per_room,
engine=engine,
)
engine.update_fov()
engine.message_log.add_message(
"Hello and welcome, adventurer, to yet another dungeon!",
color.welcome_text
)
return engine
def load_game(filename: str) -> Engine:
"""Load an engine instance from a file."""
with open(filename, "rb") as f:
engine = pickle.loads(lzma.decompress(f.read()))
assert isinstance(engine, Engine)
return engine
class MainMenu(input_handlers.BaseEventHandler):
"""Handle the main menu rendering and input."""
def on_render(self, console: tcod.Console) -> None:
"""Render the main menu on a background image."""
console.draw_semigraphics(background_image, 0, 0)
console.print(
console.width // 2,
console.height // 2 - 4,
"TOMBS OF THE ANCIENT KINGS",
fg=color.menu_title,
alignment=tcod.CENTER,
)
console.print(
console.width // 2,
console.height - 2,
"By Timothy J. Warren",
fg=color.menu_title,
alignment=tcod.CENTER,
)
menu_width = 24
for i, text in enumerate([
"[N] Play a new game",
"[C] Continue last game",
"[Q] Quit"
]):
console.print(
console.width // 2,
console.height // 2 - 2 + i,
text.ljust(menu_width),
fg=color.menu_text,
bg=color.black,
alignment=tcod.CENTER,
bg_blend=tcod.BKGND_ALPHA(64),
)
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[input_handlers.BaseEventHandler]:
if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
raise SystemExit()
elif event.sym == tcod.event.K_c:
try:
return input_handlers.MainGameEventHandler(load_game("savegame.sav"))
except FileNotFoundError:
return input_handlers.PopupMessage(self, "No saved game to load.")
except Exception as exc:
traceback.print_exc() # Print to stderr.
return input_handlers.PopupMessage(self, f"Failed to load save:\n{exc}")
elif event.sym == tcod.event.K_n:
return input_handlers.MainGameEventHandler(new_game())
return None