package game_test import ( "testing" "time" "github.com/egoist/mygo/ui" "git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/game" "git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon" "git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel" ) func TestHealthBarTrailHoldsThenDrains(t *testing.T) { wd := &world{clk: newClock()} hp := 80 tt := view(300, 60, wd, func(c *ui.Context) { game.HealthBar(c, "HP", hp, 100, game.HealthBarOptions{}) }) // The fill's middle row is 10; full is 92 dots. row := func(s pixel.Slot) int { return count(t, tt.Image(), 0, 10, 96, 1, s) } if row(pixel.Good) != 73 { t.Fatalf("80 HP fills %d dots", row(pixel.Good)) } hp = 40 tt.Frame() if row(pixel.Warn) != 36 || row(pixel.AccentLit) != 37 { t.Fatalf("at the hit: fill %d, trail %d", row(pixel.Warn), row(pixel.AccentLit)) } for _, step := range []struct { after time.Duration trail int }{{300 * time.Millisecond, 37}, {200 * time.Millisecond, 19}, {500 * time.Millisecond, 0}} { wd.clk.t = wd.clk.t.Add(step.after) tt.Frame() if row(pixel.AccentLit) != step.trail { t.Fatalf("%v later the trail is %d dots", step.after, row(pixel.AccentLit)) } } hp = 90 tt.Frame() if row(pixel.Good) != 82 || row(pixel.AccentLit) != 0 { t.Fatal("a heal left a trail") } // A heal during a live trail clears it too. hp = 30 tt.Frame() hp = 60 tt.Frame() if row(pixel.AccentLit) != 0 { t.Fatal("a heal kept a live trail") } keeps(t, tt.Image()) // A new full drops the old trail. full := 100 tt = view(300, 60, wd, func(c *ui.Context) { game.HealthBar(c, "HP", hp, full, game.HealthBarOptions{}) }) hp, full = 40, 50 tt.Frame() if row(pixel.AccentLit) != 0 || count(t, tt.Image(), 96, 0, 54, 30, pixel.AccentLit) != 0 { t.Fatal("the trail outlived a new full") } panics(t, "a bar narrower than its labels", func() { view(200, 60, wd, func(c *ui.Context) { game.HealthBar(c, "HP", 5, 100, game.HealthBarOptions{Width: 20}) }) }) panics(t, "hp past full", func() { view(200, 60, wd, func(c *ui.Context) { game.HealthBar(c, "HP", 5, 4, game.HealthBarOptions{}) }) }) } func TestHealthBarReducedHasNoTrailAndNoBlink(t *testing.T) { for _, reduce := range []bool{false, true} { wd := &world{clk: newClock(), reduce: reduce} hp := 90 tt := view(300, 60, wd, func(c *ui.Context) { game.HealthBar(c, "HP", hp, 100, game.HealthBarOptions{}) }) hp = 20 tt.Frame() if trail := count(t, tt.Image(), 0, 10, 96, 1, pixel.AccentLit); (trail == 0) != reduce { t.Fatalf("reduced %v: trail of %d dots", reduce, trail) } // Low HP blinks at 2 Hz: Bad, then Lit. seen := map[pixel.Slot]bool{} for range 2 { seen[at(t, tt.Image(), 4, 10)] = true wd.clk.t = wd.clk.t.Add(250 * time.Millisecond) tt.Frame() } if blinked := seen[pixel.Lit] && seen[pixel.Bad]; blinked == reduce { t.Fatalf("reduced %v: low HP showed %v", reduce, seen) } } } func TestManaBarTicks(t *testing.T) { tt := view(300, 60, &world{clk: newClock()}, func(c *ui.Context) { game.ManaBar(c, "MP", 35, 60, game.ManaBarOptions{}) }) img := tt.Image() // 35 of 60 fills 53 dots; three ticks cut in. if info, ticks := count(t, img, 0, 10, 96, 1, pixel.Info), count(t, img, 2, 10, 92, 1, pixel.Edge); info != 50 || ticks != 5 { t.Fatalf("fill %d dots, %d ticks", info, ticks) } } func TestExpBarFlashesWhenItFills(t *testing.T) { for _, tc := range []struct { name string level, xp int reduce, flash bool }{ {"a level up", 8, 10, false, true}, {"a fill", 7, 300, false, true}, {"a reduced level up", 8, 10, true, false}, {"a lost level", 6, 10, false, false}, } { wd := &world{clk: newClock(), reduce: tc.reduce} level, xp := 7, 290 tt := view(300, 60, wd, func(c *ui.Context) { game.ExpBar(c, "EXP", level, xp, 300, game.ExpBarOptions{}) }) level, xp = tc.level, tc.xp tt.Frame() // With no flash, the first frame already shows xp. if n, want := count(t, tt.Image(), 2, 10, 92, 1, pixel.Warn), tc.xp*92/300; !tc.flash && n != want { t.Fatalf("%s: the first frame fills %d dots, not %d", tc.name, n, want) } seen := map[pixel.Slot]bool{} for range 2 { img := tt.Image() seen[at(t, img, 4, 10)] = true if full := count(t, img, 2, 10, 92, 1, pixel.Lit) + count(t, img, 2, 10, 92, 1, pixel.Warn); tc.flash && full != 92 { t.Fatalf("%s: the flash fills %d dots", tc.name, full) } wd.clk.t = wd.clk.t.Add(100 * time.Millisecond) tt.Frame() } if flashed := seen[pixel.Lit] && seen[pixel.Warn]; flashed != tc.flash { t.Fatalf("%s: the bar showed %v", tc.name, seen) } // After any flash the bar shows xp. wd.clk.t = wd.clk.t.Add(time.Second) tt.Frame() if n, want := count(t, tt.Image(), 2, 10, 92, 1, pixel.Warn), tc.xp*92/300; n != want { t.Fatalf("%s: %d of 300 fills %d dots, not %d", tc.name, tc.xp, n, want) } } } func TestHeartsFillByQuarters(t *testing.T) { tt := view(300, 60, &world{clk: newClock()}, func(c *ui.Context) { game.Hearts(c, "Life", 1.75, 3, game.HeartsOptions{}) }) img := tt.Image() // Quadrant dots: bottom left, top left, top right, bottom right. quads := [][2]int{{4, 7}, {3, 4}, {8, 4}, {7, 7}} for i, want := range [][4]bool{{true, true, true, true}, {true, true, true, false}, {false, false, false, false}} { for q, d := range quads { if got := at(t, img, i*11+d[0], d[1]) == pixel.Bad; got != want[q] { t.Fatalf("heart %d quadrant %d filled %v", i, q, got) } } } keeps(t, img) // A lone quarter fills the bottom left only. tt = view(100, 40, &world{clk: newClock()}, func(c *ui.Context) { game.Hearts(c, "Life", 0.25, 1, game.HeartsOptions{}) }) for q, d := range quads { if got := at(t, tt.Image(), d[0], d[1]) == pixel.Bad; got != (q == 0) { t.Fatalf("a quarter heart filled quadrant %d: %v", q, got) } } for _, bad := range [][2]float64{{0.3, 3}, {1, 2.5}, {4, 3}, {-1, 3}} { panics(t, "hearts out of quarters", func() { view(100, 40, &world{clk: newClock()}, func(c *ui.Context) { game.Hearts(c, "x", bad[0], bad[1], game.HeartsOptions{}) }) }) } } func TestLivesShowTheirIcon(t *testing.T) { tt := view(300, 100, &world{clk: newClock()}, func(c *ui.Context) { game.Lives(c, "Lives", 3, game.LivesOptions{}) game.Lives(c, "Stars", 2, game.LivesOptions{Sprite: icon.Star}) }) lives, stars := find(t, tt, "Lives"), find(t, tt, "Stars") lx, ly := dots(lives) sx, sy := dots(stars) // The hero wears a Bad cap; the star is ink. if count(t, tt.Image(), lx, ly, 12, 16, pixel.Bad) == 0 || count(t, tt.Image(), sx, sy, 12, 16, pixel.Bad) != 0 { t.Fatal("life icons are wrong") } if int(lives.W)/2 != 12+2+pixel.Measure("×3") { t.Fatalf("lives are %v px wide", lives.W) } } func TestCoinSpinsUnlessReduced(t *testing.T) { for _, reduce := range []bool{false, true} { wd := &world{clk: newClock(), reduce: reduce} tt := view(300, 60, wd, func(c *ui.Context) { game.CoinCounter(c, "Gold", 1250, game.CoinCounterOptions{}) }) find(t, tt, "Gold") before := count(t, tt.Image(), 0, 0, 12, 16, pixel.Warn) wd.clk.t = wd.clk.t.Add(250 * time.Millisecond) tt.Frame() if spun := count(t, tt.Image(), 0, 0, 12, 16, pixel.Warn) != before; spun == reduce { t.Fatalf("reduced %v: the coin spun %v", reduce, spun) } } } func TestScoreBoardLightsANewBest(t *testing.T) { score := 500 tt := view(400, 100, &world{clk: newClock()}, func(c *ui.Context) { game.ScoreBoard(c, score, 9900, game.ScoreBoardOptions{}) }) if !tt.HasText("SCORE 500, HI-SCORE 9900") || count(t, tt.Image(), 0, 0, 200, 8, pixel.Warn) != 0 { t.Fatalf("texts %q", tt.Texts()) } score = 12500 tt.Frame() if !tt.HasText("SCORE 12500, HI-SCORE 12500") || count(t, tt.Image(), 0, 0, 200, 8, pixel.Warn) == 0 { t.Fatal("a new best is not lit") } } func TestTimerHUDHurriesUnderThirty(t *testing.T) { for _, tc := range []struct { left time.Duration reduce, blink bool ink pixel.Slot }{{87 * time.Second, false, false, pixel.Ink}, {12 * time.Second, false, true, pixel.Bad}, {12 * time.Second, true, false, pixel.Bad}, {29500 * time.Millisecond, true, false, pixel.Bad}} { wd := &world{clk: newClock(), reduce: tc.reduce} tt := view(300, 100, wd, func(c *ui.Context) { game.TimerHUD(c, tc.left, game.TimerHUDOptions{}) }) var shown []int for range 2 { shown = append(shown, count(t, tt.Image(), 0, 8, 60, 34, tc.ink)) wd.clk.t = wd.clk.t.Add(250 * time.Millisecond) tt.Frame() } if blinked := (shown[0] == 0) != (shown[1] == 0); blinked != tc.blink || shown[0]+shown[1] == 0 { t.Fatalf("%v left, reduced %v: digit dots %v", tc.left, tc.reduce, shown) } } }