Repository navigation
Expand file tree
/
Copy pathtest_game.py
More file actions
228 lines (182 loc) · 8.66 KB
/
Copy pathtest_game.py
File metadata and controls
228 lines (182 loc) · 8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""
Automated unit tests for Gemini PAC-MAN.
Verifies:
1. Speed specifications:
- Level 1 Ghost speed is 75% slower than Pac-Man (ratio = 0.25)
- Level 100 Ghost speed is 50x Level 1 (ratio = 50.0)
- Level progression is monotonically increasing for 100 levels
2. Encrypted Top 10 High Scores:
- Authenticated encryption roundtrip
- File tamper detection (HMAC verification rejects modified bytes)
- Top 10 sorting and trimming
"""
import os
import sys
import unittest
import tempfile
from typing import Dict, Any
# Add project root to sys.path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from pacman.constants import (
TOTAL_LEVELS, BASE_PACMAN_SPEED, GHOST_SPEED_RATIO_LEVEL_1,
GHOST_SPEED_MULTIPLIER_LEVEL_100, get_ghost_base_speed, get_frightened_duration,
MAX_HIGH_SCORES
)
from pacman.crypto import encrypt_payload, decrypt_payload
from pacman.highscores import HighScoreManager, get_default_high_scores
from pacman.maze import Maze
class TestPacmanSpeedSpecifications(unittest.TestCase):
"""Test suite validating exact speed and level requirements."""
def test_total_levels_count(self):
"""Must have exactly 100 levels."""
self.assertEqual(TOTAL_LEVELS, 100)
def test_level_1_ghost_speed_slower_by_75_percent(self):
"""
Specification:
'GHOST running speed at level 1 slower than PAC-MAN about 75%'
-> Speed(L1) = PacmanSpeed * (1 - 0.75) = 0.25 * PacmanSpeed
"""
pacman_spd = BASE_PACMAN_SPEED
ghost_l1_spd = get_ghost_base_speed(1, pacman_spd)
expected_ratio = 1.0 - 0.75 # 0.25
actual_ratio = ghost_l1_spd / pacman_spd
self.assertAlmostEqual(actual_ratio, expected_ratio, places=4,
msg=f"Level 1 Ghost speed should be 0.25x Pacman speed (75% slower), got {actual_ratio}")
def test_level_100_ghost_speed_50_times_level_1(self):
"""
Specification:
'GHOST running speed level 1 is slower than level 100 about 50 times.'
-> Speed(L100) / Speed(L1) = 50.0
"""
pacman_spd = BASE_PACMAN_SPEED
ghost_l1_spd = get_ghost_base_speed(1, pacman_spd)
ghost_l100_spd = get_ghost_base_speed(100, pacman_spd)
ratio = ghost_l100_spd / ghost_l1_spd
self.assertAlmostEqual(ratio, 50.0, places=4,
msg=f"Level 100 Ghost speed must be 50x Level 1 Ghost speed, got {ratio}")
def test_all_100_levels_monotonic_increase(self):
"""Ghost speed must smoothly and strictly increase from level 1 through 100."""
pacman_spd = BASE_PACMAN_SPEED
prev_spd = get_ghost_base_speed(1, pacman_spd)
for lvl in range(2, TOTAL_LEVELS + 1):
curr_spd = get_ghost_base_speed(lvl, pacman_spd)
self.assertGreater(curr_spd, prev_spd, f"Ghost speed at level {lvl} should exceed level {lvl-1}")
prev_spd = curr_spd
class TestEncryptedHighScores(unittest.TestCase):
"""Test suite validating encryption, tamper detection, and top 10 limit."""
def test_encryption_roundtrip(self):
"""Encrypted payload decrypts back to original dictionary."""
data = {
"test": True,
"highscores": [
{"name": "TESTER", "score": 99990, "level": 50, "date": "2026-09-05"}
]
}
encrypted = encrypt_payload(data)
self.assertIsInstance(encrypted, bytes)
# Ensure it's not plaintext
self.assertNotIn(b"TESTER", encrypted)
self.assertNotIn(b"99990", encrypted)
decrypted = decrypt_payload(encrypted)
self.assertEqual(decrypted["highscores"][0]["name"], "TESTER")
self.assertEqual(decrypted["highscores"][0]["score"], 99990)
def test_tamper_detection_on_file_modification(self):
"""
Tampering with even a single byte in the file MUST raise PermissionError
due to HMAC authentication failure, preventing player cheating.
"""
data = {"highscores": [{"name": "PLAYER", "score": 1000}]}
encrypted = bytearray(encrypt_payload(data))
# Tamper with the last byte (ciphertext payload)
encrypted[-1] ^= 0xFF
with self.assertRaises(PermissionError):
decrypt_payload(bytes(encrypted))
def test_tamper_detection_on_header_modification(self):
"""Tampering with magic header or salt must also be rejected."""
data = {"highscores": [{"name": "PLAYER", "score": 1000}]}
encrypted = bytearray(encrypt_payload(data))
# Tamper with salt byte
encrypted[10] ^= 0x55
with self.assertRaises(PermissionError):
decrypt_payload(bytes(encrypted))
def test_top_10_high_scores_manager(self):
"""Verifies top 10 storage, sorting, and file persistence."""
with tempfile.NamedTemporaryFile(suffix=".dat", delete=False) as tf:
temp_path = tf.name
try:
mgr = HighScoreManager(filepath=temp_path)
self.assertEqual(len(mgr.scores), MAX_HIGH_SCORES)
# Insert high score
rank = mgr.add_score("CHAMPION", 999999, 100)
self.assertEqual(rank, 1)
self.assertEqual(mgr.scores[0]["name"], "CHAMPION")
self.assertEqual(mgr.scores[0]["score"], 999999)
self.assertEqual(len(mgr.scores), MAX_HIGH_SCORES)
# Reload from disk to ensure persistence and encryption
mgr_reloaded = HighScoreManager(filepath=temp_path)
self.assertFalse(mgr_reloaded.tamper_detected)
self.assertEqual(mgr_reloaded.scores[0]["name"], "CHAMPION")
self.assertEqual(mgr_reloaded.scores[0]["score"], 999999)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
if os.path.exists(temp_path + ".tmp"):
os.remove(temp_path + ".tmp")
class TestMazeLayout(unittest.TestCase):
"""Test suite validating maze grid and tunnel."""
def test_maze_initialization(self):
maze = Maze()
self.assertGreater(maze.pellet_count, 100)
self.assertEqual(len(maze.energizers), 4)
def test_tunnel_wrapping_passable(self):
"""Row 17 side tunnel exits must be passable for wrapping."""
maze = Maze()
# Row 17 tunnel outer edges
self.assertTrue(maze.is_passable(-1, 17, is_ghost=False))
self.assertTrue(maze.is_passable(28, 17, is_ghost=False))
class TestEntitiesAndGameplay(unittest.TestCase):
"""Validates entity movement, ghost AI behaviors, and speed scaling on entities."""
def test_ghost_entities_speed_matching_level(self):
from pacman.entities import Blinky, Pinky, Inky, Clyde
from pacman.constants import BASE_PACMAN_SPEED, get_ghost_base_speed
ghosts = [Blinky(), Pinky(), Inky(), Clyde()]
for g in ghosts:
# Test level 1
g.reset(level=1, pacman_speed=BASE_PACMAN_SPEED)
expected_l1 = get_ghost_base_speed(1, BASE_PACMAN_SPEED)
self.assertAlmostEqual(g.base_speed, expected_l1, places=4)
self.assertAlmostEqual(g.base_speed / BASE_PACMAN_SPEED, 0.25, places=4)
# Test level 100
g.reset(level=100, pacman_speed=BASE_PACMAN_SPEED)
expected_l100 = get_ghost_base_speed(100, BASE_PACMAN_SPEED)
self.assertAlmostEqual(g.base_speed, expected_l100, places=4)
self.assertAlmostEqual(g.base_speed / expected_l1, 50.0, places=4)
def test_pacman_cornering_and_reset(self):
from pacman.entities import Pacman
from pacman.constants import DIR_UP, DIR_LEFT, DIR_RIGHT
pacman = Pacman()
self.assertTrue(pacman.alive)
self.assertFalse(pacman.dying)
pacman.request_direction(DIR_LEFT)
self.assertEqual(pacman.next_direction, DIR_LEFT)
pacman.reset_position()
self.assertTrue(pacman.alive)
self.assertEqual(pacman.death_frame, 0)
def test_frightened_duration_scaling(self):
"""Energizer duration must decrease from level 1 down to 100."""
dur_l1 = get_frightened_duration(1)
dur_l100 = get_frightened_duration(100)
self.assertEqual(dur_l1, 8.0)
self.assertEqual(dur_l100, 0.5)
self.assertGreater(dur_l1, dur_l100)
class TestZeroDependencyOperation(unittest.TestCase):
"""Ensures core engine operates 100% using Python standard library."""
def test_native_crypto_and_highscores_independent_of_third_party(self):
import pacman.crypto as crypto
import pacman.highscores as highscores
import pacman.constants as constants
self.assertIsNotNone(crypto)
self.assertIsNotNone(highscores)
self.assertIsNotNone(constants)
if __name__ == "__main__":
unittest.main()