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
11 class GameBoard(object):
13 A representation of the game board.
16 def __init__(self, state, player, board_locations):
17 self.max_health = state['max_health']
18 self.wins_required = state['wins_required']
19 self.health = state['health']
20 self.wins = state['wins']
21 self.locations = [item.copy() for item in state['locations']]
22 self.puzzle = state.get('puzzle', False)
24 self.board_locations = board_locations
25 self.player_mode = state.get('player_mode', EXAMINE)
26 self.has_cheated = state.get('cheater', options.cheat_enabled)
29 def new_game(cls, deck,
30 initial_bits=PLAYER_DEFAULTS.INITIAL_BITS,
31 initial_pos=PLAYER_DEFAULTS.INITIAL_POS,
32 max_health=PLAYER_DEFAULTS.MAX_HEALTH,
33 wins_required=PLAYER_DEFAULTS.WINS_REQUIRED):
34 if options.initial_bits:
35 initial_bits = options.initial_bits
37 'max_health': max_health,
39 'wins_required': wins_required,
41 'locations': deck['cards'],
42 'puzzle': deck.get('puzzle', False),
44 player = Player(initial_bits, initial_pos)
45 board_locations = cls.import_board_locations(
46 cls.generate_board(deck))
47 return cls(state, player, board_locations)
50 def import_game(cls, definition):
51 state = definition.copy()
52 player = Player.import_player(state.pop('player'))
53 board_locations = cls.import_board_locations(
54 state.pop('board_locations'))
55 return cls(state, player, board_locations)
59 'max_health': self.max_health,
60 'health': self.health,
61 'wins_required': self.wins_required,
63 'locations': [item.copy() for item in self.locations],
64 'puzzle': self.puzzle,
65 'player': self.player.export(),
66 'board_locations': self.export_board_locations(),
67 'player_mode': self.player_mode,
70 data['cheater'] = True
74 def import_locations(cls, locations_definition):
76 LocationCard.import_location(definition)
77 for definition in locations_definition]
79 def export_board_locations(self):
81 (position, location.export())
82 for position, location in self.board_locations.iteritems())
85 def import_board_locations(cls, board_locations_definition):
87 (tuple(position), LocationCard.import_location(definition))
88 for position, definition in board_locations_definition)
91 def generate_board(cls, deck):
92 if deck.get('puzzle', False):
93 return cls.generate_puzzle_board(deck)
95 return cls.generate_random_board(deck)
98 def generate_puzzle_board(cls, deck):
99 assert len(deck['cards']) == 5 * 5
102 LocationCard.new_location(card.copy()).export()]
103 for i, card in enumerate(deck['cards'])
105 return board_locations
108 def generate_random_board(cls, deck):
112 board_location = LocationCard.new_location(
113 choice(deck['cards']).copy())
114 board_locations.append([(x, y), board_location.export()])
115 return board_locations
117 def lose_health(self):
120 self.end_game(win=False)
122 def gain_health(self):
123 if self.health < self.max_health:
126 def acquire_win_token(self):
128 if self.wins >= self.wins_required:
129 self.end_game(win=True)
131 def card_used(self, position):
133 self.replace_card(position)
135 def replace_card(self, position):
136 location = LocationCard.new_location(choice(self.locations).copy())
137 self.board_locations[position] = location
139 def shift_location_row(self, change, is_vertical):
140 px, py = self.player.position
141 shifted_locations = {}
142 mkpos = lambda i: (px, i) if is_vertical else (i, py)
145 if (px, py) == mkpos(i):
147 new_i = (i + change) % 5
148 if (px, py) == mkpos(new_i):
149 new_i = (new_i + change) % 5
150 shifted_locations[mkpos(new_i)] = self.board_locations[mkpos(i)]
152 self.board_locations.update(shifted_locations)
154 def shift_locations(self, direction):
155 if BITS[direction] == BITS.NORTH:
156 self.shift_location_row(-1, is_vertical=True)
157 elif BITS[direction] == BITS.SOUTH:
158 self.shift_location_row(1, is_vertical=True)
159 elif BITS[direction] == BITS.EAST:
160 self.shift_location_row(1, is_vertical=False)
161 elif BITS[direction] == BITS.WEST:
162 self.shift_location_row(-1, is_vertical=False)
164 def rotate_locations(self, direction):
165 px, py = self.player.position
166 locations_to_rotate = []
167 rotated_locations = {}
170 for i in range(max(0, px - 1), min(5, px + 2)):
171 locations_to_rotate.append((i, py - 1))
174 locations_to_rotate.append((px + 1, py))
177 for i in reversed(range(max(0, px - 1), min(5, px + 2))):
178 locations_to_rotate.append((i, py + 1))
181 locations_to_rotate.append((px - 1, py))
183 if ROTATION[direction] == ROTATION.CLOCKWISE:
184 new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]]
185 elif ROTATION[direction] == ROTATION.ANTICLOCKWISE:
187 [locations_to_rotate[-1]] + locations_to_rotate[:-1])
189 for old, new in zip(locations_to_rotate, new_positions):
190 rotated_locations[old] = self.board_locations[new]
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
213 SceneChangeEvent.post(WinScene)
215 SceneChangeEvent.post(LoseScene)
218 class LocationCard(object):
220 A particular set of options available on a location.
223 def __init__(self, bitwise_operand, location_actions):
224 self.bitwise_operand = bitwise_operand
225 self.actions = location_actions
229 def import_location(cls, state):
231 cls.build_action(definition) for definition in state['actions']]
232 return cls(state['bitwise_operand'], location_actions)
235 def build_action(cls, definition):
236 action_class = getattr(actions, definition['action_class'])
237 required_bits = cls.parse_bits(definition['required_bits'])
238 data = definition.get('data', {})
239 return action_class(required_bits, **data)
242 def new_location(cls, definition):
243 if 'bits' in definition:
244 bits = cls.parse_bits(definition['bits'])
246 bits = cls.generate_bitwise_operand()
247 return cls.import_location({
248 'bitwise_operand': bits,
249 'actions': definition['actions'],
253 def parse_bits(self, bit_list):
254 # Convert names to numbers if applicable.
255 return frozenset(BITS.get(bit, bit) for bit in bit_list)
259 'bitwise_operand': sorted(self.bitwise_operand),
260 'actions': [action.export() for action in self.actions],
263 def check_actions(self):
265 print "Warning: Location has no actions."
266 self.insert_default_default_action()
267 if self.actions[0].required_bits:
268 self.insert_default_default_action()
270 def insert_default_default_action(self):
271 self.actions.insert(0, self.build_action({
272 'action_class': 'DoNothing',
277 def generate_bitwise_operand():
279 Generate a set of two or three bits. At least one direction and one
280 condition bit will be included. There is a low probability of choosing
281 a third bit from the complete set.
284 bits.add(choice(DIRECTION_BITS.values()))
285 bits.add(choice(CONDITION_BITS.values()))
286 # One in three chance of adding a third bit, with a further one in four
287 # chance that it will match a bit already chosen.
288 if choice(range(3)) == 0:
289 bits.add(choice(BITS.values()))
290 return frozenset(bits)