Add max_number limitation (with tests, nogal)
[naja.git] / naja / tests / test_gameboard.py
1 from unittest import TestCase
2
3 from naja.constants import BITS, MOVES, EXAMINE
4 from naja.gameboard import GameBoard, LocationCard
5 from naja.options import parse_args
6 from naja import actions
7
8
9 class TestGameBoard(TestCase):
10     def setUp(self):
11         parse_args([])
12
13     def make_deck(self, cards=None):
14         if cards is None:
15             cards = [{'card_name': 'card', 'actions': []}]
16         return {'cards': cards}
17
18     def assert_state(self, state1, state2, exclude=(), player_exclude=()):
19         def filter_dict(source, exclude_keys):
20             return dict((k, v) for k, v in source.items()
21                         if k not in exclude_keys)
22
23         state1 = filter_dict(state1, exclude)
24         if 'player' in state1:
25             state1['player'] = filter_dict(state1['player'], player_exclude)
26         state2 = filter_dict(state2, exclude)
27         if 'player' in state2:
28             state2['player'] = filter_dict(state2['player'], player_exclude)
29
30         self.assertEqual(state1, state2)
31
32     def test_export_new_board(self):
33         board = GameBoard.new_game({'cards': [
34             {'card_name': 'card1', 'actions': [
35             {
36                 'action_class': 'LoseHealthOrMSB',
37                 'required_bits': [],
38             }, {
39                 'action_class': 'GainHealth',
40                 'required_bits': [BITS.RED],
41             },
42         ]}]})
43         exported_state = board.export()
44         board_locations = exported_state.pop('board_locations')
45         self.assertEqual(exported_state, {
46             'max_health': 4,
47             'health': 4,
48             'wins_required': 4,
49             'wins': 0,
50             'locations': [{'card_name': 'card1', 'actions': [
51                 {
52                     'action_class': 'LoseHealthOrMSB',
53                     'required_bits': [],
54                 }, {
55                     'action_class': 'GainHealth',
56                     'required_bits': [BITS.RED],
57                 },
58             ]}],
59             'player': board.player.export(),
60             'puzzle': False,
61             'player_mode': EXAMINE,
62         })
63         positions = []
64         for position, location_state in board_locations:
65             positions.append(position)
66             self.assertEqual(
67                 sorted(location_state.keys()), ['actions', 'bitwise_operand',
68                                                 'card_name', 'max_number'])
69             self.assertEqual(location_state['actions'], [
70                 {
71                     'action_class': 'LoseHealthOrMSB',
72                     'required_bits': [],
73                     'data': {},
74                 }, {
75                     'action_class': 'GainHealth',
76                     'required_bits': [BITS.RED],
77                     'data': {},
78                 },
79             ])
80             self.assertTrue(2 <= len(location_state['bitwise_operand']) <= 3)
81         self.assertEqual(
82             positions, sorted((x, y) for x in range(5) for y in range(5)))
83
84     def test_max_number(self):
85         def _check_counts(board):
86             counts = {}
87             exported_state = board.export()
88             board_locations = exported_state.pop('board_locations')
89             for _position, location_state in board_locations:
90                 counts.setdefault(
91                     location_state['actions'][0]['action_class'], 0)
92                 counts[location_state['actions'][0]['action_class']] += 1
93             self.assertTrue(counts.get('LoseHealthOrMSB', 0) <= 5)
94
95         board = GameBoard.new_game({'cards': [
96             {'card_name': 'card1', 'actions': [{
97                 'action_class': 'LoseHealthOrMSB',
98                 'required_bits': [], }],
99              'max_number': 5},
100             {'card_name': 'card2', 'actions': [{
101                 'action_class': 'DoNothing',
102                 'required_bits': [], }],
103              'max_number': 25}]})
104         # check creation constraints
105         _check_counts(board)
106         # Check replacement
107         # Replace center card 12 times and assert invariant still holds
108         for x in range(12):
109             board.replace_card((2, 2))
110         _check_counts(board)
111         # replace a diagonal of cards
112         for x in range(5):
113             board.replace_card((x, x))
114         _check_counts(board)
115
116     def test_max_number_complex(self):
117         def _check_counts(board):
118             counts = {}
119             exported_state = board.export()
120             board_locations = exported_state.pop('board_locations')
121             for _position, location_state in board_locations:
122                 counts.setdefault(
123                     location_state['actions'][0]['action_class'], 0)
124                 counts[location_state['actions'][0]['action_class']] += 1
125             self.assertTrue(counts.get('LoseHealthOrMSB', 0) <= 5)
126             self.assertTrue(counts.get('DoNothing', 0) <= 3)
127             self.assertTrue(counts.get('AcquireWinToken', 0) <= 4)
128             self.assertTrue(counts.get('GainHealth', 0) <= 3)
129
130         board = GameBoard.new_game({'cards': [
131             {'card_name': 'card1', 'actions': [{
132                 'action_class': 'LoseHealthOrMSB',
133                 'required_bits': [], }],
134              'max_number': 5},
135             {'card_name': 'card2', 'actions': [{
136                 'action_class': 'AcquireWinToken',
137                 'required_bits': [], }],
138              'max_number': 4},
139             {'card_name': 'card3', 'actions': [{
140                 'action_class': 'GainHealth',
141                 'required_bits': [], }],
142              'max_number': 3},
143             {'card_name': 'card4', 'actions': [{
144                 'action_class': 'RotateLocations',
145                 'required_bits': [], }],
146              'max_number': 25},
147             {'card_name': 'card5', 'actions': [{
148                 'action_class': 'AllowChessMove',
149                 'required_bits': [], }],
150              'max_number': 25},
151             {'card_name': 'card6', 'actions': [{
152                 'action_class': 'DoNothing',
153                 'required_bits': [], }],
154              'max_number': 3}]})
155         # check creation constraints
156         _check_counts(board)
157         # Check replacement
158         # Replace center card 12 times and assert invariant still holds
159         for x in range(12):
160             board.replace_card((2, 2))
161         _check_counts(board)
162         # replace a diagonal of cards
163         for x in range(5):
164             board.replace_card((x, x))
165         _check_counts(board)
166
167     def test_lose_health(self):
168         board = GameBoard.new_game(self.make_deck())
169         self.assertEqual(board.health, 4)
170         state_1 = board.export()
171
172         board.lose_health()
173         self.assertEqual(board.health, 3)
174         state_2 = board.export()
175
176         self.assert_state(state_1, state_2, exclude=['health'])
177
178     def test_gain_health(self):
179         board = GameBoard.new_game(self.make_deck())
180         board.health = 2
181         self.assertEqual(board.health, 2)
182         state_1 = board.export()
183
184         board.gain_health()
185         self.assertEqual(board.health, 3)
186         state_2 = board.export()
187
188         self.assert_state(state_1, state_2, exclude=['health'])
189
190     def test_gain_health_at_max(self):
191         board = GameBoard.new_game(self.make_deck())
192         self.assertEqual(board.health, 4)
193         state_1 = board.export()
194
195         board.gain_health()
196         self.assertEqual(board.health, 4)
197         state_2 = board.export()
198
199         self.assert_state(state_1, state_2)
200
201     def generate_locations(self, override_dict=None):
202         locations_dict = dict(((x, y), '%s%s' % (x, y))
203                               for x in range(5) for y in range(5))
204         if override_dict:
205             locations_dict.update(override_dict)
206         return locations_dict
207
208     def test_shift_locations_north(self):
209         board = GameBoard.new_game(self.make_deck())
210         board.board_locations = self.generate_locations()
211         board.shift_locations('NORTH')
212         self.assertEqual(board.board_locations, self.generate_locations({
213             (2, 0): '21', (2, 1): '23', (2, 3): '24', (2, 4): '20',
214         }))
215
216     def test_shift_locations_south(self):
217         board = GameBoard.new_game(self.make_deck())
218         board.board_locations = self.generate_locations()
219         board.shift_locations('SOUTH')
220         self.assertEqual(board.board_locations, self.generate_locations({
221             (2, 0): '24', (2, 1): '20', (2, 3): '21', (2, 4): '23',
222         }))
223
224     def test_shift_locations_east(self):
225         board = GameBoard.new_game(self.make_deck())
226         board.board_locations = self.generate_locations()
227         board.shift_locations('EAST')
228         self.assertEqual(board.board_locations, self.generate_locations({
229             (0, 2): '42', (1, 2): '02', (3, 2): '12', (4, 2): '32',
230         }))
231
232     def test_shift_locations_west(self):
233         board = GameBoard.new_game(self.make_deck())
234         board.board_locations = self.generate_locations()
235         board.shift_locations('WEST')
236         self.assertEqual(board.board_locations, self.generate_locations({
237             (0, 2): '12', (1, 2): '32', (3, 2): '42', (4, 2): '02',
238         }))
239
240     def test_rotate_locations_anticlockwise(self):
241         board = GameBoard.new_game(self.make_deck())
242         board.board_locations = self.generate_locations()
243         board.rotate_locations('ANTICLOCKWISE')
244         self.assertEqual(board.board_locations, self.generate_locations({
245             (1, 1): '21', (2, 1): '31', (3, 1): '32',
246             (1, 2): '11',               (3, 2): '33',
247             (1, 3): '12', (2, 3): '13', (3, 3): '23',
248         }))
249
250     def test_rotate_locations_anticlockwise_top(self):
251         board = GameBoard.new_game(self.make_deck(), initial_pos=(2, 0))
252         board.board_locations = self.generate_locations()
253         board.rotate_locations('ANTICLOCKWISE')
254         self.assertEqual(board.board_locations, self.generate_locations({
255             (1, 0): '30',               (3, 0): '31',
256             (1, 1): '10', (2, 1): '11', (3, 1): '21',
257         }))
258
259     def test_rotate_locations_anticlockwise_right(self):
260         board = GameBoard.new_game(self.make_deck(), initial_pos=(0, 2))
261         board.board_locations = self.generate_locations()
262         board.rotate_locations('ANTICLOCKWISE')
263         self.assertEqual(board.board_locations, self.generate_locations({
264             (0, 1): '11', (1, 1): '12',
265                           (1, 2): '13',
266             (0, 3): '01', (1, 3): '03',
267         }))
268
269     def test_rotate_locations_anticlockwise_corner(self):
270         board = GameBoard.new_game(self.make_deck(), initial_pos=(0, 4))
271         board.board_locations = self.generate_locations()
272         board.rotate_locations('ANTICLOCKWISE')
273         self.assertEqual(board.board_locations, self.generate_locations({
274             (0, 3): '13', (1, 3): '14',
275                           (1, 4): '03',
276         }))
277
278     def test_rotate_locations_clockwise(self):
279         board = GameBoard.new_game(self.make_deck())
280         board.board_locations = self.generate_locations()
281         board.rotate_locations('CLOCKWISE')
282         self.assertEqual(board.board_locations, self.generate_locations({
283             (1, 1): '12', (2, 1): '11', (3, 1): '21',
284             (1, 2): '13',               (3, 2): '31',
285             (1, 3): '23', (2, 3): '33', (3, 3): '32',
286         }))
287
288     def test_rotate_locations_clockwise_1_3(self):
289         board = GameBoard.new_game(self.make_deck(), initial_pos=(1, 3))
290         board.board_locations = self.generate_locations()
291         board.rotate_locations('CLOCKWISE')
292         self.assertEqual(board.board_locations, self.generate_locations({
293             (0, 2): '03', (1, 2): '02', (2, 2): '12',
294             (0, 3): '04',               (2, 3): '22',
295             (0, 4): '14', (1, 4): '24', (2, 4): '23',
296         }))
297
298     def test_allow_chess_move_knight(self):
299         board = GameBoard.new_game(self.make_deck())
300         board.allow_chess_move(MOVES.KNIGHT)
301         self.assertEqual(board.player.movement_mode, MOVES.KNIGHT)
302
303     def test_allow_chess_move_bishop(self):
304         board = GameBoard.new_game(self.make_deck())
305         board.allow_chess_move(MOVES.BISHOP)
306         self.assertEqual(board.player.movement_mode, MOVES.BISHOP)
307
308     def test_allow_chess_move_castle(self):
309         board = GameBoard.new_game(self.make_deck())
310         board.allow_chess_move(MOVES.CASTLE)
311         self.assertEqual(board.player.movement_mode, MOVES.CASTLE)
312
313
314 class TestLocationCard(TestCase):
315     def test_generate_bitwise_operand(self):
316         # This is testing a random process, so it may fail occasionally.
317         operand_sets = []
318         for _ in range(100):
319             operand_sets.append(LocationCard.generate_bitwise_operand())
320         sizes = {2: 0, 3: 0}
321         bits = set()
322         for operand_set in operand_sets:
323             sizes[len(operand_set)] += 1
324             bits.update(operand_set)
325             # TODO: Test that there's at least one condition and one direction.
326         self.assertTrue(sizes[2] > 0)
327         self.assertTrue(sizes[3] > 0)
328         self.assertTrue(sizes[2] > sizes[3])
329         self.assertEqual(bits, set(BITS.values()))
330
331     def test_new_location_no_actions(self):
332         location = LocationCard.new_location({'card_name': 'card',
333                                               'actions': []})
334         [action] = location.actions
335         self.assertEqual(type(action), actions.DoNothing)
336         self.assertEqual(action.required_bits, set())
337
338     def test_new_location_one_action(self):
339         location = LocationCard.new_location({'card_name': 'card1',
340             'actions': [{'required_bits': [], 'action_class': 'DoNothing'},
341         ]})
342         [action] = location.actions
343         self.assertEqual(type(action), actions.DoNothing)
344         self.assertEqual(action.required_bits, set())
345
346     def test_parse_bits(self):
347         self.assertEqual(
348             LocationCard.parse_bits([]), frozenset([]))
349         self.assertEqual(
350             LocationCard.parse_bits(['RED']), frozenset([BITS.RED]))
351         self.assertEqual(
352             LocationCard.parse_bits([BITS.BLUE]), frozenset([BITS.BLUE]))
353         self.assertEqual(
354             LocationCard.parse_bits([BITS.NORTH, 'MSB']),
355             frozenset([BITS.NORTH, BITS.MSB]))