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
10 from naja.utils import parse_bits
14 class GameBoard(object):
16 A representation of the game board.
19 def __init__(self, state, player, board_locations):
20 self.max_health = state['max_health']
21 self.wins_required = state['wins_required']
22 self.health = state['health']
23 self.wins = state['wins']
24 self.locations = [item.copy() for item in state['locations']]
25 self.puzzle = state.get('puzzle', False)
27 self.board_locations = board_locations
28 self.player_mode = state.get('player_mode', EXAMINE)
29 self.has_cheated = state.get('cheater', options.cheat_enabled)
30 self.clock_count = state.get('clock_count', 0)
31 self.replacement_params = state.get('replacement_params', None)
34 def new_game(cls, deck, initial_bits=None, initial_pos=None,
35 max_health=None, wins_required=None):
38 'initial_bits': PLAYER_DEFAULTS.INITIAL_BITS,
39 'initial_pos': PLAYER_DEFAULTS.INITIAL_POS,
40 'max_health': PLAYER_DEFAULTS.MAX_HEALTH,
41 'wins_required': PLAYER_DEFAULTS.WINS_REQUIRED,
44 puzzle = deck.get('puzzle', False)
47 puzzle_defaults = deck.get('defaults', {})
48 for k, v in puzzle_defaults.iteritems():
49 if isinstance(v, list):
50 puzzle_defaults[k] = tuple(v)
51 defaults.update(puzzle_defaults)
53 if initial_bits is None:
54 initial_bits = defaults['initial_bits']
55 if initial_pos is None:
56 initial_pos = defaults['initial_pos']
57 if max_health is None:
58 max_health = defaults['max_health']
59 if wins_required is None:
60 wins_required = defaults['wins_required']
62 # Overriden by command line
63 if options.initial_bits:
64 initial_bits = options.initial_bits
67 'max_health': max_health,
69 'wins_required': wins_required,
71 'locations': deck['cards'],
74 'replacement_params': deck.get('replacement_params', None),
76 player = Player(initial_bits, initial_pos)
77 board_locations = cls.import_board_locations(
78 cls.generate_board(deck))
79 board = cls(state, player, board_locations)
80 player.set_gameboard(board)
84 def import_game(cls, definition):
85 state = definition.copy()
86 player = Player.import_player(state.pop('player'))
87 board_locations = cls.import_board_locations(
88 state.pop('board_locations'))
89 return cls(state, player, board_locations)
93 'max_health': self.max_health,
94 'health': self.health,
95 'wins_required': self.wins_required,
97 'locations': [item.copy() for item in self.locations],
98 'puzzle': self.puzzle,
99 'player': self.player.export(),
100 'board_locations': self.export_board_locations(),
101 'player_mode': self.player_mode,
102 'clock_count': self.clock_count,
103 'replacement_params': self.replacement_params,
105 if options.cheat_enabled:
106 self.has_cheated = True
108 data['cheater'] = True
112 def import_locations(cls, locations_definition):
114 LocationCard.import_location(definition)
115 for definition in locations_definition]
117 def export_board_locations(self):
119 (position, location.export())
120 for position, location in self.board_locations.iteritems())
123 def import_board_locations(cls, board_locations_definition):
125 (tuple(position), LocationCard.import_location(definition))
126 for position, definition in board_locations_definition)
129 def generate_board(cls, deck):
130 if deck.get('puzzle', False):
131 return cls.generate_puzzle_board(deck)
133 return cls.generate_random_board(deck)
136 def generate_puzzle_board(cls, deck):
137 assert len(deck['cards']) == 5 * 5
138 replacement_params = deck.get('replacement_params', None)
141 LocationCard.new_location(
142 card.copy(), replacement_params, puzzle=True).export()]
143 for i, card in enumerate(deck['cards'])
145 return board_locations
148 def generate_random_board(cls, deck):
150 replacement_params = deck.get('replacement_params', None)
153 new_choice = cls.choose_card(deck['cards'], board_locations)
154 board_location = LocationCard.new_location(
155 new_choice.copy(), replacement_params)
156 board_locations.append([(x, y), board_location.export()])
157 return board_locations
159 def lose_health(self):
162 self.end_game(win=False)
164 def gain_health(self):
165 if self.health < self.max_health:
168 def acquire_win_token(self):
170 if self.wins >= self.wins_required:
171 self.end_game(win=True)
173 def card_used(self, position):
175 self.replace_card(position)
177 def replace_card(self, position):
178 new_choice = self.choose_card(self.locations,
179 self.board_locations.items(),
181 location = LocationCard.new_location(new_choice.copy(),
182 self.replacement_params)
183 self.board_locations[position] = location
186 def choose_card(cls, cards, board_locations, position=None):
187 # Find which cards are at their maximum and exclude them from
190 choices = {card['card_name']: card for card in cards}
191 for pos, card in board_locations:
193 # skip the card we're replacing if appropriate
195 if isinstance(card, LocationCard):
197 max_num = card.max_number
199 key = card['card_name']
200 max_num = card.get('max_number', 25)
201 counts.setdefault(key, 0)
203 if counts[key] >= max_num:
206 return choice(choices.values())
208 def shift_location_row(self, change, is_vertical):
209 px, py = self.player.position
210 shifted_locations = {}
211 mkpos = lambda i: (px, i) if is_vertical else (i, py)
214 if (px, py) == mkpos(i):
216 new_i = (i + change) % 5
217 if (px, py) == mkpos(new_i):
218 new_i = (new_i + change) % 5
219 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
221 self.board_locations.update(shifted_locations)
223 def shift_locations(self, direction):
224 if BITS[direction] == BITS.NORTH:
225 self.shift_location_row(-1, is_vertical=True)
226 elif BITS[direction] == BITS.SOUTH:
227 self.shift_location_row(1, is_vertical=True)
228 elif BITS[direction] == BITS.EAST:
229 self.shift_location_row(1, is_vertical=False)
230 elif BITS[direction] == BITS.WEST:
231 self.shift_location_row(-1, is_vertical=False)
233 def rotate_locations(self, direction):
234 px, py = self.player.position
235 locations_to_rotate = []
236 rotated_locations = {}
239 for i in range(max(0, px - 1), min(5, px + 2)):
240 locations_to_rotate.append((i, py - 1))
243 locations_to_rotate.append((px + 1, py))
246 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
247 locations_to_rotate.append((i, py + 1))
250 locations_to_rotate.append((px - 1, py))
252 if ROTATION[direction] == ROTATION.CLOCKWISE:
253 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
254 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
256 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
258 for old, new in zip(locations_to_rotate, new_positions):
259 rotated_locations[new] = self.board_locations[old]
261 self.board_locations.update(rotated_locations)
263 def allow_chess_move(self, chesspiece):
264 self.player.allow_chess_move(chesspiece)
266 def change_mode(self, new_mode):
267 """Advance to the next mode"""
268 if new_mode == self.player_mode:
269 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
271 elif new_mode in (ACT, EXAMINE):
272 self.player_mode = new_mode
273 if new_mode is EXAMINE:
276 raise RuntimeError("Illegal player mode %s" % self.player_mode)
278 def board_update(self):
279 self.clock_count += 1
280 for position, location in self.board_locations.iteritems():
281 location.timer_action(position, self)
283 def end_game(self, win):
284 # TODO: Find a way to not have UI stuff in game logic stuff.
285 from naja.events import SceneChangeEvent
286 from naja.scenes.lose import LoseScene
287 from naja.scenes.win import WinScene
290 SceneChangeEvent.post(WinScene)
292 SceneChangeEvent.post(LoseScene)
295 class LocationCard(object):
297 A particular set of options available on a location.
300 def __init__(self, card_name, bitwise_operand, location_actions,
301 replacement_time=None, max_number=25):
302 self.card_name = card_name
303 self.bitwise_operand = bitwise_operand
304 self.actions = location_actions
305 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 = 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=None, puzzle=False):
325 if 'bits' in definition:
326 bits = 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 location = 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,
346 location.check_actions()
351 'bitwise_operand': sorted(self.bitwise_operand),
352 'actions': [action.export() for action in self.actions],
353 'max_number': self.max_number,
354 'card_name': self.card_name,
355 'replacement_time': self.replacement_time,
358 def check_actions(self):
360 print "Warning: Location has no actions."
361 self.insert_default_default_action()
362 if self.actions[0].required_bits:
363 self.insert_default_default_action()
365 def insert_default_default_action(self):
366 self.actions.insert(0, self.build_action({
367 'action_class': 'DoNothing',
372 def generate_bitwise_operand():
374 Generate a set of two or three bits. At least one direction and one
375 condition bit will be included. There is a low probability of choosing
376 a third bit from the complete set.
379 bits.add(choice(DIRECTION_BITS.values()))
380 bits.add(choice(CONDITION_BITS.values()))
381 # One in three chance of adding a third bit, with a further one in four
382 # chance that it will match a bit already chosen.
383 if choice(range(3)) == 0:
384 bits.add(choice(BITS.values()))
385 return frozenset(bits)
388 def generate_replacement_time(replacement_params):
389 if replacement_params is None:
392 if replacement_params['chance'] > random.random():
393 return random.randint(replacement_params['min'],
394 replacement_params['max'])
398 def timer_action(self, position, board):
399 if self.replacement_time is not None:
400 self.replacement_time -= 1
401 if self.replacement_time <= 0:
402 board.replace_card(position)