aboutsummaryrefslogtreecommitdiff
path: root/engine/integration_test.go
blob: b999b45e91c0852fa0f602fa40207dfc29133115 (plain) (blame)
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

package engine

import (
	"image/color"
	"testing"
	"time"

	"age_of_empires/physics"
	"age_of_empires/selection"

	"github.com/stretchr/testify/assert"
)

func TestCameraScreenToWorldRoundTrip(t *testing.T) {
	zooms := []float64{0.25, 0.5, 1.0, 2.0, 3.0}
	points := []physics.Point{
		{X: 1000, Y: 1000},
		{X: 0, Y: 0},
		{X: 3200, Y: 2400},
		{X: 1600, Y: 1200},
	}
	for _, zoom := range zooms {
		cam := Camera{X: 1600, Y: 1200, Zoom: zoom}
		for _, p := range points {
			sx, sy := cam.WorldToScreen(p)
			got := cam.ScreenToWorld(int(sx), int(sy))
			assert.Equal(t, p, got, "zoom=%.2f point=%v", zoom, p)
		}
	}
}

func TestCameraClampsToBounds(t *testing.T) {
	cases := []struct{ x, y float64 }{
		{-99999, -99999},
		{99999, 99999},
		{-99999, 99999},
		{99999, -99999},
	}
	for _, c := range cases {
		cam := Camera{X: c.x, Y: c.y, Zoom: 1.0}
		cam.clampToBounds()
		halfW := (ScreenWidth / 2.0) / cam.Zoom
		halfH := (ScreenHeight / 2.0) / cam.Zoom
		assert.GreaterOrEqual(t, cam.X, halfW, "X=%v should be >= halfW", c.x)
		assert.LessOrEqual(t, cam.X, float64(WorldWidth)-halfW, "X=%v should be <= maxX", c.x)
		assert.GreaterOrEqual(t, cam.Y, halfH, "Y=%v should be >= halfH", c.y)
		assert.LessOrEqual(t, cam.Y, float64(WorldHeight)-halfH, "Y=%v should be <= maxY", c.y)
	}
}

func TestCameraZoomTowardCursor(t *testing.T) {
	zooms := []struct{ before, after float64 }{
		{1.0, 2.0},
		{1.0, 0.5},
		{2.0, 3.0},
	}
	cursorSX, cursorSY := 400, 300
	for _, z := range zooms {
		cam := Camera{X: 1600, Y: 1200, Zoom: z.before}
		worldBefore := cam.ScreenToWorld(cursorSX, cursorSY)

		// replicate the zoom-toward-cursor logic from Camera.Update
		wx0 := float64(cursorSX-ScreenWidth/2)/cam.Zoom + cam.X
		wy0 := float64(cursorSY-ScreenHeight/2)/cam.Zoom + cam.Y
		cam.Zoom = z.after
		wx1 := float64(cursorSX-ScreenWidth/2)/cam.Zoom + cam.X
		wy1 := float64(cursorSY-ScreenHeight/2)/cam.Zoom + cam.Y
		cam.X -= wx1 - wx0
		cam.Y -= wy1 - wy0

		worldAfter := cam.ScreenToWorld(cursorSX, cursorSY)
		assert.Equal(t, worldBefore, worldAfter, "zoom %.1f→%.1f: world point under cursor should be stable", z.before, z.after)
	}
}

func TestGatherLoop(t *testing.T) {
	// Initialize Game
	game := &Game{}

	// Town Center (Storage) at -100,0 (different grid cell from unit at 0,0)
	tcBuilder := NewEntityBuilder().
		WithPosition(physics.Point{X: -100, Y: 0}).
		WithResourceStorage()
	_ = game.Append(tcBuilder.Build())

	// Mine (Source) at 0, 200
	mineBuilder := NewEntityBuilder().
		WithPosition(physics.Point{X: 0, Y: 200}).
		WithResourceSource(100)
	mineID := game.Append(mineBuilder.Build())

	// Unit (Gatherer) at 0, 0
	unitBuilder := NewEntityBuilder().
		WithPosition(physics.Point{X: 0, Y: 0}).
		WithSolid(NewFilledCircleImage(100, color.White)).
		WithSelectable("round", selection.Unit).
		WithMove().
		WithOrder().
		WithResourceGatherer(10)
	game.Append(unitBuilder.Build())

	// Set up simulated time
	simTime := time.Now()
	game.NowFunc = func() time.Time { return simTime }

	// Click unit to select, then right-click mine — same gesture as in-game
	game.UpdateSimulation(Input{EscapePressed: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 50, Y: 50}, LeftMouseDown: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 50, Y: 50}, LeftMouseUp: true})
	game.UpdateSimulation(Input{Cursor: game.Position[mineID], RightMouseUp: true})

	initialResources := game.ResourceAmount
	dt := 16 * time.Millisecond
	maxTicks := 100_000

	deposited := false

	for i := 0; i < maxTicks; i++ {
		simTime = simTime.Add(dt)
		game.UpdateSimulation(Input{})

		if game.ResourceAmount > initialResources {
			deposited = true
			break
		}
	}

	if !deposited {
		t.Errorf("Unit failed to gather resources within %d ticks. ResourceAmount: %d", maxTicks, game.ResourceAmount)
	}
}

func TestTwoEntitiesMovingToSameCellDontOverlap(t *testing.T) {
	// Two units close together both ordered to the same cell should not overlap.
	game := &Game{}

	target := physics.Point{X: 500, Y: 0}
	unitImg := NewFilledCircleImage(100, color.White)

	unitA := game.Append(NewEntityBuilder().
		WithPosition(physics.Point{X: 200, Y: 0}).
		WithSolid(unitImg).WithSelectable("round", selection.Unit).
		WithMove().WithOrder().Build())
	unitB := game.Append(NewEntityBuilder().
		WithPosition(physics.Point{X: 100, Y: 0}).
		WithSolid(unitImg).WithSelectable("round", selection.Unit).
		WithMove().WithOrder().Build())

	// Area-select both units then right-click target — same gesture as in-game
	game.UpdateSimulation(Input{EscapePressed: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 50, Y: -50}, LeftMouseDown: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 350, Y: 50}, LeftMouseUp: true})
	game.UpdateSimulation(Input{Cursor: target, RightMouseUp: true})

	// MOVE_SPEED=10, cell=100 → 10 ticks per cell. B detours around A → ~60 ticks.
	for range 60 {
		game.UpdateSimulation(Input{})
	}

	assert.False(t, game.Move[unitA].IsActive, "Entity A should have stopped moving")
	assert.False(t, game.Move[unitB].IsActive, "Entity B should have stopped moving")
	assert.NotEqual(t, game.Position[unitA], game.Position[unitB],
		"Entity A and B should NOT overlap: A at %s, B at %s", game.Position[unitA], game.Position[unitB])
}

func TestUnitDoesNotStopWhenDestinationOccupied(t *testing.T) {
	// Unit B is far away, unit A is close. Both ordered to the same cell.
	// A arrives first and settles. B should repath to an adjacent free cell
	// rather than stopping mid-journey.
	game := &Game{}
	game.CurrentAction = Selecting
	unitImg := NewFilledCircleImage(100, color.White)

	target := physics.Point{X: 500, Y: 0}

	unitA := game.Append(NewEntityBuilder().
		WithPosition(physics.Point{X: 400, Y: 0}).
		WithSolid(unitImg).WithSelectable("round", selection.Unit).
		WithMove().WithOrder().Build())
	unitB := game.Append(NewEntityBuilder().
		WithPosition(physics.Point{X: 0, Y: 0}).
		WithSolid(unitImg).WithSelectable("round", selection.Unit).
		WithMove().WithOrder().Build())

	game.UpdateSimulation(Input{EscapePressed: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 50, Y: -50}, LeftMouseDown: true})
	game.UpdateSimulation(Input{Cursor: physics.Point{X: 450, Y: 50}, LeftMouseUp: true})
	game.UpdateSimulation(Input{Cursor: target, RightMouseUp: true})

	// Run long enough for both units to settle (B travels ~5 cells)
	for range 100 {
		game.UpdateSimulation(Input{})
	}

	assert.False(t, game.Move[unitA].IsActive, "unit A should have stopped")
	assert.False(t, game.Move[unitB].IsActive, "unit B should have stopped near target, not mid-journey")
	assert.NotEqual(t, game.Position[unitA], game.Position[unitB], "units should not overlap")
	// B should be adjacent to target, not stuck far away
	assert.LessOrEqual(t, physics.Distance(game.Position[unitB], target), 150.0,
		"unit B should be adjacent to target, got %s", game.Position[unitB])
}