Files
ZacharyZhang-NY 5605d785de Add shop components 436-450; Odometer exact at 15 fps
Fifteen shop components: PriceTag, Wallet, ShopItem, Cart, Receipt,
Checkout, CurrencyConvert, SaleBadge, Quantity, PricingTiers, Coupon,
DailyReward, BidTicker, TradeWindow and VendingMachine.

Receipt prints dot by dot with feed holes and a torn edge; Coupon cuts
real notches; SaleBadge is a sixteen-point starburst; VendingMachine
sells A1 to D4 by code and drops the snack into its tray. Sums are
checked against overflow, TradeWindow withdraws both confirms on any
change to an offer, and a disabled Checkout veils its inert content.

text.Odometer now counts digits as text, steps at 15 fps as S5 asks,
draws exact digits at rest and lands rolls that floats would stall.
2026-10-12 11:15:07 -04:00

237 lines
7.9 KiB
Go

package shop_test
import (
"image"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/shop"
)
func TestCheckoutEndsInThanks(t *testing.T) {
step, done := 0, 0
tt := view(600, 300, newWorld(), func(c *ui.Context) {
if shop.Checkout(c, "Checkout", &step, shop.CheckoutOptions{}, func() {
pixel.Text(c, []string{"cart", "pay", "confirm"}[step], pixel.TextOptions{})
}).Done {
done++
}
})
click(t, tt, "BACK")
click(t, tt, "NEXT ▶")
click(t, tt, "NEXT ▶")
if step != 2 || !tt.HasText("confirm") {
t.Fatalf("BACK at the start, then NEXT twice: step %d", step)
}
click(t, tt, "PLACE ORDER")
if step != 3 || done != 1 || !tt.HasText("THANK YOU!") || tt.HasText("confirm") {
t.Fatalf("PLACE ORDER: step %d, done %d, texts %q", step, done, tt.Texts())
}
mustPanic(t, "a fifth step", func() {
s := 4
view(600, 300, newWorld(), func(c *ui.Context) { shop.Checkout(c, "C", &s, shop.CheckoutOptions{}, func() {}) })
})
}
func TestCurrencyConvertTradesAtTheRate(t *testing.T) {
gold, gems, got := 350, 1, 0
tt := view(600, 300, newWorld(), func(c *ui.Context) {
got += shop.CurrencyConvert(c, "Swap", &gold, &gems, shop.CurrencyConvertOptions{}).Converted
})
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnd)
click(t, tt, "CONVERT")
if gold != 50 || gems != 4 || got != 3 {
t.Fatalf("End, CONVERT left %d gold, %d gems, converted %d", gold, gems, got)
}
if !tt.HasText("NOT ENOUGH") {
t.Fatalf("50 gold buys no gem: %q", tt.Texts())
}
click(t, tt, "CONVERT")
if gold != 50 {
t.Fatal("CONVERT spent gold it did not have")
}
}
func TestPricingTiersChooseAndIgnoreTurnOffKeys(t *testing.T) {
tiers := [3]shop.Tier{{Price: 0, Perks: []string{"ADS"}}, {Price: 5}, {Price: 9, Perks: []string{"A HAT"}}}
for _, inert := range []bool{false, true} {
chosen, off := -1, false
wd := newWorld()
tt := view(600, 300, wd, func(c *ui.Context) {
shop.PricingTiers(c, "Plans", tiers, &chosen, shop.PricingTiersOptions{Disabled: off && !inert})
})
click(t, tt, "SILVER, 5 GOLD")
if chosen != 1 {
t.Fatalf("clicking SILVER chose %d", chosen)
}
// Space, sent while on, lands as the plans turn off.
tt.Key(0, ui.KeyRight)
off, wd.inert = true, inert
tt.Key(0, ui.KeySpace)
if chosen != 1 {
t.Errorf("inert %v: Space as the plans turned off chose %d", inert, chosen)
}
}
mustPanic(t, "a fourth plan", func() {
ch := 3
view(600, 300, newWorld(), func(c *ui.Context) { shop.PricingTiers(c, "P", tiers, &ch, shop.PricingTiersOptions{}) })
})
}
func TestDailyRewardClaimsToday(t *testing.T) {
marks, claims := []bool{true, false, true, false, false, false, false}, 0
tt := view(700, 300, newWorld(), func(c *ui.Context) {
if shop.DailyReward(c, "Daily", 4, marks, shop.DailyRewardOptions{}).Claimed {
claims++
}
})
click(t, tt, "CLAIM")
click(t, tt, "CLAIM")
if !marks[3] || claims != 1 || !tt.HasText("SEE YOU TOMORROW!") {
t.Fatalf("CLAIM: %v, texts %q", marks, tt.Texts())
}
mustPanic(t, "a claim from the future", func() {
future := []bool{false, false, false, false, true, false, false}
view(700, 300, newWorld(), func(c *ui.Context) { shop.DailyReward(c, "D", 4, future, shop.DailyRewardOptions{}) })
})
}
func TestBidTickerCrownsTheFirstHighest(t *testing.T) {
wd := newWorld()
bids := []shop.Bid{{Bidder: "Aria", Amount: 150, At: wd.clk.t}, {Bidder: "Brom", Amount: 150, At: wd.clk.t}, {Bidder: "Cass", Amount: 90, At: wd.clk.t}}
tt := view(600, 300, wd, func(c *ui.Context) { shop.BidTicker(c, "Bids", bids, shop.BidTickerOptions{}) })
img := tt.Image()
// The crown sits left of the first to bid 150.
for name, want := range map[string]bool{"Aria": true, "Brom": false, "Cass": false} {
x, y, _, _ := dots(find(t, tt, name))
if got := count(img, x-16, y, 12, 12, pixel.Warn) > 0; got != want {
t.Errorf("%s crowned %v, want %v", name, got, want)
}
}
}
func TestTradeWindowNeedsBothAndResetsOnChange(t *testing.T) {
mine := []data.Item{{Sprite: icon.Sword, Name: "Sword"}}
var trade shop.Trade
traded := 0
tt := view(700, 400, newWorld(), func(c *ui.Context) {
if shop.TradeWindow(c, "Trade", "Brom", mine, []data.Item{{Sprite: icon.Gem, Name: "Gem", Count: 3}}, &trade, shop.TradeWindowOptions{}).Traded {
traded++
}
})
click(t, tt, "CONFIRM")
trade.Theirs = true
tt.Frame()
tt.Frame()
if traded != 1 || !tt.HasText("TRADE DONE!") {
t.Fatalf("both confirmed: traded %d, texts %q", traded, tt.Texts())
}
// A new item on my side takes both confirms back.
mine = append(mine, data.Item{Sprite: icon.Potion, Name: "Potion"})
tt.Frame()
if trade.Mine || trade.Theirs {
t.Fatalf("a changed offer kept %+v", trade)
}
mustPanic(t, "nine items", func() {
var tr shop.Trade
view(700, 400, newWorld(), func(c *ui.Context) {
shop.TradeWindow(c, "T", "B", make([]data.Item, 9), nil, &tr, shop.TradeWindowOptions{})
})
})
}
func TestVendingMachineVendsByCode(t *testing.T) {
var slots [16]shop.Snack
slots[6] = shop.Snack{Name: "LEAF", Sprite: icon.Leaf, Price: 30, Stock: 1}
slots[7] = shop.Snack{Name: "DROP", Sprite: icon.Drop, Price: 90, Stock: 0}
credit, vended := 100, -1
tt := view(700, 400, newWorld(), func(c *ui.Context) {
if v := shop.VendingMachine(c, "Machine", slots, &credit, shop.VendingMachineOptions{}).Vended; v >= 0 {
vended = v
}
})
// Keys sit four a row: A-D, 1-4, CLR, OK.
r := find(t, tt, "KEYPAD")
key := func(i int) { tt.ClickAt(r.X+2*float32(i%4*22+11), r.Y+2*float32(i/4*18+9)) }
key(1)
key(6)
key(9)
if vended != 6 || credit != 70 || !tt.HasText("ENJOY!") {
t.Fatalf("B3 OK: vended %d, credit %d, texts %q", vended, credit, tt.Texts())
}
key(1)
key(7)
key(9)
if !tt.HasText("SOLD OUT") {
t.Fatalf("B4 has none: %q", tt.Texts())
}
key(9)
if !tt.HasText("CODE?") {
t.Fatalf("OK with no code: %q", tt.Texts())
}
}
// offPalette counts dots no palette color matches.
func offPalette(img *image.RGBA) int {
b := img.Bounds()
return count(img, 0, 0, b.Dx()/2, b.Dy()/2, none)
}
func TestShopStaysOnThePaletteWhenOff(t *testing.T) {
builds := map[string]func(c *ui.Context, off bool){
"item": func(c *ui.Context, off bool) {
shop.ShopItem(c, shop.Ware{Name: "P", Sprite: icon.Potion, Price: 1}, shop.ShopItemOptions{Disabled: off})
},
"quantity": func(c *ui.Context, off bool) {
n := 1
shop.Quantity(c, "Q", &n, shop.QuantityOptions{Disabled: off})
},
"cart": func(c *ui.Context, off bool) {
shop.Cart(c, "C", []shop.CartLine{{Name: "P", Price: 1, Qty: 1}}, shop.CartOptions{Disabled: off})
},
"checkout": func(c *ui.Context, off bool) {
s := 1
shop.Checkout(c, "C", &s, shop.CheckoutOptions{Disabled: off}, func() {})
},
"convert": func(c *ui.Context, off bool) {
g, m := 500, 0
shop.CurrencyConvert(c, "X", &g, &m, shop.CurrencyConvertOptions{Disabled: off})
},
"tiers": func(c *ui.Context, off bool) {
ch := 0
shop.PricingTiers(c, "T", [3]shop.Tier{}, &ch, shop.PricingTiersOptions{Disabled: off})
},
"coupon": func(c *ui.Context, off bool) {
shop.Coupon(c, "OFF", "X", shop.CouponOptions{Disabled: off})
},
"daily": func(c *ui.Context, off bool) {
shop.DailyReward(c, "D", 2, make([]bool, 7), shop.DailyRewardOptions{Disabled: off})
},
"trade": func(c *ui.Context, off bool) {
var tr shop.Trade
shop.TradeWindow(c, "T", "B", nil, nil, &tr, shop.TradeWindowOptions{Disabled: off})
},
"vending": func(c *ui.Context, off bool) {
cr := 0
shop.VendingMachine(c, "V", [16]shop.Snack{}, &cr, shop.VendingMachineOptions{Disabled: off})
},
}
for name, build := range builds {
for _, inert := range []bool{false, true} {
wd := newWorld()
wd.inert = inert
wd.clk.t = wd.clk.t.Add(time.Second)
tt := view(800, 500, wd, func(c *ui.Context) { build(c, !inert) })
if n := offPalette(tt.Image()); n != 0 {
t.Errorf("%s, inert %v: %d dots off the palette", name, inert, n)
}
}
}
}