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 deck_defaults = deck.get('defaults', {})
45 for k, v in deck_defaults.iteritems():
46 if isinstance(v, list):
47 deck_defaults[k] = tuple(v)
48 defaults.update(deck_defaults)
50 if initial_bits is None:
51 initial_bits = defaults['initial_bits']
52 if initial_pos is None:
53 initial_pos = defaults['initial_pos']
54 if max_health is None:
55 max_health = defaults['max_health']
56 if wins_required is None:
57 wins_required = defaults['wins_required']
59 assert wins_required + max_health == 8
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'],
71 'puzzle': deck.get('puzzle', False),
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 board = cls(state, player, board_locations)
89 player.set_gameboard(board)
94 'max_health': self.max_health,
95 'health': self.health,
96 'wins_required': self.wins_required,
98 'locations': [item.copy() for item in self.locations],
99 'puzzle': self.puzzle,
100 'player': self.player.export(),
101 'board_locations': self.export_board_locations(),
102 'player_mode': self.player_mode,
103 'clock_count': self.clock_count,
104 'replacement_params': self.replacement_params,
106 if options.cheat_enabled:
107 self.has_cheated = True
109 data['cheater'] = True
113 def import_locations(cls, locations_definition):
115 LocationCard.import_location(definition)
116 for definition in locations_definition]
118 def export_board_locations(self):
120 (position, location.export())
121 for position, location in self.board_locations.iteritems())
124 def import_board_locations(cls, board_locations_definition):
126 (tuple(position), LocationCard.import_location(definition))
127 for position, definition in board_locations_definition)
130 def generate_board(cls, deck):
131 if deck.get('puzzle', False):
132 return cls.generate_puzzle_board(deck)
134 return cls.generate_random_board(deck)
137 def generate_puzzle_board(cls, deck):
138 assert len(deck['cards']) == 5 * 5
139 replacement_params = deck.get('replacement_params', None)
142 LocationCard.new_location(
143 card.copy(), replacement_params, puzzle=True).export()]
144 for i, card in enumerate(deck['cards'])
146 return board_locations
149 def generate_random_board(cls, deck):
151 replacement_params = deck.get('replacement_params', None)
154 new_choice = cls.choose_card(deck['cards'], board_locations)
155 board_location = LocationCard.new_location(
156 new_choice.copy(), replacement_params)
157 board_locations.append([(x, y), board_location.export()])
158 return board_locations
160 def lose_health(self):
163 self.end_game(win=False)
165 def gain_health(self):
166 if self.health < self.max_health:
169 def acquire_win_token(self):
171 if self.wins >= self.wins_required:
172 self.end_game(win=True)
174 def card_used(self, position):
176 self.replace_card(position)
178 def replace_card(self, position):
179 new_choice = self.choose_card(self.locations,
180 self.board_locations.items(),
182 location = LocationCard.new_location(new_choice.copy(),
183 self.replacement_params)
184 self.board_locations[position] = location
187 def choose_card(cls, cards, board_locations, position=None):
188 # Find which cards are at their maximum and exclude them from
191 choices = dict((card['card_name'], card) for card in cards)
192 for pos, card in board_locations:
194 # skip the card we're replacing if appropriate
196 if isinstance(card, LocationCard):
198 max_num = card.max_number
200 key = card['card_name']
201 max_num = card.get('max_number', 25)
202 counts.setdefault(key, 0)
204 if counts[key] >= max_num:
207 return choice(choices.values())
209 def shift_location_row(self, change, is_vertical):
210 px, py = self.player.position
211 shifted_locations = {}
212 mkpos = lambda i: (px, i) if is_vertical else (i, py)
215 if (px, py) == mkpos(i):
217 new_i = (i + change) % 5
218 if (px, py) == mkpos(new_i):
219 new_i = (new_i + change) % 5
220 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
222 self.board_locations.update(shifted_locations)
224 def shift_locations(self, direction):
225 if BITS[direction] == BITS.NORTH:
226 self.shift_location_row(-1, is_vertical=True)
227 elif BITS[direction] == BITS.SOUTH:
228 self.shift_location_row(1, is_vertical=True)
229 elif BITS[direction] == BITS.EAST:
230 self.shift_location_row(1, is_vertical=False)
231 elif BITS[direction] == BITS.WEST:
232 self.shift_location_row(-1, is_vertical=False)
234 def rotate_locations(self, direction):
235 px, py = self.player.position
236 locations_to_rotate = []
237 rotated_locations = {}
240 for i in range(max(0, px - 1), min(5, px + 2)):
241 locations_to_rotate.append((i, py - 1))
244 locations_to_rotate.append((px + 1, py))
247 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
248 locations_to_rotate.append((i, py + 1))
251 locations_to_rotate.append((px - 1, py))
253 if ROTATION[direction] == ROTATION.CLOCKWISE:
254 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
255 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
257 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
259 for old, new in zip(locations_to_rotate, new_positions):
260 rotated_locations[new] = self.board_locations[old]
262 self.board_locations.update(rotated_locations)
264 def allow_chess_move(self, chesspiece):
265 self.player.allow_chess_move(chesspiece)
267 def change_mode(self, new_mode):
268 """Advance to the next mode"""
269 if new_mode == self.player_mode:
270 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
272 elif new_mode in (ACT, EXAMINE):
273 self.player_mode = new_mode
274 if new_mode is EXAMINE:
277 raise RuntimeError("Illegal player mode %s" % self.player_mode)
279 def board_update(self):
280 self.clock_count += 1
281 for position, location in self.board_locations.iteritems():
282 location.timer_action(position, self)
284 def end_game(self, win):
285 # TODO: Find a way to not have UI stuff in game logic stuff.
286 from naja.events import SceneChangeEvent
287 from naja.scenes.lose import LoseScene
288 from naja.scenes.win import WinScene
291 SceneChangeEvent.post(WinScene)
293 SceneChangeEvent.post(LoseScene)
296 class LocationCard(object):
298 A particular set of options available on a location.
301 def __init__(self, card_name, bitwise_operand, location_actions,
302 replacement_time=None, max_number=25):
303 self.card_name = card_name
304 self.bitwise_operand = bitwise_operand
305 self.actions = location_actions
306 self.max_number = max_number
307 self.replacement_time = replacement_time
309 for action in self.actions:
310 action.sanity_check(self)
313 def import_location(cls, state):
315 cls.build_action(definition) for definition in state['actions']]
316 return cls(state['card_name'], state['bitwise_operand'],
317 location_actions, state['replacement_time'],
321 def build_action(cls, definition):
322 action_class = getattr(actions, definition['action_class'])
323 required_bits = parse_bits(definition['required_bits'])
324 data = definition.get('data', {})
325 return action_class(required_bits, **data)
328 def new_location(cls, definition, replacement_params=None, puzzle=False):
329 if 'bits' in definition:
330 bits = parse_bits(definition['bits'])
332 bits = cls.generate_bitwise_operand()
334 if 'replacement_time' in definition:
335 replacement_time = definition['replacement_time']
337 replacement_time = cls.generate_replacement_time(
340 max_number = definition.get('max_number', 25)
341 card_name = definition['card_name']
342 location = cls.import_location({
343 'bitwise_operand': bits,
344 'actions': definition['actions'],
345 'max_number': max_number,
346 'card_name': card_name,
347 'replacement_time': replacement_time,
350 location.check_actions()
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)