1 from random import choice
3 from naja.constants import(
4 BITS, DIRECTION_BITS, CONDITION_BITS, PLAYER_DEFAULTS,
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.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 deck_defaults = deck.get('defaults', {})
44 for k, v in deck_defaults.iteritems():
45 if isinstance(v, list):
46 deck_defaults[k] = tuple(v)
47 defaults.update(deck_defaults)
49 if initial_bits is None:
50 initial_bits = defaults['initial_bits']
51 if initial_pos is None:
52 initial_pos = defaults['initial_pos']
53 if max_health is None:
54 max_health = defaults['max_health']
55 if wins_required is None:
56 wins_required = defaults['wins_required']
58 assert wins_required + max_health == 8
60 # Overriden by command line
61 if options.initial_bits:
62 initial_bits = options.initial_bits
65 'max_health': max_health,
67 'wins_required': wins_required,
69 'locations': deck['cards'],
70 'puzzle': deck.get('puzzle', False),
72 'replacement_params': deck.get('replacement_params', None),
74 player = Player(initial_bits, initial_pos)
75 board_locations = cls.import_board_locations(
76 cls.generate_board(deck))
77 board = cls(state, player, board_locations)
78 player.set_gameboard(board)
82 def import_game(cls, definition):
83 state = definition.copy()
84 player = Player.import_player(state.pop('player'))
85 board_locations = cls.import_board_locations(
86 state.pop('board_locations'))
87 board = cls(state, player, board_locations)
88 player.set_gameboard(board)
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 '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 = dict((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, skip_player=True):
208 px, py = self.player.position
209 shifted_locations = {}
210 mkpos = lambda i: (px, i) if is_vertical else (i, py)
213 if skip_player and (px, py) == mkpos(i):
215 new_i = (i + change) % 5
216 if skip_player and (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, skip_player=True):
223 if BITS[direction] == BITS.NORTH:
224 self.shift_location_row(-1, is_vertical=True,
225 skip_player=skip_player)
226 elif BITS[direction] == BITS.SOUTH:
227 self.shift_location_row(1, is_vertical=True,
228 skip_player=skip_player)
229 elif BITS[direction] == BITS.EAST:
230 self.shift_location_row(1, is_vertical=False,
231 skip_player=skip_player)
232 elif BITS[direction] == BITS.WEST:
233 self.shift_location_row(-1, is_vertical=False,
234 skip_player=skip_player)
236 def rotate_locations(self, direction):
237 px, py = self.player.position
238 locations_to_rotate = []
239 rotated_locations = {}
242 for i in range(max(0, px - 1), min(5, px + 2)):
243 locations_to_rotate.append((i, py - 1))
246 locations_to_rotate.append((px + 1, py))
249 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
250 locations_to_rotate.append((i, py + 1))
253 locations_to_rotate.append((px - 1, py))
255 if ROTATION[direction] == ROTATION.CLOCKWISE:
256 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
257 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
259 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
261 for old, new in zip(locations_to_rotate, new_positions):
262 rotated_locations[new] = self.board_locations[old]
264 self.board_locations.update(rotated_locations)
266 def allow_chess_move(self, chesspiece):
267 self.player.allow_chess_move(chesspiece)
269 def board_update(self):
270 self.clock_count += 1
271 for position, location in self.board_locations.iteritems():
272 location.timer_action(position, self)
274 def end_game(self, win):
275 # TODO: Find a way to not have UI stuff in game logic stuff.
276 from naja.events import SceneChangeEvent
277 from naja.scenes.lose import LoseScene
278 from naja.scenes.win import WinScene
281 SceneChangeEvent.post(WinScene)
283 SceneChangeEvent.post(LoseScene)
286 class LocationCard(object):
288 A particular set of options available on a location.
291 def __init__(self, card_name, bitwise_operand, location_actions,
292 replacement_time=None, max_number=25):
293 self.card_name = card_name
294 self.bitwise_operand = bitwise_operand
295 self.actions = location_actions
296 self.max_number = max_number
297 self.replacement_time = replacement_time
299 for action in self.actions:
300 action.sanity_check(self)
303 def import_location(cls, state):
305 cls.build_action(definition) for definition in state['actions']]
306 return cls(state['card_name'], state['bitwise_operand'],
307 location_actions, state['replacement_time'],
311 def build_action(cls, definition):
312 action_class = getattr(actions, definition['action_class'])
313 required_bits = parse_bits(definition['required_bits'])
314 data = definition.get('data', {})
315 return action_class(required_bits, **data)
318 def new_location(cls, definition, replacement_params=None, puzzle=False):
319 if 'bits' in definition:
320 bits = parse_bits(definition['bits'])
322 bits = cls.generate_bitwise_operand()
324 if 'replacement_time' in definition:
325 replacement_time = definition['replacement_time']
327 replacement_time = cls.generate_replacement_time(
330 max_number = definition.get('max_number', 25)
331 card_name = definition['card_name']
332 location = cls.import_location({
333 'bitwise_operand': bits,
334 'actions': definition['actions'],
335 'max_number': max_number,
336 'card_name': card_name,
337 'replacement_time': replacement_time,
340 location.check_actions()
345 'bitwise_operand': sorted(self.bitwise_operand),
346 'actions': [action.export() for action in self.actions],
347 'max_number': self.max_number,
348 'card_name': self.card_name,
349 'replacement_time': self.replacement_time,
352 def check_actions(self):
354 print "Warning: Location has no actions."
355 self.insert_default_default_action()
356 if self.actions[0].required_bits:
357 self.insert_default_default_action()
359 def insert_default_default_action(self):
360 self.actions.insert(0, self.build_action({
361 'action_class': 'DoNothing',
366 def generate_bitwise_operand():
368 Generate a set of two or three bits. At least one direction and one
369 condition bit will be included. There is a low probability of choosing
370 a third bit from the complete set.
373 bits.add(choice(DIRECTION_BITS.values()))
374 bits.add(choice(CONDITION_BITS.values()))
375 # One in three chance of adding a third bit, with a further one in four
376 # chance that it will match a bit already chosen.
377 if choice(range(3)) == 0:
378 bits.add(choice(BITS.values()))
379 return frozenset(bits)
382 def generate_replacement_time(replacement_params):
383 if replacement_params is None:
386 if replacement_params['chance'] > random.random():
387 return random.randint(replacement_params['min'],
388 replacement_params['max'])
392 def timer_action(self, position, board):
393 if self.replacement_time is not None:
394 self.replacement_time -= 1
395 if self.replacement_time <= 0:
396 board.replace_card(position)