package game import ( "os" "path/filepath" "testing" ) func TestResolvePhasesPrefersExplicitPhases(t *testing.T) { obs := ObstacleDef{ RoomID: 5, Verb: "climb", Phases: []ObstaclePhase{ {Message: "start", Delay: 0}, {Message: "middle", Delay: 2, FailCheck: true}, {Message: "end", Delay: 1.5}, }, // Legacy fields set as a decoy; should be ignored when Phases present. TicksPerPhase: 99, Messages: []string{"ignored1", "ignored2", "ignored3"}, } got := resolvePhases(obs) if len(got) != 3 { t.Fatalf("expected 3 phases, got %d", len(got)) } if got[0].Message != "start" || got[0].Delay != 0 || got[0].FailCheck { t.Errorf("phase 0 wrong: %+v", got[0]) } if got[1].Message != "middle" || got[1].Delay != 2 || !got[1].FailCheck { t.Errorf("phase 1 (fail check) wrong: %+v", got[1]) } if got[2].Message != "end" || got[2].Delay != 1.5 { t.Errorf("phase 2 wrong: %+v", got[2]) } } func TestResolvePhasesLegacyMigration(t *testing.T) { obs := ObstacleDef{ RoomID: 7, Verb: "jump", TicksPerPhase: 2, Messages: []string{"a", "b", "c"}, } got := resolvePhases(obs) if len(got) != 3 { t.Fatalf("expected 3 legacy phases, got %d", len(got)) } if got[0].Message != "a" || got[0].Delay != 0 || got[0].FailCheck { t.Errorf("legacy phase 0 wrong: %+v", got[0]) } if got[1].Message != "b" || got[1].Delay != 2 || !got[1].FailCheck { t.Errorf("legacy phase 1 must carry fail_check at index 1: %+v", got[1]) } if got[2].Message != "c" || got[2].Delay != 2 || got[2].FailCheck { t.Errorf("legacy phase 2 wrong: %+v", got[2]) } } func TestCourseStoreLoadsWithExplicitFailChance(t *testing.T) { dir := t.TempDir() coursesDir := filepath.Join(dir, "courses") if err := os.MkdirAll(coursesDir, 0755); err != nil { t.Fatal(err) } yaml := []byte(` name: "Test Course" required_level: 10 start_room: 100 completion_xp: 50 obstacles: - room_id: 101 verb: climb xp: 12 fail_damage: [2, 5] fail_chance: 0.20 phases: - message: "begin" delay: 0 - message: "mid" delay: 2 fail_check: true - message: "done" delay: 2 - room_id: 102 verb: jump xp: 14 fail_damage: [1, 3] messages: ["p0", "p1", "p2"] ticks_per_phase: 1 `) if err := os.WriteFile(filepath.Join(coursesDir, "testcourse.yaml"), yaml, 0644); err != nil { t.Fatal(err) } cs := NewCourseStore(dir) cs.LoadAll() // Obstacle 1: explicit phases + explicit fail_chance o1 := cs.GetObstacle(101) if o1 == nil { t.Fatal("expected obstacle for room 101") } if len(o1.Phases) != 3 || o1.Phases[1].FailCheck != true { t.Errorf("obstacle 1 phases wrong: %+v", o1.Phases) } if o1.FailChance == nil || *o1.FailChance != 0.20 { t.Errorf("expected explicit fail_chance 0.20, got %v", o1.FailChance) } if o1.ObstacleIndex != 0 || o1.TotalObstacles != 2 { t.Errorf("index/total wrong: %d/%d", o1.ObstacleIndex, o1.TotalObstacles) } if o1.NextRoom != 102 { t.Errorf("expected next room 102, got %d", o1.NextRoom) } if o1.CompletionXP != 0 { t.Errorf("first obstacle should have no completion xp, got %d", o1.CompletionXP) } // Obstacle 2: legacy migration, derived fail chance (nil) o2 := cs.GetObstacle(102) if o2 == nil { t.Fatal("expected obstacle for room 102") } if len(o2.Phases) != 3 || !o2.Phases[1].FailCheck { t.Errorf("legacy obstacle phases not migrated: %+v", o2.Phases) } if o2.FailChance != nil { t.Errorf("legacy obstacle should have nil (derived) fail_chance, got %v", *o2.FailChance) } if o2.CompletionXP != 50 { t.Errorf("last obstacle should carry completion_xp, got %d", o2.CompletionXP) } if o2.NextRoom != 0 { t.Errorf("last obstacle should have next_room 0, got %d", o2.NextRoom) } if cs.GetObstacle(999) != nil { t.Error("expected nil for unrelated room") } }