1 from random import choice
3 from naja.constants import(
4 BITS, DIRECTION_BITS, CONDITION_BITS, PLAYER_DEFAULTS,
5 ACT, EXAMINE, ROTATION)
6 from naja.options import options
7 from naja.player import Player
8 from naja import actions
11 class GameBoard(object):
13 A representation of the game board.
16 def __init__(self, state, player, board_locations):
17 self.max_health = state['max_health']
18 self.wins_required = state['wins_required']
19 self.health = state['health']
20 self.wins = state['wins']
21 self.locations = [item.copy() for item in state['locations']]
22 self.puzzle = state['puzzle']
24 self.board_locations = board_locations
25 self.player_mode = EXAMINE
28 def new_game(cls, deck,
29 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
30 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
31 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
32 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
33 if options.initial_bits:
34 initial_bits = options.initial_bits
36 'max_health': max_health,
38 'wins_required': wins_required,
40 'locations': deck['cards'],
41 'puzzle': deck.get('puzzle', False),
43 player = Player(initial_bits, initial_pos)
44 board_locations = cls.import_board_locations(
45 cls.generate_board(deck))
46 return cls(state, player, board_locations)
49 def import_game(cls, definition):
50 state = definition.copy()
51 player = Player.import_player(state.pop('player'))
52 board_locations = cls.import_board_locations(
53 state.pop('board_locations'))
54 return cls(state, player, board_locations)
58 'max_health': self.max_health,
59 'health': self.health,
60 'wins_required': self.wins_required,
62 'locations': [item.copy() for item in self.locations],
63 'puzzle': self.puzzle,
64 'player': self.player.export(),
65 'board_locations': self.export_board_locations(),
69 def import_locations(cls, locations_definition):
71 LocationCard.import_location(definition)
72 for definition in locations_definition]
74 def export_board_locations(self):
76 (position, location.export())
77 for position, location in self.board_locations.iteritems())
80 def import_board_locations(cls, board_locations_definition):
82 (tuple(position), LocationCard.import_location(definition))
83 for position, definition in board_locations_definition)
86 def generate_board(cls, deck):
87 if deck.get('puzzle', False):
88 return cls.generate_puzzle_board(deck)
90 return cls.generate_random_board(deck)
93 def generate_puzzle_board(cls, deck):
94 assert len(deck['cards']) == 5 * 5
97 LocationCard.new_location(card.copy()).export()]
98 for i, card in enumerate(deck['cards'])
100 return board_locations
103 def generate_random_board(cls, deck):
107 board_location = LocationCard.new_location(
108 choice(deck['cards']).copy())
109 board_locations.append([(x, y), board_location.export()])
110 return board_locations
112 def lose_health(self):
115 self.end_game(win=False)
117 def gain_health(self):
118 if self.health < self.max_health:
121 def acquire_win_token(self):
123 if self.wins >= self.wins_required:
124 self.end_game(win=True)
126 def replace_card(self, position):
127 location = LocationCard.new_location(choice(self.locations).copy())
128 self.board_locations[position] = location
130 def shift_location_row(self, change, is_vertical):
131 px, py = self.player.position
132 shifted_locations = {}
133 mkpos = lambda i: (px, i) if is_vertical else (i, py)
136 if (px, py) == mkpos(i):
138 new_i = (i + change) % 5
139 if (px, py) == mkpos(new_i):
140 new_i = (new_i + change) % 5
141 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
143 self.board_locations.update(shifted_locations)
145 def shift_locations(self, direction):
146 if BITS[direction] == BITS.NORTH:
147 self.shift_location_row(-1, is_vertical=True)
148 elif BITS[direction] == BITS.SOUTH:
149 self.shift_location_row(1, is_vertical=True)
150 elif BITS[direction] == BITS.EAST:
151 self.shift_location_row(1, is_vertical=False)
152 elif BITS[direction] == BITS.WEST:
153 self.shift_location_row(-1, is_vertical=False)
155 def rotate_locations(self, direction):
156 px, py = self.player.position
157 locations_to_rotate = []
158 rotated_locations = {}
161 for i in range(max(0, px - 1), min(5, px + 2)):
162 locations_to_rotate.append((i, py - 1))
165 locations_to_rotate.append((px + 1, py))
168 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
169 locations_to_rotate.append((i, py + 1))
172 locations_to_rotate.append((px - 1, py))
174 if ROTATION[direction] == ROTATION.CLOCKWISE:
175 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
176 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
178 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
180 for old, new in zip(locations_to_rotate, new_positions):
181 rotated_locations[old] = self.board_locations[new]
183 self.board_locations.update(rotated_locations)
185 def allow_chess_move(self, chesspiece):
186 self.player.allow_chess_move(chesspiece)
188 def change_mode(self, new_mode):
189 """Advance to the next mode"""
190 if new_mode == self.player_mode:
191 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
193 elif new_mode in (ACT, EXAMINE):
194 self.player_mode = new_mode
196 raise RuntimeError("Illegal player mode %s" % self.player_mode)
198 def end_game(self, win):
199 # TODO: Find a way to not have UI stuff in game logic stuff.
200 from naja.events import SceneChangeEvent
201 from naja.scenes.lose import LoseScene
202 from naja.scenes.win import WinScene
204 SceneChangeEvent.post(WinScene)
206 SceneChangeEvent.post(LoseScene)
209 class LocationCard(object):
211 A particular set of options available on a location.
214 def __init__(self, bitwise_operand, location_actions):
215 self.bitwise_operand = bitwise_operand
216 self.actions = location_actions
220 def import_location(cls, state):
222 cls.build_action(definition) for definition in state['actions']]
223 return cls(state['bitwise_operand'], location_actions)
226 def build_action(cls, definition):
227 action_class = getattr(actions, definition['action_class'])
228 required_bits = cls.parse_bits(definition['required_bits'])
229 data = definition.get('data', {})
230 return action_class(required_bits, **data)
233 def new_location(cls, definition):
234 if 'bits' in definition:
235 bits = cls.parse_bits(definition['bits'])
237 bits = cls.generate_bitwise_operand()
238 return cls.import_location({
239 'bitwise_operand': bits,
240 'actions': definition['actions'],
244 def parse_bits(self, bit_list):
245 # Convert names to numbers if applicable.
246 return frozenset(BITS.get(bit, bit) for bit in bit_list)
250 'bitwise_operand': sorted(self.bitwise_operand),
251 'actions': [action.export() for action in self.actions],
254 def check_actions(self):
256 print "Warning: Location has no actions."
257 self.insert_default_default_action()
258 if self.actions[0].required_bits:
259 self.insert_default_default_action()
261 def insert_default_default_action(self):
262 self.actions.insert(0, self.build_action({
263 'action_class': 'DoNothing',
268 def generate_bitwise_operand():
270 Generate a set of two or three bits. At least one direction and one
271 condition bit will be included. There is a low probability of choosing
272 a third bit from the complete set.
275 bits.add(choice(DIRECTION_BITS.values()))
276 bits.add(choice(CONDITION_BITS.values()))
277 # One in three chance of adding a third bit, with a further one in four
278 # chance that it will match a bit already chosen.
279 if choice(range(3)) == 0:
280 bits.add(choice(BITS.values()))
281 return frozenset(bits)