import json import unittest from pydantic import ValidationError from content.catalog import STATS, TRAITS, ASPIRATIONS from engine.game import RuleError, act, new_game, spend_ap from engine.saves import dumps, loads class GameTests(unittest.TestCase): def test_full_first_day_and_save(self): state = new_game("naruto") state = act(state, "travel", "academy") state = act(state, "train") self.assertEqual((state.clock.ap, state.chakra, state.tutorial), (1, 90, "rest")) state = act(state, "end_day") self.assertEqual((state.clock.day, state.location, state.tutorial), (2, "home", "completed")) self.assertEqual(loads(dumps(state)), state) def test_slot_carry(self): state = new_game("naruto") state.clock.slot, state.clock.ap = 1, 1 spend_ap(state, 3) self.assertEqual((state.clock.slot, state.clock.ap), (2, 1)) def test_all_time_boundaries(self): for slot in range(5): for ap in range(1, 4): for cost in range(4): state = new_game("naruto") state.clock.slot, state.clock.ap = slot, ap elapsed = slot * 3 + (3 - ap) + cost spend_ap(state, cost) self.assertEqual((state.clock.day, state.clock.slot, state.clock.ap), (1 + elapsed // 15, (elapsed % 15) // 3, 3 - elapsed % 3)) def test_night_returns_home(self): state = new_game("sakura") state.clock.slot, state.clock.ap = 4, 1 state = act(state, "travel", "academy") self.assertEqual((state.clock.day, state.clock.slot, state.clock.ap, state.location), (2, 0, 3, "home")) def test_invalid_action_is_atomic(self): state = new_game("naruto") before = dumps(state) with self.assertRaises(RuleError): act(state, "train") self.assertEqual(dumps(state), before) def test_sessions_and_profiles_are_isolated(self): first, second = new_game("naruto"), new_game("naruto") first.character.traits.clear() first.short_memory.append("private") self.assertEqual(len(second.character.traits), 3) self.assertNotIn("private", second.short_memory) self.assertEqual(len(new_game("naruto").character.traits), 3) def test_canonical_ignores_custom_input(self): self.assertEqual(new_game("naruto", name="fake").character.name, "Naruto Uzumaki") def test_creation_budget(self): fields = dict(name="Işık", gender="Diğer", stats=dict.fromkeys(STATS, 5), traits=list(TRAITS[:3]), aspirations=list(ASPIRATIONS[:2])) self.assertEqual(new_game(**fields).character.name, "Işık") fields["stats"]["strength"] = 10 with self.assertRaises(ValidationError): new_game(**fields) def test_corrupt_save_rejected(self): data = json.loads(dumps(new_game("naruto"))) for raw in ("{", "null", "x" * 131073): with self.assertRaises(RuleError): loads(raw) for section, key, value in ((data, "schema_version", 99), (data["state"]["clock"], "ap", 0), (data["state"], "hp", True), (data["state"], "location", "unknown")): original = section[key] section[key] = value with self.assertRaises(RuleError): loads(json.dumps(data)) section[key] = original def test_training_resource_limit(self): state = act(new_game("naruto"), "travel", "academy") state.chakra = 9 with self.assertRaises(RuleError): act(state, "train") self.assertEqual(state.chakra, 9) def test_stale_revision_is_rejected(self): state = act(new_game("naruto"), "travel", "academy", expected_revision=0) with self.assertRaises(RuleError): act(state, "train", expected_revision=0) if __name__ == "__main__": unittest.main()