Files
ZacharyZhang-NY 81003b21ee Add tabletop components 486-500 and ten game icons
Fifteen pieces of a games night: Dice, DiceTray, PlayingCard, CardHand,
Deck, Chessboard, GoBoard, MahjongTile, Domino, Roulette, CoinFlip,
BingoCard, TicTacToe, ScoreTrack and Meeple. Component number 500 is
a meeple, which seems right.

Dice tumble, coins turn and wheels spin on a cue, a frame at a time,
and land at once under Reduce Motion. Hands lift the card you point
at and play the one drawn on top; pieces and stones pick the lighter
of Lit and Ink for White, so they read in every palette; the computer
plays a fair O; FREE is a star in every language.

New 12x12 icons: Spade, Club, Diamond, Meeple, King, Queen, Rook,
Bishop, Knight and Pawn.
2026-10-12 14:03:58 -04:00

195 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tabletop_test
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/tabletop"
)
func TestDiceTumbleThenLand(t *testing.T) {
for _, reduce := range []bool{false, true} {
wd := newWorld()
wd.reduce = reduce
value, cue := 3, 0
tt := view(200, 200, wd, func(c *ui.Context) { tabletop.Dice(c, "Die", value, tabletop.DiceOptions{Cue: cue}) })
landed := func(v int) []byte {
return view(200, 200, newWorld(), func(c *ui.Context) { tabletop.Dice(c, "Die", v, tabletop.DiceOptions{}) }).Image().Pix
}
if !bytes.Equal(tt.Image().Pix, landed(3)) {
t.Fatal("a die shown first tumbled")
}
value, cue = 5, 1
tt.Frame()
if tumbling := !bytes.Equal(tt.Image().Pix, landed(5)); tumbling == reduce {
t.Errorf("reduce %v: tumbling is %v", reduce, tumbling)
}
later(tt, wd, time.Second)
if !bytes.Equal(tt.Image().Pix, landed(5)) {
t.Errorf("reduce %v: it never landed on 5", reduce)
}
}
}
func TestDiceTrayAddsUp(t *testing.T) {
tt := view(400, 200, newWorld(), func(c *ui.Context) { tabletop.DiceTray(c, "Dice", []int{6, 6, 1}, tabletop.DiceTrayOptions{}) })
find(t, tt, "TOTAL 13")
find(t, tt, "Dice 3")
}
func TestCardsReadAndInk(t *testing.T) {
tt := view(400, 400, newWorld(), func(c *ui.Context) {
tabletop.PlayingCard(c, tabletop.Card{Rank: 1, Suit: tabletop.Spades}, tabletop.PlayingCardOptions{})
tabletop.PlayingCard(c, tabletop.Card{Rank: 12, Suit: tabletop.Hearts}, tabletop.PlayingCardOptions{})
tabletop.PlayingCard(c, tabletop.Card{Rank: 9, Suit: tabletop.Clubs}, tabletop.PlayingCardOptions{Back: true})
})
img := tt.Image()
x, y, w, h := dots(find(t, tt, "ACE OF SPADES"))
if count(img, x, y, w, h, pixel.Bad) > 0 {
t.Error("a spade is red")
}
x, y, w, h = dots(find(t, tt, "QUEEN OF HEARTS"))
if count(img, x, y, w, h, pixel.Bad) == 0 {
t.Error("a heart is not red")
}
x, y, w, h = dots(find(t, tt, "CARD BACK"))
if count(img, x, y, w, h, pixel.Accent) == 0 {
t.Error("the back shows no pattern")
}
}
func TestCardHandLiftsAndPlays(t *testing.T) {
cards := []tabletop.Card{{Rank: 2, Suit: tabletop.Clubs}, {Rank: 3, Suit: tabletop.Clubs}, {Rank: 4, Suit: tabletop.Clubs}}
wd, played, off := newWorld(), -1, false
tt := view(400, 300, wd, func(c *ui.Context) {
if i := tabletop.CardHand(c, "Hand", cards, tabletop.CardHandOptions{Disabled: off}).Played; i >= 0 {
played = i
}
})
x, y, _, _ := dots(find(t, tt, "Hand"))
// top is the first row of a card's outline.
top := func(i int) int {
for j := y; j < y+60; j++ {
if at(tt.Image(), x+i*18+4, j) == pixel.Edge {
return j - y
}
}
return -1
}
rest := top(1)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyRight)
later(tt, wd, time.Second)
if top(1) != rest-6 || top(0) != rest {
t.Fatalf("the chosen card rose to %d from %d", top(1), rest)
}
tt.Key(0, ui.KeyEnter)
if played != 1 {
t.Fatalf("Enter played %d", played)
}
r := find(t, tt, "Hand")
tt.ClickAt(r.X+r.W-4, r.Y+r.H/2)
if played != 2 {
t.Fatalf("a click on the last card played %d", played)
}
off, played = true, -1
tt.Key(0, ui.KeyEnter)
if played != -1 {
t.Fatal("a disabled hand played")
}
}
func TestDeckDrawsUntilEmpty(t *testing.T) {
left, drawn := 2, 0
tt := view(300, 300, newWorld(), func(c *ui.Context) {
if tabletop.Deck(c, "Deck", left, tabletop.DeckOptions{}).Drawn {
left--
drawn++
}
})
// The count's badge is the only AccentInk on the pile.
x, y, w, h := dots(find(t, tt, "Deck"))
if count(tt.Image(), x, y, w, h, pixel.AccentInk) == 0 {
t.Fatal("no count badge")
}
click(t, tt, "Deck")
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeyEnter)
click(t, tt, "Deck")
if left != 0 || drawn != 2 {
t.Fatalf("drew %d, %d left", drawn, left)
}
}
func TestDominoAndMahjongRead(t *testing.T) {
tt := view(600, 600, newWorld(), func(c *ui.Context) {
tabletop.Domino(c, 3, 5, tabletop.DominoOptions{Vertical: true})
tabletop.MahjongTile(c, tabletop.Tile{Family: tabletop.Dots, Rank: 3}, tabletop.MahjongTileOptions{})
tabletop.MahjongTile(c, tabletop.Tile{Family: tabletop.Winds, Rank: 1}, tabletop.MahjongTileOptions{})
tabletop.MahjongTile(c, tabletop.Tile{Family: tabletop.Dragons, Rank: 1}, tabletop.MahjongTileOptions{})
tabletop.MahjongTile(c, tabletop.Tile{Family: tabletop.Bamboo, Rank: 2}, tabletop.MahjongTileOptions{Back: true})
})
if _, _, w, h := dots(find(t, tt, "3 | 5")); w != 20 || h != 39 {
t.Fatalf("a standing domino is %d×%d", w, h)
}
for _, label := range []string{"3 DOTS", "EAST WIND", "TILE BACK"} {
find(t, tt, label)
}
x, y, w, h := dots(find(t, tt, "RED DRAGON"))
if count(tt.Image(), x, y, w, h, pixel.Bad) == 0 {
t.Fatal("the red dragon is not red")
}
}
func TestDiceShowPips(t *testing.T) {
for v := 2; v <= 6; v++ {
tt := view(200, 200, newWorld(), func(c *ui.Context) { tabletop.Dice(c, "Die", v, tabletop.DiceOptions{}) })
x, y, w, h := dots(find(t, tt, "Die"))
// Each pip is two dots square, in Ink.
if n := count(tt.Image(), x, y, w, h, pixel.Ink); n != 4*v {
t.Errorf("a %d shows %d Ink dots", v, n)
}
}
}
func TestWhitePiecesLookLight(t *testing.T) {
var board [64]tabletop.Piece
board[1] = tabletop.Piece{Kind: tabletop.Pawn, White: true}
board[3] = tabletop.Piece{Kind: tabletop.Pawn}
tt := view(600, 400, newWorld(), func(c *ui.Context) { tabletop.Chessboard(c, "B", &board, tabletop.ChessboardOptions{}) })
x, y, _, _ := dots(find(t, tt, "CHESSBOARD"))
// On dark squares, a white pawn takes the lighter slot.
white, black := count(tt.Image(), x+16, y, 16, 16, pixel.Lit), count(tt.Image(), x+48, y, 16, 16, pixel.Lit)
if white <= black {
t.Fatalf("the white pawn has %d Lit dots, the black %d", white, black)
}
}
func TestEveryTileAndDominoLooksItsOwn(t *testing.T) {
seen := map[string]tabletop.Tile{}
for f, ranks := range []int{9, 9, 9, 4, 3} {
for r := 1; r <= ranks; r++ {
tile := tabletop.Tile{Family: tabletop.Family(f), Rank: r}
pix := string(view(100, 100, newWorld(), func(c *ui.Context) { tabletop.MahjongTile(c, tile, tabletop.MahjongTileOptions{}) }).Image().Pix)
if other, ok := seen[pix]; ok {
t.Errorf("%+v looks like %+v", tile, other)
}
seen[pix] = tile
}
}
ink := func(a, b int) int {
tt := view(200, 100, newWorld(), func(c *ui.Context) { tabletop.Domino(c, a, b, tabletop.DominoOptions{}) })
x, y, w, h := dots(find(t, tt, fmt.Sprintf("%d | %d", a, b)))
return count(tt.Image(), x, y, w, h, pixel.Ink)
}
// Two dots square a pip, as on dice.
if blank, two, six := ink(0, 0), ink(1, 1), ink(6, 6); blank != 0 || two != 8 || six != 48 {
t.Errorf("0|0, 1|1 and 6|6 show %d, %d and %d Ink dots", blank, two, six)
}
}