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
9 from naja.sound import sound
13 class GameBoard(object):
15 A representation of the game board.
18 def __init__(self, state, player, board_locations):
19 self.max_health = state['max_health']
20 self.wins_required = state['wins_required']
21 self.health = state['health']
22 self.wins = state['wins']
23 self.locations = [item.copy() for item in state['locations']]
24 self.puzzle = state.get('puzzle', False)
26 self.board_locations = board_locations
27 self.player_mode = state.get('player_mode', EXAMINE)
28 self.has_cheated = state.get('cheater', options.cheat_enabled)
29 self.clock_count = state.get('clock_count', 0)
30 self.replacement_params = state.get('replacement_params', None)
33 def new_game(cls, deck, initial_bits=None, initial_pos=None,
34 max_health=None, wins_required=None):
37 'initial_bits': PLAYER_DEFAULTS.INITIAL_BITS,
38 'initial_pos': PLAYER_DEFAULTS.INITIAL_POS,
39 'max_health': PLAYER_DEFAULTS.MAX_HEALTH,
40 'wins_required': PLAYER_DEFAULTS.WINS_REQUIRED,
43 puzzle = deck.get('puzzle', False)
46 puzzle_defaults = deck.get('defaults', {})
47 for k, v in puzzle_defaults.iteritems():
48 if isinstance(v, list):
49 puzzle_defaults[k] = [int(x) for x in v]
51 puzzle_defaults[k] = int(v)
52 defaults.update(puzzle_defaults)
54 if initial_bits is None:
55 initial_bits = defaults['initial_bits']
56 if initial_pos is None:
57 initial_pos = defaults['initial_pos']
58 if max_health is None:
59 max_health = defaults['max_health']
60 if wins_required is None:
61 wins_required = defaults['wins_required']
63 # Overriden by command line
64 if options.initial_bits:
65 initial_bits = options.initial_bits
68 'max_health': max_health,
70 'wins_required': wins_required,
72 'locations': deck['cards'],
75 'replacement_params': deck.get('replacement_params', None),
77 player = Player(initial_bits, initial_pos)
78 board_locations = cls.import_board_locations(
79 cls.generate_board(deck))
80 return cls(state, player, board_locations)
83 def import_game(cls, definition):
84 state = definition.copy()
85 player = Player.import_player(state.pop('player'))
86 board_locations = cls.import_board_locations(
87 state.pop('board_locations'))
88 return cls(state, player, board_locations)
92 'max_health': self.max_health,
93 'health': self.health,
94 'wins_required': self.wins_required,
96 'locations': [item.copy() for item in self.locations],
97 'puzzle': self.puzzle,
98 'player': self.player.export(),
99 'board_locations': self.export_board_locations(),
100 'player_mode': self.player_mode,
101 'clock_count': self.clock_count,
102 'replacement_params': self.replacement_params,
105 data['cheater'] = True
109 def import_locations(cls, locations_definition):
111 LocationCard.import_location(definition)
112 for definition in locations_definition]
114 def export_board_locations(self):
116 (position, location.export())
117 for position, location in self.board_locations.iteritems())
120 def import_board_locations(cls, board_locations_definition):
122 (tuple(position), LocationCard.import_location(definition))
123 for position, definition in board_locations_definition)
126 def generate_board(cls, deck):
127 if deck.get('puzzle', False):
128 return cls.generate_puzzle_board(deck)
130 return cls.generate_random_board(deck)
133 def generate_puzzle_board(cls, deck):
134 assert len(deck['cards']) == 5 * 5
135 replacement_params = deck.get('replacement_params', None)
138 LocationCard.new_location(
139 card.copy(), replacement_params).export()]
140 for i, card in enumerate(deck['cards'])
142 return board_locations
145 def generate_random_board(cls, deck):
147 replacement_params = deck.get('replacement_params', None)
150 new_choice = cls.choose_card(deck['cards'], board_locations)
151 board_location = LocationCard.new_location(
152 new_choice.copy(), replacement_params)
153 board_locations.append([(x, y), board_location.export()])
154 return board_locations
156 def lose_health(self):
159 self.end_game(win=False)
161 def gain_health(self):
162 if self.health < self.max_health:
165 def acquire_win_token(self):
167 if self.wins >= self.wins_required:
168 self.end_game(win=True)
170 def card_used(self, position):
172 self.replace_card(position)
174 def replace_card(self, position):
175 new_choice = self.choose_card(self.locations,
176 self.board_locations.items(),
178 location = LocationCard.new_location(new_choice.copy(),
179 self.replacement_params)
180 self.board_locations[position] = location
183 def choose_card(cls, cards, board_locations, position=None):
184 # Find which cards are at their maximum and exclude them from
187 choices = {card['card_name']: card for card in cards}
188 for pos, card in board_locations:
190 # skip the card we're replacing if appropriate
192 if isinstance(card, LocationCard):
194 max_num = card.max_number
196 key = card['card_name']
197 max_num = card.get('max_number', 25)
198 counts.setdefault(key, 0)
200 if counts[key] >= max_num:
203 return choice(choices.values())
205 def shift_location_row(self, change, is_vertical):
206 px, py = self.player.position
207 shifted_locations = {}
208 mkpos = lambda i: (px, i) if is_vertical else (i, py)
211 if (px, py) == mkpos(i):
213 new_i = (i + change) % 5
214 if (px, py) == mkpos(new_i):
215 new_i = (new_i + change) % 5
216 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
218 self.board_locations.update(shifted_locations)
220 def shift_locations(self, direction):
221 if BITS[direction] == BITS.NORTH:
222 self.shift_location_row(-1, is_vertical=True)
223 elif BITS[direction] == BITS.SOUTH:
224 self.shift_location_row(1, is_vertical=True)
225 elif BITS[direction] == BITS.EAST:
226 self.shift_location_row(1, is_vertical=False)
227 elif BITS[direction] == BITS.WEST:
228 self.shift_location_row(-1, is_vertical=False)
230 def rotate_locations(self, direction):
231 px, py = self.player.position
232 locations_to_rotate = []
233 rotated_locations = {}
236 for i in range(max(0, px - 1), min(5, px + 2)):
237 locations_to_rotate.append((i, py - 1))
240 locations_to_rotate.append((px + 1, py))
243 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
244 locations_to_rotate.append((i, py + 1))
247 locations_to_rotate.append((px - 1, py))
249 if ROTATION[direction] == ROTATION.CLOCKWISE:
250 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
251 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
252 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
254 for old, new in zip(locations_to_rotate, new_positions):
255 rotated_locations[new] = self.board_locations[old]
257 self.board_locations.update(rotated_locations)
259 def allow_chess_move(self, chesspiece):
260 self.player.allow_chess_move(chesspiece)
262 def change_mode(self, new_mode):
263 """Advance to the next mode"""
264 if new_mode == self.player_mode:
265 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
267 elif new_mode in (ACT, EXAMINE):
268 self.player_mode = new_mode
269 if new_mode is EXAMINE:
272 raise RuntimeError("Illegal player mode %s" % self.player_mode)
274 def board_update(self):
275 self.clock_count += 1
276 for position, location in self.board_locations.iteritems():
277 location.timer_action(position, self)
279 def end_game(self, win):
280 # TODO: Find a way to not have UI stuff in game logic stuff.
281 from naja.events import SceneChangeEvent
282 from naja.scenes.lose import LoseScene
283 from naja.scenes.win import WinScene
286 SceneChangeEvent.post(WinScene)
288 SceneChangeEvent.post(LoseScene)
291 class LocationCard(object):
293 A particular set of options available on a location.
296 def __init__(self, card_name, bitwise_operand, location_actions,
297 replacement_time, max_number=25):
298 self.card_name = card_name
299 self.bitwise_operand = bitwise_operand
300 self.actions = location_actions
301 self.max_number = max_number
303 self.replacement_time = replacement_time
306 def import_location(cls, state):
308 cls.build_action(definition) for definition in state['actions']]
309 return cls(state['card_name'], state['bitwise_operand'],
310 location_actions, state['replacement_time'],
314 def build_action(cls, definition):
315 action_class = getattr(actions, definition['action_class'])
316 required_bits = cls.parse_bits(definition['required_bits'])
317 data = definition.get('data', {})
318 return action_class(required_bits, **data)
321 def new_location(cls, definition, replacement_params):
322 if 'bits' in definition:
323 bits = cls.parse_bits(definition['bits'])
325 bits = cls.generate_bitwise_operand()
327 if 'replacement_time' in definition:
328 replacement_time = definition['replacement_time']
330 replacement_time = cls.generate_replacement_time(
333 max_number = definition.get('max_number', 25)
334 card_name = definition['card_name']
335 return cls.import_location({
336 'bitwise_operand': bits,
337 'actions': definition['actions'],
338 'max_number': max_number,
339 'card_name': card_name,
340 'replacement_time': replacement_time,
344 def parse_bits(self, bit_list):
345 # Convert names to numbers if applicable.
346 return frozenset(BITS.get(bit, bit) for bit in bit_list)
350 'bitwise_operand': sorted(self.bitwise_operand),
351 'actions': [action.export() for action in self.actions],
352 'max_number': self.max_number,
353 'card_name': self.card_name,
354 'replacement_time': self.replacement_time,
357 def check_actions(self):
359 print "Warning: Location has no actions."
360 self.insert_default_default_action()
361 if self.actions[0].required_bits:
362 self.insert_default_default_action()
364 def insert_default_default_action(self):
365 self.actions.insert(0, self.build_action({
366 'action_class': 'DoNothing',
371 def generate_bitwise_operand():
373 Generate a set of two or three bits. At least one direction and one
374 condition bit will be included. There is a low probability of choosing
375 a third bit from the complete set.
378 bits.add(choice(DIRECTION_BITS.values()))
379 bits.add(choice(CONDITION_BITS.values()))
380 # One in three chance of adding a third bit, with a further one in four
381 # chance that it will match a bit already chosen.
382 if choice(range(3)) == 0:
383 bits.add(choice(BITS.values()))
384 return frozenset(bits)
387 def generate_replacement_time(replacement_params):
388 if replacement_params is None:
391 if replacement_params['chance'] > random.random():
392 return random.randint(replacement_params['min'],
393 replacement_params['max'])
397 def timer_action(self, position, board):
398 if self.replacement_time is not None:
399 self.replacement_time -= 1
400 if self.replacement_time <= 0:
401 board.replace_card(position)