## Sprint 19 — Game session management (#258, D-085) ## Per-game save directories: created on New Game, resumed via game-id. ## SessionManager autoload: new_game(), resume_game(), list_game_dirs(). class_name TestSessionManagerSprint19 extends GdUnitTestSuite const MAIN_MENU_SCENE = preload("res://scenes/main_menu.tscn") # Game IDs created during the current test — deleted in after_test(). var _created_ids: Array = [] # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func before_test() -> void: GameState.current_game_id = "" _created_ids = [] func after_test() -> void: for game_id in _created_ids: var path := "user://saves/" + game_id DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) _created_ids.clear() GameState.current_game_id = "" # --------------------------------------------------------------------------- # Helper: call new_game() and track the created directory for cleanup. # --------------------------------------------------------------------------- func _new_game() -> String: var game_id := SessionManager.new_game() if not game_id.is_empty(): _created_ids.append(game_id) return game_id # --------------------------------------------------------------------------- # GameState.current_game_id field # --------------------------------------------------------------------------- func test_current_game_id_field_exists() -> void: ## D-085: GameState must have current_game_id field. assert_bool(GameState.has("current_game_id")).override_failure_message( "GameState must have 'current_game_id' field (D-085 #258)" ).is_true() func test_current_game_id_default_is_empty_string() -> void: ## Before any session starts, current_game_id is empty. GameState.current_game_id = "" assert_str(GameState.current_game_id).override_failure_message( "GameState.current_game_id default must be empty string" ).is_empty() # --------------------------------------------------------------------------- # SessionManager autoload exists # --------------------------------------------------------------------------- func test_session_manager_autoload_exists() -> void: ## SessionManager must be registered as an autoload. var sm := Engine.get_singleton("SessionManager") assert_that(sm != null).override_failure_message( "SessionManager must be registered as autoload in project.godot (#258)" ).is_true() # --------------------------------------------------------------------------- # new_game() — game-id format and GameState update # --------------------------------------------------------------------------- func test_new_game_returns_non_empty_string() -> void: var game_id := _new_game() assert_str(game_id).override_failure_message( "SessionManager.new_game() must return a non-empty game-id string" ).is_not_empty() func test_new_game_sets_current_game_id_on_gamestate() -> void: var game_id := _new_game() assert_str(GameState.current_game_id).override_failure_message( "new_game() must set GameState.current_game_id" ).is_equal(game_id) func test_new_game_id_format_has_two_dashes() -> void: ## Format: -- — two separator dashes. var game_id := _new_game() var parts := game_id.split("-") assert_int(parts.size()).override_failure_message( "game-id must have format -- (3 parts separated by '-')" ).is_equal(3) func test_new_game_id_first_part_is_8_digits() -> void: ## First part is YYYYMMDD — 8 decimal digits. var game_id := _new_game() var parts := game_id.split("-") assert_int(parts[0].length()).override_failure_message( "game-id first part (date) must be 8 characters (YYYYMMDD)" ).is_equal(8) func test_new_game_id_second_part_is_6_digits() -> void: ## Second part is HHMMSS — 6 decimal digits. var game_id := _new_game() var parts := game_id.split("-") assert_int(parts[1].length()).override_failure_message( "game-id second part (time) must be 6 characters (HHMMSS)" ).is_equal(6) func test_new_game_id_third_part_is_6_hex_chars() -> void: ## Third part is 6 hex characters (RNG seed). var game_id := _new_game() var parts := game_id.split("-") assert_int(parts[2].length()).override_failure_message( "game-id third part (hex seed) must be 6 characters" ).is_equal(6) func test_new_game_ids_are_unique() -> void: ## Two rapid new_game() calls should produce different IDs ## (different RNG seeds; same-second timestamps are valid but seeds differ). var id1 := _new_game() var id2 := _new_game() # Check that hex seeds differ (they almost certainly will) var seed1 := id1.split("-")[2] var seed2 := id2.split("-")[2] assert_str(seed1).override_failure_message( "Successive new_game() calls should have different RNG seeds" ).is_not_equal(seed2) # --------------------------------------------------------------------------- # resume_game() — sets GameState.current_game_id # --------------------------------------------------------------------------- func test_resume_game_sets_current_game_id() -> void: var test_id := "20260225-143022-a7b3f1" SessionManager.resume_game(test_id) assert_str(GameState.current_game_id).override_failure_message( "resume_game() must set GameState.current_game_id to the given id" ).is_equal(test_id) func test_resume_game_overwrites_previous_game_id() -> void: SessionManager.resume_game("20260225-100000-aabbcc") SessionManager.resume_game("20260225-120000-112233") assert_str(GameState.current_game_id).is_equal("20260225-120000-112233") # --------------------------------------------------------------------------- # Main menu scene # --------------------------------------------------------------------------- func test_main_menu_scene_exists() -> void: assert_bool(ResourceLoader.exists("res://scenes/main_menu.tscn")).override_failure_message( "Main menu scene must exist at res://scenes/main_menu.tscn (#258)" ).is_true() func test_main_menu_instantiates_without_crash() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): push_warning("TestSessionManagerSprint19: main_menu.tscn not found — skip") return var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) assert_that(scene).is_not_null() func test_main_menu_has_new_game_button() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): return var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) var btn := scene.get_node_or_null("VBox/NewGameBtn") assert_that(btn != null).override_failure_message( "Main menu must have VBox/NewGameBtn (#258)" ).is_true() func test_main_menu_has_continue_button() -> void: if not ResourceLoader.exists("res://scenes/main_menu.tscn"): return var scene: Node = MAIN_MENU_SCENE.instantiate() auto_free(scene) add_child(scene) var btn := scene.get_node_or_null("VBox/ContinueBtn") assert_that(btn != null).override_failure_message( "Main menu must have VBox/ContinueBtn (#258)" ).is_true() # --------------------------------------------------------------------------- # Project main scene changed to main_menu.tscn # --------------------------------------------------------------------------- func test_project_main_scene_is_main_menu() -> void: ## D-085: project boots to main menu, not directly to game scene. var scene_path: String = ProjectSettings.get_setting("application/run/main_scene", "") assert_str(scene_path).override_failure_message( "project.godot run/main_scene must be res://scenes/main_menu.tscn (#258)" ).is_equal("res://scenes/main_menu.tscn")