Save game.
authorJeremy Thurgood <firxen@gmail.com>
Fri, 16 May 2014 17:00:46 +0000 (19:00 +0200)
committerJeremy Thurgood <firxen@gmail.com>
Fri, 16 May 2014 17:00:46 +0000 (19:00 +0200)
naja/actions.py
naja/gameboard.py
naja/gamestate.py
naja/options.py
naja/scenes/load_save.py [new file with mode: 0644]
naja/scenes/menu.py
naja/tests/test_gameboard.py
naja/widgets/save_slot.py [new file with mode: 0644]
naja/widgets/text.py

index ca472d3a75a7b50a79f84d406dca1267eaf17b9c..596a505c7bc9178347a0b32386ca415beef4885d 100644 (file)
@@ -32,7 +32,8 @@ class LocationAction(object):
                 self.data['chesspiece'])
 
         if 'rot_direction' in self.data:
-            substitutions['rot_direction_name'] = '{%s}' % (substitutions['rot_direction'],)
+            substitutions['rot_direction_name'] = '{%s}' % (
+                substitutions['rot_direction'],)
 
         if location is None:
             substitutions['location_bits'] = 'bits specified by this location'
index 2cce169544823e843c86e82f2577c3a634f99e5e..bd4eb825c22e6f1bf1a7a686ee192eacf9474230 100644 (file)
@@ -66,24 +66,24 @@ class GameBoard(object):
             for definition in locations_definition]
 
     def export_board_locations(self):
-        return dict(
+        return sorted(
             (position, location.export())
             for position, location in self.board_locations.iteritems())
 
     @classmethod
     def import_board_locations(cls, board_locations_definition):
         return dict(
-            (position, LocationCard.import_location(definition))
-            for position, definition in board_locations_definition.iteritems())
+            (tuple(position), LocationCard.import_location(definition))
+            for position, definition in board_locations_definition)
 
     @classmethod
     def generate_board(cls, locations_definition):
-        board_locations = {}
+        board_locations = []
         for x in range(5):
             for y in range(5):
                 board_location = LocationCard.new_location(
                     choice(locations_definition).copy())
-                board_locations[(x, y)] = board_location.export()
+                board_locations.append([(x, y), board_location.export()])
         return board_locations
 
     def lose_health(self):
@@ -135,14 +135,14 @@ class GameBoard(object):
         rotated_locations = {}
 
         if py > 0:
-            for i in range(max(0, px -1), min(5, px + 2)):
+            for i in range(max(0, px - 1), min(5, px + 2)):
                 locations_to_rotate.append((i, py - 1))
 
         if px < 4:
             locations_to_rotate.append((px + 1, py))
 
         if py < 4:
-            for i in reversed(range(max(0, px -1), min(5, px + 2))):
+            for i in reversed(range(max(0, px - 1), min(5, px + 2))):
                 locations_to_rotate.append((i, py + 1))
 
         if px > 0:
@@ -151,7 +151,8 @@ class GameBoard(object):
         if ROTATION[direction] == ROTATION.CLOCKWISE:
             new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
         elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
-            new_positions = [locations_to_rotate[-1]] + locations_to_rotate[:-1]
+            new_positions = (
+                [locations_to_rotate[-1]] + locations_to_rotate[:-1])
 
         for old, new in zip(locations_to_rotate, new_positions):
             rotated_locations[old] = self.board_locations[new]
@@ -223,7 +224,7 @@ class LocationCard(object):
 
     def export(self):
         return {
-            'bitwise_operand': self.bitwise_operand,
+            'bitwise_operand': sorted(self.bitwise_operand),
             'actions': [action.export() for action in self.actions],
         }
 
index a2ed6e0d70225dc0f50d1a6e70eeca52d5372766..d5c624c4882513ba7cf27201bf8ec29c263429f8 100644 (file)
@@ -18,12 +18,12 @@ class GameState(object):
     Naja game state.
     """
 
-    def __init__(self):
-        # This is a very simple deck to allow testing more drawing logic
-        # on tiles. These will need to be replaced with better stuff.
+    def __init__(self, data=None):
         locations_deck = load_location_deck('standard')
-        # locations_deck = load_location_deck('test')
-        self.gameboard = GameBoard.new_game(locations_deck['cards'])
+        if data is None:
+            self.gameboard = GameBoard.new_game(locations_deck['cards'])
+        else:
+            self.gameboard = GameBoard.import_game(data)
 
     @property
     def player(self):
index 816d91ca3c29f87329a2b8f6b9428d899e851280..6e5f00d723be67c9e9ef5c15bc0666fe99b05b2e 100644 (file)
@@ -1,5 +1,6 @@
 import optparse
 import os
+import sys
 
 from naja.attrdict import AttrDict
 from naja.constants import DEFAULTS
@@ -28,8 +29,24 @@ def parse_args(args):
                       dest='music', action='store_false', default=True,
                       help='Disable music (but not sound)')
 
+    parser.add_option("--save-location", default=_get_default_save_location(),
+                      dest="save_location", help="Saved game location")
+
     opts, _ = parser.parse_args(args)
 
     for k in DEFAULTS:
         if getattr(opts, k, None) is not None:
             options[k] = getattr(opts, k)
+    options['save_location'] = opts.save_location
+
+
+def _get_default_save_location():
+    """Return a default save game location."""
+    app = "naja"
+    if sys.platform.startswith("win"):
+        if "APPDATA" in os.environ:
+            return os.path.join(os.environ["APPDATA"], app)
+        return os.path.join(os.path.expanduser("~"), "." + app)
+    elif 'XDG_DATA_HOME' in os.environ:
+        return os.path.join(os.environ["XDG_DATA_HOME"], app)
+    return os.path.join(os.path.expanduser("~"), ".local", "share", app)
diff --git a/naja/scenes/load_save.py b/naja/scenes/load_save.py
new file mode 100644 (file)
index 0000000..485b032
--- /dev/null
@@ -0,0 +1,79 @@
+"""
+Load and save scenes.
+"""
+
+import json
+import os
+from datetime import datetime
+
+import pygame.locals as pgl
+
+from naja.constants import KEYS
+from naja.events import SceneChangeEvent, InvalidateTheWorld
+from naja.options import options
+from naja.scenes.scene import Scene
+from naja.widgets.save_slot import SaveSlotWidget
+from naja.widgets.selector import SelectorWidget
+
+
+def save_path(path):
+    return os.path.join(options.save_location, *path.split('/'))
+
+
+def ensure_save_path_exists():
+    location = save_path('')
+    if not os.path.isdir(location):
+        os.makedirs(location)
+
+
+class SaveGameScene(Scene):
+    def __init__(self, state):
+        super(SaveGameScene, self).__init__(state)
+        selector = SelectorWidget()
+        self.add(selector)
+        self.slots = {}
+
+        for slot_num in range(8):
+            slot = self.make_slot_widget(slot_num)
+            self.slots[slot_num] = slot
+            selector.add(slot)
+
+    def save_path(self, slot_num):
+        return save_path('slot_%s.json' % (slot_num,))
+
+    def get_game_data(self, slot_num):
+        try:
+            with open(self.save_path(slot_num), 'r') as save_file:
+                return json.load(save_file)
+        except IOError:
+            return None
+        except Exception as e:
+            print "Error reading savegame in slot %s: %s" % (slot_num, e)
+            return None
+
+    def make_slot_widget(self, slot_num):
+        game_data = self.get_game_data(slot_num)
+        y_offset = 74 * slot_num
+        slot = SaveSlotWidget((100, y_offset), slot_num, game_data)
+        slot.add_callback('click', lambda event: self.save_game(slot_num))
+        return slot
+
+    def save_game(self, slot_num):
+        save_data = {
+            'timestamp': datetime.now().ctime(),
+            'data': self.state.gameboard.export(),
+        }
+        try:
+            ensure_save_path_exists()
+            with open(self.save_path(slot_num), 'w') as save_file:
+                json.dump(save_data, save_file)
+        except Exception as e:
+            print "Error saving game in slot %s: %s" % (slot_num, e)
+        self.slots[slot_num].game_data = self.get_game_data(slot_num)
+        InvalidateTheWorld.post()
+
+    def handle_scene_event(self, ev):
+        if ev.type == pgl.KEYDOWN and ev.key in KEYS.QUIT:
+            from naja.scenes.menu import MenuScene
+            SceneChangeEvent.post(MenuScene)
+            return
index 3a7896908347cb037ecb26ef95717b269e32662e..e0383b5ca3159a3d3abd63b4a0e04f56cb2c4107 100644 (file)
@@ -5,13 +5,13 @@ Main menu scene.
 import pygame.locals as pgl
 
 from naja.constants import KEYS
+from naja.events import SceneChangeEvent, QuitGameEvent
 from naja.scenes.scene import Scene
-from naja.widgets.text import TextWidget
-from naja.widgets.selector import SelectorWidget
-from naja.events import QuitGameEvent
 from naja.scenes.credits import CreditsScene
 from naja.scenes.game import GameScene
-from naja.events import SceneChangeEvent
+from naja.scenes.load_save import SaveGameScene
+from naja.widgets.selector import SelectorWidget
+from naja.widgets.text import TextWidget
 
 
 class MenuScene(Scene):
@@ -19,15 +19,23 @@ class MenuScene(Scene):
         super(MenuScene, self).__init__(state)
         selector = SelectorWidget()
         self.add(selector)
+
         run_game = TextWidget((100, 100), 'Game', fontsize=32, colour='white')
-        run_game.add_callback('click',
-                              lambda event: SceneChangeEvent.post(GameScene))
+        run_game.add_callback(
+            'click', lambda event: SceneChangeEvent.post(GameScene))
         selector.add(run_game)
+
         credits = TextWidget(
-            (100, 200), 'Credits', fontsize=32, colour='white')
-        credits.add_callback('click',
-                             lambda event: SceneChangeEvent.post(CreditsScene))
+            (100, 150), 'Credits', fontsize=32, colour='white')
+        credits.add_callback(
+            'click', lambda event: SceneChangeEvent.post(CreditsScene))
         selector.add(credits)
+
+        save = TextWidget((100, 250), 'Save', fontsize=32, colour='white')
+        save.add_callback(
+            'click', lambda event: SceneChangeEvent.post(SaveGameScene))
+        selector.add(save)
+
         quit = TextWidget((100, 300), 'Quit', fontsize=32, colour='white')
         quit.add_callback('click', lambda event: QuitGameEvent.post())
         selector.add(quit)
index d843c7e9fc86e2cee5e913551c08946f3f9f8d31..e3aa32e03be48a27e0da319219fa880e25491c77 100644 (file)
@@ -21,10 +21,15 @@ class TestGameBoard(TestCase):
         self.assertEqual(state1, state2)
 
     def test_export_new_board(self):
-        board = GameBoard.new_game([{'actions': [{
-            'action_class': 'LoseHealthOrMSB',
-            'required_bits': [],
-        }]}])
+        board = GameBoard.new_game([{'actions': [
+            {
+                'action_class': 'LoseHealthOrMSB',
+                'required_bits': [],
+            }, {
+                'action_class': 'GainHealth',
+                'required_bits': [BITS.RED],
+            },
+        ]}])
         exported_state = board.export()
         board_locations = exported_state.pop('board_locations')
         self.assertEqual(exported_state, {
@@ -32,24 +37,36 @@ class TestGameBoard(TestCase):
             'health': 4,
             'wins_required': 4,
             'wins': 0,
-            'locations': [{'actions': [{
-                'action_class': 'LoseHealthOrMSB',
-                'required_bits': [],
-            }]}],
+            'locations': [{'actions': [
+                {
+                    'action_class': 'LoseHealthOrMSB',
+                    'required_bits': [],
+                }, {
+                    'action_class': 'GainHealth',
+                    'required_bits': [BITS.RED],
+                },
+            ]}],
             'player': board.player.export(),
         })
-        self.assertEqual(
-            set(board_locations.keys()),
-            set((x, y) for x in range(5) for y in range(5)))
-        for location_state in board_locations.values():
+        positions = []
+        for position, location_state in board_locations:
+            positions.append(position)
             self.assertEqual(
                 sorted(location_state.keys()), ['actions', 'bitwise_operand'])
-            self.assertEqual(location_state['actions'], [{
-                'action_class': 'LoseHealthOrMSB',
-                'required_bits': [],
-                'data': {},
-            }])
+            self.assertEqual(location_state['actions'], [
+                {
+                    'action_class': 'LoseHealthOrMSB',
+                    'required_bits': [],
+                    'data': {},
+                }, {
+                    'action_class': 'GainHealth',
+                    'required_bits': [BITS.RED],
+                    'data': {},
+                },
+            ])
             self.assertTrue(2 <= len(location_state['bitwise_operand']) <= 3)
+        self.assertEqual(
+            positions, sorted((x, y) for x in range(5) for y in range(5)))
 
     def test_lose_health(self):
         board = GameBoard.new_game([{'actions': []}])
diff --git a/naja/widgets/save_slot.py b/naja/widgets/save_slot.py
new file mode 100644 (file)
index 0000000..97be9ee
--- /dev/null
@@ -0,0 +1,72 @@
+import pygame
+
+from naja.constants import BITS, PALETTE
+from naja.widgets.base import Widget
+from naja.widgets.text import TextWidget, TextBoxWidget
+
+
+class SaveSlotWidget(Widget):
+    """
+    Widget for displaying a save slot.
+    """
+    def __init__(self, pos, slot, game_data):
+        super(SaveSlotWidget, self).__init__(pos, (600, 64))
+        self.slot = slot
+        self.game_data = game_data
+
+    def prepare(self):
+        self.surface = pygame.surface.Surface(self.size)
+        header = self.get_slot_header()
+        name_text = "Slot %s: %s" % (self.slot, header)
+        name = TextWidget((0, 0), name_text, colour=PALETTE.WHITE)
+        name.render(self.surface)
+        self.prepare_game_state()
+
+    def get_slot_header(self):
+        if self.game_data is None:
+            return "--"
+        else:
+            return self.game_data['timestamp']
+
+    def render_state_glyph(self, x_offset, text, colour):
+        widget = TextBoxWidget(
+            (x_offset, 32), text, box_width=32, colour=colour,
+            bg_colour=PALETTE.BLACK)
+        widget.render(self.surface)
+
+    def prepare_game_state(self):
+        if self.game_data is None:
+            state_text = TextWidget((32, 32), '-EMPTY-', colour=PALETTE.WHITE)
+            state_text.render(self.surface)
+        else:
+            state_data = self.game_data['data']
+            x_offset = 0
+
+            for i in range(state_data['max_health']):
+                x_offset += 32
+                if i < state_data['health']:
+                    colour = PALETTE.PINK
+                else:
+                    colour = PALETTE.DARK_RED
+                self.render_state_glyph(x_offset, '{HEALTH_NOCOLOUR}', colour)
+
+            for i in range(state_data['wins_required']):
+                x_offset += 32
+                if i < state_data['wins']:
+                    colour = PALETTE.LIGHT_TURQUOISE
+                else:
+                    colour = PALETTE.DARK_OLIVE
+                self.render_state_glyph(
+                    x_offset, '{WINTOKEN_NOCOLOUR}', colour)
+
+            x_offset += 64
+            bit_names = dict((v, k) for k, v in BITS.items())
+            player_bits = state_data['player']['bits']
+            for bit in reversed(range(8)):
+                x_offset += 32
+                if (1 << bit) & player_bits:
+                    text = '{%s}' % (bit_names[bit],)
+                    self.render_state_glyph(x_offset, text, PALETTE.GREY)
+
+    def draw(self, surface):
+        surface.blit(self.surface, self.pos)
index e4eecaacad57e2f53ffdf07a9b8419a43896166f..4343d015e2519f77b48accf314f1d99bf21fe65d 100644 (file)
@@ -21,6 +21,9 @@ MARKUP_MAP = {
     'BLUE': ('glyphs/key.png', PALETTE.BLUE),
     'CLOCKWISE': ('glyphs/clockwise.png', None),
     'ANTICLOCKWISE': ('glyphs/anticlockwise.png', None),
+
+    'HEALTH_NOCOLOUR': ('glyphs/health.png', None),
+    'WINTOKEN_NOCOLOUR': ('glyphs/win.png', None),
 }