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, bitwise_operand, location_actions):
225 self.bitwise_operand = bitwise_operand
226 self.actions = location_actions
230 def import_location(cls, state):
232 cls.build_action(definition) for definition in state['actions']]
233 return cls(state['bitwise_operand'], location_actions)
236 def build_action(cls, definition):
237 action_class = getattr(actions, definition['action_class'])
238 required_bits = cls.parse_bits(definition['required_bits'])
239 data = definition.get('data', {})
240 return action_class(required_bits, **data)
243 def new_location(cls, definition):
244 if 'bits' in definition:
245 bits = cls.parse_bits(definition['bits'])
247 bits = cls.generate_bitwise_operand()
248 return cls.import_location({
249 'bitwise_operand': bits,
250 'actions': definition['actions'],
254 def parse_bits(self, bit_list):
255 # Convert names to numbers if applicable.
256 return frozenset(BITS.get(bit, bit) for bit in bit_list)
260 'bitwise_operand': sorted(self.bitwise_operand),
261 'actions': [action.export() for action in self.actions],
264 def check_actions(self):
266 print "Warning: Location has no actions."
267 self.insert_default_default_action()
268 if self.actions[0].required_bits:
269 self.insert_default_default_action()
271 def insert_default_default_action(self):
272 self.actions.insert(0, self.build_action({
273 'action_class': 'DoNothing',
278 def generate_bitwise_operand():
280 Generate a set of two or three bits. At least one direction and one
281 condition bit will be included. There is a low probability of choosing
282 a third bit from the complete set.
285 bits.add(choice(DIRECTION_BITS.values()))
286 bits.add(choice(CONDITION_BITS.values()))
287 # One in three chance of adding a third bit, with a further one in four
288 # chance that it will match a bit already chosen.
289 if choice(range(3)) == 0:
290 bits.add(choice(BITS.values()))
291 return frozenset(bits)