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:
188 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
190 for old, new in zip(locations_to_rotate, new_positions):
191 rotated_locations[old] = self.board_locations[new]
193 self.board_locations.update(rotated_locations)
195 def allow_chess_move(self, chesspiece):
196 self.player.allow_chess_move(chesspiece)
198 def change_mode(self, new_mode):
199 """Advance to the next mode"""
200 if new_mode == self.player_mode:
201 raise RuntimeError("Inconsistent state. Setting mode %s to itself"
203 elif new_mode in (ACT, EXAMINE):
204 self.player_mode = new_mode
206 raise RuntimeError("Illegal player mode %s" % self.player_mode)
208 def end_game(self, win):
209 # TODO: Find a way to not have UI stuff in game logic stuff.
210 from naja.events import SceneChangeEvent
211 from naja.scenes.lose import LoseScene
212 from naja.scenes.win import WinScene
215 SceneChangeEvent.post(WinScene)
217 SceneChangeEvent.post(LoseScene)
220 class LocationCard(object):
222 A particular set of options available on a location.
225 def __init__(self, bitwise_operand, location_actions):
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['bitwise_operand'], location_actions)
237 def build_action(cls, definition):
238 action_class = getattr(actions, definition['action_class'])
239 required_bits = cls.parse_bits(definition['required_bits'])
240 data = definition.get('data', {})
241 return action_class(required_bits, **data)
244 def new_location(cls, definition):
245 if 'bits' in definition:
246 bits = cls.parse_bits(definition['bits'])
248 bits = cls.generate_bitwise_operand()
249 return cls.import_location({
250 'bitwise_operand': bits,
251 'actions': definition['actions'],
255 def parse_bits(self, bit_list):
256 # Convert names to numbers if applicable.
257 return frozenset(BITS.get(bit, bit) for bit in bit_list)
261 'bitwise_operand': sorted(self.bitwise_operand),
262 'actions': [action.export() for action in self.actions],
265 def check_actions(self):
267 print "Warning: Location has no actions."
268 self.insert_default_default_action()
269 if self.actions[0].required_bits:
270 self.insert_default_default_action()
272 def insert_default_default_action(self):
273 self.actions.insert(0, self.build_action({
274 'action_class': 'DoNothing',
279 def generate_bitwise_operand():
281 Generate a set of two or three bits. At least one direction and one
282 condition bit will be included. There is a low probability of choosing
283 a third bit from the complete set.
286 bits.add(choice(DIRECTION_BITS.values()))
287 bits.add(choice(CONDITION_BITS.values()))
288 # One in three chance of adding a third bit, with a further one in four
289 # chance that it will match a bit already chosen.
290 if choice(range(3)) == 0:
291 bits.add(choice(BITS.values()))
292 return frozenset(bits)