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
|
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")
}
}
|