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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 14:03:58 -04:00
1 parent 7800003418
commit 81003b21ee
32 files changed
+2781 -1

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
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)
}
}