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 board = cls(state, player, board_locations)
90 player.set_gameboard(board)
95 'max_health': self.max_health,
96 'health': self.health,
97 'wins_required': self.wins_required,
99 'locations': [item.copy() for item in self.locations],
100 'puzzle': self.puzzle,
101 'player': self.player.export(),
102 'board_locations': self.export_board_locations(),
103 'player_mode': self.player_mode,
104 'clock_count': self.clock_count,
105 'replacement_params': self.replacement_params,
107 if options.cheat_enabled:
108 self.has_cheated = True
110 data['cheater'] = True
114 def import_locations(cls, locations_definition):
116 LocationCard.import_location(definition)
117 for definition in locations_definition]
119 def export_board_locations(self):
121 (position, location.export())
122 for position, location in self.board_locations.iteritems())
125 def import_board_locations(cls, board_locations_definition):
127 (tuple(position), LocationCard.import_location(definition))
128 for position, definition in board_locations_definition)
131 def generate_board(cls, deck):
132 if deck.get('puzzle', False):
133 return cls.generate_puzzle_board(deck)
135 return cls.generate_random_board(deck)
138 def generate_puzzle_board(cls, deck):
139 assert len(deck['cards']) == 5 * 5
140 replacement_params = deck.get('replacement_params', None)
143 LocationCard.new_location(
144 card.copy(), replacement_params, puzzle=True).export()]
145 for i, card in enumerate(deck['cards'])
147 return board_locations
150 def generate_random_board(cls, deck):
152 replacement_params = deck.get('replacement_params', None)
155 new_choice = cls.choose_card(deck['cards'], board_locations)
156 board_location = LocationCard.new_location(
157 new_choice.copy(), replacement_params)
158 board_locations.append([(x, y), board_location.export()])
159 return board_locations
161 def lose_health(self):
164 self.end_game(win=False)
166 def gain_health(self):
167 if self.health < self.max_health:
170 def acquire_win_token(self):
172 if self.wins >= self.wins_required:
173 self.end_game(win=True)
175 def card_used(self, position):
177 self.replace_card(position)
179 def replace_card(self, position):
180 new_choice = self.choose_card(self.locations,
181 self.board_locations.items(),
183 location = LocationCard.new_location(new_choice.copy(),
184 self.replacement_params)
185 self.board_locations[position] = location
188 def choose_card(cls, cards, board_locations, position=None):
189 # Find which cards are at their maximum and exclude them from
192 choices = {card['card_name']: card for card in cards}
193 for pos, card in board_locations:
195 # skip the card we're replacing if appropriate
197 if isinstance(card, LocationCard):
199 max_num = card.max_number
201 key = card['card_name']
202 max_num = card.get('max_number', 25)
203 counts.setdefault(key, 0)
205 if counts[key] >= max_num:
208 return choice(choices.values())
210 def shift_location_row(self, change, is_vertical):
211 px, py = self.player.position
212 shifted_locations = {}
213 mkpos = lambda i: (px, i) if is_vertical else (i, py)
216 if (px, py) == mkpos(i):
218 new_i = (i + change) % 5
219 if (px, py) == mkpos(new_i):
220 new_i = (new_i + change) % 5
221 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
223 self.board_locations.update(shifted_locations)
225 def shift_locations(self, direction):
226 if BITS[direction] == BITS.NORTH:
227 self.shift_location_row(-1, is_vertical=True)
228 elif BITS[direction] == BITS.SOUTH:
229 self.shift_location_row(1, is_vertical=True)
230 elif BITS[direction] == BITS.EAST:
231 self.shift_location_row(1, is_vertical=False)
232 elif BITS[direction] == BITS.WEST:
233 self.shift_location_row(-1, is_vertical=False)
235 def rotate_locations(self, direction):
236 px, py = self.player.position
237 locations_to_rotate = []
238 rotated_locations = {}
241 for i in range(max(0, px - 1), min(5, px + 2)):
242 locations_to_rotate.append((i, py - 1))
245 locations_to_rotate.append((px + 1, py))
248 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
249 locations_to_rotate.append((i, py + 1))
252 locations_to_rotate.append((px - 1, py))
254 if ROTATION[direction] == ROTATION.CLOCKWISE:
255 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
256 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
258 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
260 for old, new in zip(locations_to_rotate, new_positions):
261 rotated_locations[new] = self.board_locations[old]
263 self.board_locations.update(rotated_locations)
265 def allow_chess_move(self, chesspiece):
266 self.player.allow_chess_move(chesspiece)
268 def change_mode(self, new_mode):
269 """Advance to the next mode"""
270 if new_mode == self.player_mode:
271 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
273 elif new_mode in (ACT, EXAMINE):
274 self.player_mode = new_mode
275 if new_mode is EXAMINE:
278 raise RuntimeError("Illegal player mode %s" % self.player_mode)
280 def board_update(self):
281 self.clock_count += 1
282 for position, location in self.board_locations.iteritems():
283 location.timer_action(position, self)
285 def end_game(self, win):
286 # TODO: Find a way to not have UI stuff in game logic stuff.
287 from naja.events import SceneChangeEvent
288 from naja.scenes.lose import LoseScene
289 from naja.scenes.win import WinScene
292 SceneChangeEvent.post(WinScene)
294 SceneChangeEvent.post(LoseScene)
297 class LocationCard(object):
299 A particular set of options available on a location.
302 def __init__(self, card_name, bitwise_operand, location_actions,
303 replacement_time=None, max_number=25):
304 self.card_name = card_name
305 self.bitwise_operand = bitwise_operand
306 self.actions = location_actions
307 self.max_number = max_number
308 self.replacement_time = replacement_time
311 def import_location(cls, state):
313 cls.build_action(definition) for definition in state['actions']]
314 return cls(state['card_name'], state['bitwise_operand'],
315 location_actions, state['replacement_time'],
319 def build_action(cls, definition):
320 action_class = getattr(actions, definition['action_class'])
321 required_bits = parse_bits(definition['required_bits'])
322 data = definition.get('data', {})
323 return action_class(required_bits, **data)
326 def new_location(cls, definition, replacement_params=None, puzzle=False):
327 if 'bits' in definition:
328 bits = parse_bits(definition['bits'])
330 bits = cls.generate_bitwise_operand()
332 if 'replacement_time' in definition:
333 replacement_time = definition['replacement_time']
335 replacement_time = cls.generate_replacement_time(
338 max_number = definition.get('max_number', 25)
339 card_name = definition['card_name']
340 location = cls.import_location({
341 'bitwise_operand': bits,
342 'actions': definition['actions'],
343 'max_number': max_number,
344 'card_name': card_name,
345 'replacement_time': replacement_time,
348 location.check_actions()
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)