Basic save/load functionality
This commit is contained in:
parent
e82d2e5b49
commit
5d0915b82b
3
.gitignore
vendored
3
.gitignore
vendored
@ -264,3 +264,6 @@ dmypy.json
|
|||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python,jetbrains+all,macos
|
# End of https://www.toptal.com/developers/gitignore/api/python,jetbrains+all,macos
|
||||||
|
|
||||||
|
# Don't save game saves to git!
|
||||||
|
savegame.sav
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import lzma
|
||||||
|
import pickle
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from tcod.console import Console
|
from tcod.console import Console
|
||||||
@ -54,3 +56,9 @@ class Engine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
render_names_at_mouse_location(console, x=21, y=44, engine=self)
|
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)
|
||||||
|
@ -89,6 +89,33 @@ class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
|
|||||||
raise SystemExit()
|
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):
|
class EventHandler(BaseEventHandler):
|
||||||
def __init__(self, engine: Engine):
|
def __init__(self, engine: Engine):
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
|
59
main.py
59
main.py
@ -1,31 +1,25 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import copy
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import tcod
|
import tcod
|
||||||
|
|
||||||
import color
|
import color
|
||||||
from engine import Engine
|
|
||||||
import entity_factories
|
|
||||||
import exceptions
|
import exceptions
|
||||||
import input_handlers
|
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:
|
def main() -> None:
|
||||||
screen_width = 80
|
screen_width = 80
|
||||||
screen_height = 50
|
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(
|
tileset = tcod.tileset.load_tilesheet(
|
||||||
"dejavu10x10_gs_tc.png",
|
"dejavu10x10_gs_tc.png",
|
||||||
32,
|
32,
|
||||||
@ -33,28 +27,7 @@ def main() -> None:
|
|||||||
tcod.tileset.CHARMAP_TCOD
|
tcod.tileset.CHARMAP_TCOD
|
||||||
)
|
)
|
||||||
|
|
||||||
player = copy.deepcopy(entity_factories.player)
|
handler: input_handlers.BaseEventHandler = setup_game.MainMenu()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
with tcod.context.new_terminal(
|
with tcod.context.new_terminal(
|
||||||
screen_width,
|
screen_width,
|
||||||
@ -67,25 +40,29 @@ def main() -> None:
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
root_console.clear()
|
root_console.clear()
|
||||||
engine.event_handler.on_render(console=root_console)
|
handler.on_render(console=root_console)
|
||||||
context.present(root_console)
|
context.present(root_console)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in tcod.event.wait():
|
for event in tcod.event.wait():
|
||||||
context.convert_event(event)
|
context.convert_event(event)
|
||||||
engine.event_handler.handle_events(event)
|
handler = handler.handle_events(event)
|
||||||
except Exception: # Handle exceptions in game.
|
except Exception: # Handle exceptions in game.
|
||||||
traceback.print_exc() # Print error to stderr.
|
traceback.print_exc() # Print error to stderr.
|
||||||
# Then print the error to the message log.
|
# 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:
|
except exceptions.QuitWithoutSaving:
|
||||||
raise
|
raise
|
||||||
except SystemExit: # Save and quit.
|
except SystemExit: # Save and quit.
|
||||||
# TODO: Add the save function here
|
save_game(handler, "savegame.sav")
|
||||||
raise
|
raise
|
||||||
except BaseException: # Save on any other unexpected exception.
|
except BaseException: # Save on any other unexpected exception.
|
||||||
# TODO: Add the save function here
|
save_game(handler, "savegame.sav")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
BIN
menu_background.png
Normal file
BIN
menu_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
119
setup_game.py
Normal file
119
setup_game.py
Normal 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
|
Loading…
Reference in New Issue
Block a user