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
12 class GameBoard(object):
14 A representation of the game board.
17 def __init__(self, state, player, board_locations):
18 self.max_health = state['max_health']
19 self.wins_required = state['wins_required']
20 self.health = state['health']
21 self.wins = state['wins']
22 self.locations = [item.copy() for item in state['locations']]
23 self.puzzle = state.get('puzzle', False)
25 self.board_locations = board_locations
26 self.player_mode = state.get('player_mode', EXAMINE)
27 self.has_cheated = state.get('cheater', options.cheat_enabled)
30 def new_game(cls, deck,
31 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
32 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
33 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
34 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
35 if options.initial_bits:
36 initial_bits = options.initial_bits
38 'max_health': max_health,
40 'wins_required': wins_required,
42 'locations': deck['cards'],
43 'puzzle': deck.get('puzzle', False),
45 player = Player(initial_bits, initial_pos)
46 board_locations = cls.import_board_locations(
47 cls.generate_board(deck))
48 return cls(state, player, board_locations)
51 def import_game(cls, definition):
52 state = definition.copy()
53 player = Player.import_player(state.pop('player'))
54 board_locations = cls.import_board_locations(
55 state.pop('board_locations'))
56 return cls(state, player, board_locations)
60 'max_health': self.max_health,
61 'health': self.health,
62 'wins_required': self.wins_required,
64 'locations': [item.copy() for item in self.locations],
65 'puzzle': self.puzzle,
66 'player': self.player.export(),
67 'board_locations': self.export_board_locations(),
68 'player_mode': self.player_mode,
71 data['cheater'] = True
75 def import_locations(cls, locations_definition):
77 LocationCard.import_location(definition)
78 for definition in locations_definition]
80 def export_board_locations(self):
82 (position, location.export())
83 for position, location in self.board_locations.iteritems())
86 def import_board_locations(cls, board_locations_definition):
88 (tuple(position), LocationCard.import_location(definition))
89 for position, definition in board_locations_definition)
92 def generate_board(cls, deck):
93 if deck.get('puzzle', False):
94 return cls.generate_puzzle_board(deck)
96 return cls.generate_random_board(deck)
99 def generate_puzzle_board(cls, deck):
100 assert len(deck['cards']) == 5 * 5
103 LocationCard.new_location(card.copy()).export()]
104 for i, card in enumerate(deck['cards'])
106 return board_locations
109 def generate_random_board(cls, deck):
113 board_location = LocationCard.new_location(
114 choice(deck['cards']).copy())
115 board_locations.append([(x, y), board_location.export()])
116 return board_locations
118 def lose_health(self):
121 self.end_game(win=False)
123 def gain_health(self):
124 if self.health < self.max_health:
127 def acquire_win_token(self):
129 if self.wins >= self.wins_required:
130 self.end_game(win=True)
132 def card_used(self, position):
134 self.replace_card(position)
136 def replace_card(self, position):
137 location = LocationCard.new_location(choice(self.locations).copy())
138 self.board_locations[position] = location
140 def shift_location_row(self, change, is_vertical):
141 px, py = self.player.position
142 shifted_locations = {}
143 mkpos = lambda i: (px, i) if is_vertical else (i, py)
146 if (px, py) == mkpos(i):
148 new_i = (i + change) % 5
149 if (px, py) == mkpos(new_i):
150 new_i = (new_i + change) % 5
151 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
153 self.board_locations.update(shifted_locations)
155 def shift_locations(self, direction):
156 if BITS[direction] == BITS.NORTH:
157 self.shift_location_row(-1, is_vertical=True)
158 elif BITS[direction] == BITS.SOUTH:
159 self.shift_location_row(1, is_vertical=True)
160 elif BITS[direction] == BITS.EAST:
161 self.shift_location_row(1, is_vertical=False)
162 elif BITS[direction] == BITS.WEST:
163 self.shift_location_row(-1, is_vertical=False)
165 def rotate_locations(self, direction):
166 px, py = self.player.position
167 locations_to_rotate = []
168 rotated_locations = {}
171 for i in range(max(0, px - 1), min(5, px + 2)):
172 locations_to_rotate.append((i, py - 1))
175 locations_to_rotate.append((px + 1, py))
178 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
179 locations_to_rotate.append((i, py + 1))
182 locations_to_rotate.append((px - 1, py))
184 if ROTATION[direction] == ROTATION.CLOCKWISE:
185 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
186 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
187 new_positions = ([locations_to_rotate[-1]] + locations_to_rotate[:-1])
189 for old, new in zip(locations_to_rotate, new_positions):
190 rotated_locations[new] = self.board_locations[old]
192 self.board_locations.update(rotated_locations)
194 def allow_chess_move(self, chesspiece):
195 self.player.allow_chess_move(chesspiece)
197 def change_mode(self, new_mode):
198 """Advance to the next mode"""
199 if new_mode == self.player_mode:
200 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
202 elif new_mode in (ACT, EXAMINE):
203 self.player_mode = new_mode
205 raise RuntimeError("Illegal player mode %s" % self.player_mode)
207 def end_game(self, win):
208 # TODO: Find a way to not have UI stuff in game logic stuff.
209 from naja.events import SceneChangeEvent
210 from naja.scenes.lose import LoseScene
211 from naja.scenes.win import WinScene
214 SceneChangeEvent.post(WinScene)
216 SceneChangeEvent.post(LoseScene)
219 class LocationCard(object):
221 A particular set of options available on a location.
224 def __init__(self, card_name, bitwise_operand, location_actions):
225 self.card_name = card_name
226 self.bitwise_operand = bitwise_operand
227 self.actions = location_actions
231 def import_location(cls, state):
233 cls.build_action(definition) for definition in state['actions']]
234 return cls(state['card_name'], state['bitwise_operand'],
238 def build_action(cls, definition):
239 action_class = getattr(actions, definition['action_class'])
240 required_bits = cls.parse_bits(definition['required_bits'])
241 data = definition.get('data', {})
242 return action_class(required_bits, **data)
245 def new_location(cls, definition):
246 if 'bits' in definition:
247 bits = cls.parse_bits(definition['bits'])
249 bits = cls.generate_bitwise_operand()
250 card_name = definition['card_name']
251 return cls.import_location({
252 'bitwise_operand': bits,
253 'actions': definition['actions'],
254 'card_name': card_name,
258 def parse_bits(self, bit_list):
259 # Convert names to numbers if applicable.
260 return frozenset(BITS.get(bit, bit) for bit in bit_list)
264 'bitwise_operand': sorted(self.bitwise_operand),
265 'actions': [action.export() for action in self.actions],
266 'card_name': self.card_name,
269 def check_actions(self):
271 print "Warning: Location has no actions."
272 self.insert_default_default_action()
273 if self.actions[0].required_bits:
274 self.insert_default_default_action()
276 def insert_default_default_action(self):
277 self.actions.insert(0, self.build_action({
278 'action_class': 'DoNothing',
283 def generate_bitwise_operand():
285 Generate a set of two or three bits. At least one direction and one
286 condition bit will be included. There is a low probability of choosing
287 a third bit from the complete set.
290 bits.add(choice(DIRECTION_BITS.values()))
291 bits.add(choice(CONDITION_BITS.values()))
292 # One in three chance of adding a third bit, with a further one in four
293 # chance that it will match a bit already chosen.
294 if choice(range(3)) == 0:
295 bits.add(choice(BITS.values()))
296 return frozenset(bits)