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] = tuple(v)
50 defaults.update(puzzle_defaults)
52 if initial_bits is None:
53 initial_bits = defaults['initial_bits']
54 if initial_pos is None:
55 initial_pos = defaults['initial_pos']
56 if max_health is None:
57 max_health = defaults['max_health']
58 if wins_required is None:
59 wins_required = defaults['wins_required']
61 # Overriden by command line
62 if options.initial_bits:
63 initial_bits = options.initial_bits
66 'max_health': max_health,
68 'wins_required': wins_required,
70 'locations': deck['cards'],
73 'replacement_params': deck.get('replacement_params', None),
75 player = Player(initial_bits, initial_pos)
76 board_locations = cls.import_board_locations(
77 cls.generate_board(deck))
78 board = cls(state, player, board_locations)
79 player.set_gameboard(board)
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, puzzle=True).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=None, 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
305 self.replacement_time = replacement_time
308 def import_location(cls, state):
310 cls.build_action(definition) for definition in state['actions']]
311 return cls(state['card_name'], state['bitwise_operand'],
312 location_actions, state['replacement_time'],
316 def build_action(cls, definition):
317 action_class = getattr(actions, definition['action_class'])
318 required_bits = cls.parse_bits(definition['required_bits'])
319 data = definition.get('data', {})
320 return action_class(required_bits, **data)
323 def new_location(cls, definition, replacement_params=None, puzzle=False):
324 if 'bits' in definition:
325 bits = cls.parse_bits(definition['bits'])
327 bits = cls.generate_bitwise_operand()
329 if 'replacement_time' in definition:
330 replacement_time = definition['replacement_time']
332 replacement_time = cls.generate_replacement_time(
335 max_number = definition.get('max_number', 25)
336 card_name = definition['card_name']
337 location = cls.import_location({
338 'bitwise_operand': bits,
339 'actions': definition['actions'],
340 'max_number': max_number,
341 'card_name': card_name,
342 'replacement_time': replacement_time,
345 location.check_actions()
349 def parse_bits(self, bit_list):
350 # Convert names to numbers if applicable.
351 return frozenset(BITS.get(bit, bit) for bit in bit_list)
355 'bitwise_operand': sorted(self.bitwise_operand),
356 'actions': [action.export() for action in self.actions],
357 'max_number': self.max_number,
358 'card_name': self.card_name,
359 'replacement_time': self.replacement_time,
362 def check_actions(self):
364 print "Warning: Location has no actions."
365 self.insert_default_default_action()
366 if self.actions[0].required_bits:
367 self.insert_default_default_action()
369 def insert_default_default_action(self):
370 self.actions.insert(0, self.build_action({
371 'action_class': 'DoNothing',
376 def generate_bitwise_operand():
378 Generate a set of two or three bits. At least one direction and one
379 condition bit will be included. There is a low probability of choosing
380 a third bit from the complete set.
383 bits.add(choice(DIRECTION_BITS.values()))
384 bits.add(choice(CONDITION_BITS.values()))
385 # One in three chance of adding a third bit, with a further one in four
386 # chance that it will match a bit already chosen.
387 if choice(range(3)) == 0:
388 bits.add(choice(BITS.values()))
389 return frozenset(bits)
392 def generate_replacement_time(replacement_params):
393 if replacement_params is None:
396 if replacement_params['chance'] > random.random():
397 return random.randint(replacement_params['min'],
398 replacement_params['max'])
402 def timer_action(self, position, board):
403 if self.replacement_time is not None:
404 self.replacement_time -= 1
405 if self.replacement_time <= 0:
406 board.replace_card(position)