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,
104 if options.cheat_enabled:
105 self.has_cheated = True
107 data['cheater'] = True
111 def import_locations(cls, locations_definition):
113 LocationCard.import_location(definition)
114 for definition in locations_definition]
116 def export_board_locations(self):
118 (position, location.export())
119 for position, location in self.board_locations.iteritems())
122 def import_board_locations(cls, board_locations_definition):
124 (tuple(position), LocationCard.import_location(definition))
125 for position, definition in board_locations_definition)
128 def generate_board(cls, deck):
129 if deck.get('puzzle', False):
130 return cls.generate_puzzle_board(deck)
132 return cls.generate_random_board(deck)
135 def generate_puzzle_board(cls, deck):
136 assert len(deck['cards']) == 5 * 5
137 replacement_params = deck.get('replacement_params', None)
140 LocationCard.new_location(
141 card.copy(), replacement_params).export()]
142 for i, card in enumerate(deck['cards'])
144 return board_locations
147 def generate_random_board(cls, deck):
149 replacement_params = deck.get('replacement_params', None)
152 new_choice = cls.choose_card(deck['cards'], board_locations)
153 board_location = LocationCard.new_location(
154 new_choice.copy(), replacement_params)
155 board_locations.append([(x, y), board_location.export()])
156 return board_locations
158 def lose_health(self):
161 self.end_game(win=False)
163 def gain_health(self):
164 if self.health < self.max_health:
167 def acquire_win_token(self):
169 if self.wins >= self.wins_required:
170 self.end_game(win=True)
172 def card_used(self, position):
174 self.replace_card(position)
176 def replace_card(self, position):
177 new_choice = self.choose_card(self.locations,
178 self.board_locations.items(),
180 location = LocationCard.new_location(new_choice.copy(),
181 self.replacement_params)
182 self.board_locations[position] = location
185 def choose_card(cls, cards, board_locations, position=None):
186 # Find which cards are at their maximum and exclude them from
189 choices = {card['card_name']: card for card in cards}
190 for pos, card in board_locations:
192 # skip the card we're replacing if appropriate
194 if isinstance(card, LocationCard):
196 max_num = card.max_number
198 key = card['card_name']
199 max_num = card.get('max_number', 25)
200 counts.setdefault(key, 0)
202 if counts[key] >= max_num:
205 return choice(choices.values())
207 def shift_location_row(self, change, is_vertical):
208 px, py = self.player.position
209 shifted_locations = {}
210 mkpos = lambda i: (px, i) if is_vertical else (i, py)
213 if (px, py) == mkpos(i):
215 new_i = (i + change) % 5
216 if (px, py) == mkpos(new_i):
217 new_i = (new_i + change) % 5
218 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
220 self.board_locations.update(shifted_locations)
222 def shift_locations(self, direction):
223 if BITS[direction] == BITS.NORTH:
224 self.shift_location_row(-1, is_vertical=True)
225 elif BITS[direction] == BITS.SOUTH:
226 self.shift_location_row(1, is_vertical=True)
227 elif BITS[direction] == BITS.EAST:
228 self.shift_location_row(1, is_vertical=False)
229 elif BITS[direction] == BITS.WEST:
230 self.shift_location_row(-1, is_vertical=False)
232 def rotate_locations(self, direction):
233 px, py = self.player.position
234 locations_to_rotate = []
235 rotated_locations = {}
238 for i in range(max(0, px - 1), min(5, px + 2)):
239 locations_to_rotate.append((i, py - 1))
242 locations_to_rotate.append((px + 1, py))
245 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
246 locations_to_rotate.append((i, py + 1))
249 locations_to_rotate.append((px - 1, py))
251 if ROTATION[direction] == ROTATION.CLOCKWISE:
252 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
253 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
255 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
257 for old, new in zip(locations_to_rotate, new_positions):
258 rotated_locations[new] = self.board_locations[old]
260 self.board_locations.update(rotated_locations)
262 def allow_chess_move(self, chesspiece):
263 self.player.allow_chess_move(chesspiece)
265 def change_mode(self, new_mode):
266 """Advance to the next mode"""
267 if new_mode == self.player_mode:
268 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
270 elif new_mode in (ACT, EXAMINE):
271 self.player_mode = new_mode
272 if new_mode is EXAMINE:
275 raise RuntimeError("Illegal player mode %s" % self.player_mode)
277 def board_update(self):
278 self.clock_count += 1
279 for position, location in self.board_locations.iteritems():
280 location.timer_action(position, self)
282 def end_game(self, win):
283 # TODO: Find a way to not have UI stuff in game logic stuff.
284 from naja.events import SceneChangeEvent
285 from naja.scenes.lose import LoseScene
286 from naja.scenes.win import WinScene
289 SceneChangeEvent.post(WinScene)
291 SceneChangeEvent.post(LoseScene)
294 class LocationCard(object):
296 A particular set of options available on a location.
299 def __init__(self, card_name, bitwise_operand, location_actions,
300 replacement_time, max_number=25):
301 self.card_name = card_name
302 self.bitwise_operand = bitwise_operand
303 self.actions = location_actions
304 self.max_number = max_number
306 self.replacement_time = replacement_time
309 def import_location(cls, state):
311 cls.build_action(definition) for definition in state['actions']]
312 return cls(state['card_name'], state['bitwise_operand'],
313 location_actions, state['replacement_time'],
317 def build_action(cls, definition):
318 action_class = getattr(actions, definition['action_class'])
319 required_bits = cls.parse_bits(definition['required_bits'])
320 data = definition.get('data', {})
321 return action_class(required_bits, **data)
324 def new_location(cls, definition, replacement_params):
325 if 'bits' in definition:
326 bits = cls.parse_bits(definition['bits'])
328 bits = cls.generate_bitwise_operand()
330 if 'replacement_time' in definition:
331 replacement_time = definition['replacement_time']
333 replacement_time = cls.generate_replacement_time(
336 max_number = definition.get('max_number', 25)
337 card_name = definition['card_name']
338 return cls.import_location({
339 'bitwise_operand': bits,
340 'actions': definition['actions'],
341 'max_number': max_number,
342 'card_name': card_name,
343 'replacement_time': replacement_time,
347 def parse_bits(self, bit_list):
348 # Convert names to numbers if applicable.
349 return frozenset(BITS.get(bit, bit) for bit in bit_list)
353 'bitwise_operand': sorted(self.bitwise_operand),
354 'actions': [action.export() for action in self.actions],
355 'max_number': self.max_number,
356 'card_name': self.card_name,
357 'replacement_time': self.replacement_time,
360 def check_actions(self):
362 print "Warning: Location has no actions."
363 self.insert_default_default_action()
364 if self.actions[0].required_bits:
365 self.insert_default_default_action()
367 def insert_default_default_action(self):
368 self.actions.insert(0, self.build_action({
369 'action_class': 'DoNothing',
374 def generate_bitwise_operand():
376 Generate a set of two or three bits. At least one direction and one
377 condition bit will be included. There is a low probability of choosing
378 a third bit from the complete set.
381 bits.add(choice(DIRECTION_BITS.values()))
382 bits.add(choice(CONDITION_BITS.values()))
383 # One in three chance of adding a third bit, with a further one in four
384 # chance that it will match a bit already chosen.
385 if choice(range(3)) == 0:
386 bits.add(choice(BITS.values()))
387 return frozenset(bits)
390 def generate_replacement_time(replacement_params):
391 if replacement_params is None:
394 if replacement_params['chance'] > random.random():
395 return random.randint(replacement_params['min'],
396 replacement_params['max'])
400 def timer_action(self, position, board):
401 if self.replacement_time is not None:
402 self.replacement_time -= 1
403 if self.replacement_time <= 0:
404 board.replace_card(position)