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'
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):
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:
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]
def export(self):
return {
- 'bitwise_operand': self.bitwise_operand,
+ 'bitwise_operand': sorted(self.bitwise_operand),
'actions': [action.export() for action in self.actions],
}
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):
import optparse
import os
+import sys
from naja.attrdict import AttrDict
from naja.constants import DEFAULTS
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)
--- /dev/null
+"""
+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
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):
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)
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, {
'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': []}])
--- /dev/null
+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)
'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),
}