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

+127
View File
@@ -0,0 +1,127 @@
package gallery
import (
"fmt"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/tabletop"
)
var parlorEntries = []Entry{
{494, "Domino", "多米诺", "tabletop", "Two halves, zero to six pips. Lay them flat or on end.", demoDomino},
{495, "Roulette", "轮盘", "tabletop", "SPIN and the wheel runs a lap before it stops. The house is green.", demoRoulette},
{496, "CoinFlip", "抛硬币", "tabletop", "Heads or tails, turned in the air frame by frame.", demoCoinFlip},
{497, "BingoCard", "宾果卡", "tabletop", "Mark your numbers; five in a line shouts BINGO!", demoBingoCard},
{498, "TicTacToe", "井字棋", "tabletop", "You are X. The computer is a decent O, not a genius.", demoTicTacToe},
{499, "ScoreTrack", "计分轨道", "tabletop", "A winding track of spaces; each player's meeple marks the score.", demoScoreTrack},
{500, "Meeple", "棋子", "tabletop", "A player's token in its slot. Number five hundred: thank you for scrolling!", demoMeeple},
}
type parlorDemo struct {
spins, result, flips int
heads bool
marked [25]bool
scores []int
turn int
}
// prizes ring the demo wheel; the house takes the first.
var prizes = []string{"HOUSE", "50", "100", "1UP", "200", "LOSE", "500", "x2", "300", "BOMB", "1000", "?"}
func parlorState(c *ui.Context) *parlorDemo {
return ui.Local(c.Root(), demoKey("parlor"), func() parlorDemo {
return parlorDemo{result: 3, heads: true, scores: []int{12, 7, 12}}
})
}
func demoDomino(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
tabletop.Domino(c, 6, 6, tabletop.DominoOptions{})
tabletop.Domino(c, 3, 5, tabletop.DominoOptions{})
tabletop.Domino(c, 0, 1, tabletop.DominoOptions{Vertical: true})
tabletop.Domino(c, 2, 4, tabletop.DominoOptions{Vertical: true})
})
}
func demoRoulette(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := parlorState(c)
rng := tableState(c).rng
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
tabletop.Roulette(c, "Wheel", prizes, s.result, tabletop.RouletteOptions{Cue: s.spins})
if input.Button(c, "SPIN", input.ButtonOptions{Size: input.Small}).Clicked {
s.result, s.spins = rng.IntN(len(prizes)), s.spins+1
log("landed on " + prizes[s.result])
}
})
}
func demoCoinFlip(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := parlorState(c)
rng := tableState(c).rng
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
tabletop.CoinFlip(c, "Coin", s.heads, tabletop.CoinFlipOptions{Cue: s.flips})
if input.Button(c, "FLIP", input.ButtonOptions{Size: input.Small}).Clicked {
s.heads, s.flips = rng.IntN(2) == 0, s.flips+1
log(fmt.Sprintf("heads %v", s.heads))
}
})
}
func demoBingoCard(c *ui.Context, log func(string)) {
s := parlorState(c)
numbers := [25]int{}
for i := range numbers {
numbers[i] = i%5*15 + 1 + (i/5*7)%15
}
r := tabletop.BingoCard(c, "Card", numbers, &s.marked, tabletop.BingoCardOptions{})
if r.Changed {
log("marked")
}
if r.Bingo {
log("BINGO!")
}
}
func demoTicTacToe(c *ui.Context, log func(string)) {
r := tabletop.TicTacToe(c, "Grid", tabletop.TicTacToeOptions{})
if r.Played >= 0 {
log(fmt.Sprintf("X takes %d", r.Played))
}
if r.Over {
log([]string{"a draw", "you won", "the computer won"}[r.Winner])
}
if r.Restarted {
log("a new game")
}
}
func demoScoreTrack(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := parlorState(c)
rng := tableState(c).rng
ui.Column(c).Gap(k.D(6)).Children(func() {
tabletop.ScoreTrack(c, "Scores", s.scores, tabletop.ScoreTrackOptions{})
if input.Button(c, "TAKE A TURN", input.ButtonOptions{Size: input.Small}).Clicked {
gain := rng.IntN(6) + 1
s.scores[s.turn] += gain
log(fmt.Sprintf("player %d +%d", s.turn+1, gain))
s.turn = (s.turn + 1) % len(s.scores)
}
})
}
func demoMeeple(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(4)).Children(func() {
for i := range 6 {
tabletop.Meeple(c, fmt.Sprintf("Player %d", i+1), tabletop.MeepleOptions{Player: i})
}
tabletop.Meeple(c, "Open seat", tabletop.MeepleOptions{Empty: true})
})
}
+167
View File
@@ -0,0 +1,167 @@
package gallery
import (
"fmt"
"math/rand/v2"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/tabletop"
)
var tabletopEntries = []Entry{
{486, "Dice", "骰子", "tabletop", "A d6 with pips. ROLL and it tumbles before it lands.", demoDice},
{487, "DiceTray", "骰盘", "tabletop", "Several dice on felt, and what they add up to.", demoDiceTray},
{488, "PlayingCard", "扑克牌", "tabletop", "Rank and suit, court cards with their pieces, or the back.", demoPlayingCard},
{489, "CardHand", "手牌", "tabletop", "A fanned hand. The card you point at or choose lifts; a click plays it.", demoCardHand},
{490, "Deck", "牌堆", "tabletop", "A face-down pile with its count. Click to draw.", demoDeck},
{491, "Chessboard", "棋盘", "tabletop", "Click a piece, then a square. The rules are on the honor system.", demoChessboard},
{492, "GoBoard", "围棋盘", "tabletop", "Black, then White, a stone a click. 9, 13 or 19 lines.", demoGoBoard},
{493, "MahjongTile", "麻将牌", "tabletop", "Dots, bamboo, characters, the four winds and three dragons.", demoMahjongTile},
}
type tableDemo struct {
rng *rand.Rand
die, rolls int
tray []int
hand []tabletop.Card
left int
drawn tabletop.Card
board [64]tabletop.Piece
stones []tabletop.Stone
}
func tableState(c *ui.Context) *tableDemo {
return ui.Local(c.Root(), demoKey("tabletop"), func() tableDemo {
return tableDemo{rng: rand.New(rand.NewPCG(8, 16)), die: 6, tray: []int{3, 5, 2}, hand: deal(), left: 52,
board: chessStart(), stones: make([]tabletop.Stone, 81)}
})
}
// deal is the demo's five-card hand.
func deal() []tabletop.Card {
return []tabletop.Card{{Rank: 1, Suit: tabletop.Spades}, {Rank: 10, Suit: tabletop.Hearts}, {Rank: 7, Suit: tabletop.Clubs},
{Rank: 12, Suit: tabletop.Diamonds}, {Rank: 13, Suit: tabletop.Hearts}}
}
// chessStart is the opening position, Black at the top.
func chessStart() (b [64]tabletop.Piece) {
back := []tabletop.Kind{tabletop.Rook, tabletop.Knight, tabletop.Bishop, tabletop.Queen, tabletop.King, tabletop.Bishop, tabletop.Knight, tabletop.Rook}
for i, kind := range back {
b[i], b[56+i] = tabletop.Piece{Kind: kind}, tabletop.Piece{Kind: kind, White: true}
b[8+i], b[48+i] = tabletop.Piece{Kind: tabletop.Pawn}, tabletop.Piece{Kind: tabletop.Pawn, White: true}
}
return b
}
func demoDice(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := tableState(c)
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
tabletop.Dice(c, "Die", s.die, tabletop.DiceOptions{Cue: s.rolls})
if input.Button(c, "ROLL", input.ButtonOptions{Size: input.Small}).Clicked {
s.die, s.rolls = s.rng.IntN(6)+1, s.rolls+1
log(fmt.Sprintf("rolled a %d", s.die))
}
})
}
func demoDiceTray(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := tableState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
tabletop.DiceTray(c, "Dice", s.tray, tabletop.DiceTrayOptions{Cue: s.rolls})
if input.Button(c, "ROLL ALL", input.ButtonOptions{Size: input.Small}).Clicked {
total := 0
for i := range s.tray {
s.tray[i] = s.rng.IntN(6) + 1
total += s.tray[i]
}
s.rolls++
log(fmt.Sprintf("rolled %d", total))
}
})
}
func demoPlayingCard(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(4)).Children(func() {
for _, cd := range deal()[:4] {
tabletop.PlayingCard(c, cd, tabletop.PlayingCardOptions{})
}
tabletop.PlayingCard(c, tabletop.Card{Rank: 1}, tabletop.PlayingCardOptions{Back: true})
})
}
func demoCardHand(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := tableState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
if i := tabletop.CardHand(c, "Hand", s.hand, tabletop.CardHandOptions{}).Played; i >= 0 {
log(fmt.Sprintf("played the %d of suit %d", s.hand[i].Rank, s.hand[i].Suit))
s.hand = append(s.hand[:i:i], s.hand[i+1:]...)
}
if input.Button(c, "DEAL", input.ButtonOptions{Size: input.Small}).Clicked {
s.hand = deal()
log("a fresh hand")
}
})
}
func demoDeck(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := tableState(c)
ui.Row(c).Gap(k.D(8)).AlignItems(ui.End).Children(func() {
if tabletop.Deck(c, "Draw pile", s.left, tabletop.DeckOptions{}).Drawn {
s.left--
s.drawn = tabletop.Card{Rank: s.rng.IntN(13) + 1, Suit: tabletop.Suit(s.rng.IntN(4))}
log(fmt.Sprintf("drew a card, %d left", s.left))
}
if s.drawn.Rank > 0 {
tabletop.PlayingCard(c, s.drawn, tabletop.PlayingCardOptions{})
}
})
}
func demoChessboard(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := tableState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
r := tabletop.Chessboard(c, "Board", &s.board, tabletop.ChessboardOptions{})
if r.Changed {
log(fmt.Sprintf("holding square %d", r.Held))
}
if r.Moved {
log(fmt.Sprintf("moved from square %d to %d", r.From, r.To))
}
if input.Button(c, "NEW GAME", input.ButtonOptions{Size: input.Small}).Clicked {
s.board = chessStart()
log("pieces reset")
}
})
}
func demoGoBoard(c *ui.Context, log func(string)) {
s := tableState(c)
if i := tabletop.GoBoard(c, "Goban", s.stones, tabletop.GoBoardOptions{}).Placed; i >= 0 {
log(fmt.Sprintf("stone at %d", i))
}
}
func demoMahjongTile(c *ui.Context, log func(string)) {
k := pixel.K(c)
tiles := []tabletop.Tile{
{Family: tabletop.Dots, Rank: 1}, {Family: tabletop.Dots, Rank: 5}, {Family: tabletop.Dots, Rank: 9},
{Family: tabletop.Bamboo, Rank: 3}, {Family: tabletop.Bamboo, Rank: 8}, {Family: tabletop.Characters, Rank: 7},
{Family: tabletop.Winds, Rank: 1}, {Family: tabletop.Winds, Rank: 4}, {Family: tabletop.Dragons, Rank: 1},
{Family: tabletop.Dragons, Rank: 2}, {Family: tabletop.Dragons, Rank: 3},
}
ui.Row(c).Gap(k.D(2)).Wrap().Width(k.D(300)).Children(func() {
for _, t := range tiles {
tabletop.MahjongTile(c, t, tabletop.MahjongTileOptions{})
}
tabletop.MahjongTile(c, tiles[0], tabletop.MahjongTileOptions{Back: true})
})
}
+1 -1
View File
@@ -56,7 +56,7 @@ var Categories = []Category{
{"tabletop", "Tabletop", "桌游", icon.Crown},
}
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries, projectEntries, teamEntries, shopEntries, marketEntries, agentEntries, guildEntries, deskEntries, retroEntries)
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries, projectEntries, teamEntries, shopEntries, marketEntries, agentEntries, guildEntries, deskEntries, retroEntries, tabletopEntries, parlorEntries)
func Entries() []Entry { return entries }
+45
View File
@@ -0,0 +1,45 @@
package gallery
import (
"strconv"
"strings"
"testing"
)
func TestTabletopDemosLogTheirActions(t *testing.T) {
// Focusing, or a move that ends nothing, logs nothing.
quiet := map[string]bool{"key:Right": true}
for _, tc := range []struct {
path string
steps []string
}{
{"/c/486-dice", []string{"ROLL"}}, {"/c/487-dicetray", []string{"ROLL ALL"}},
{"/c/489-cardhand", []string{"Hand", "DEAL"}}, {"/c/490-deck", []string{"Draw pile"}},
{"/c/491-chessboard", []string{"square:52", "NEW GAME"}}, {"/c/492-goboard", []string{"Goban"}},
{"/c/495-roulette", []string{"SPIN"}}, {"/c/496-coinflip", []string{"FLIP"}},
{"/c/497-bingocard", []string{"Card", "key:Right", "key:Enter"}}, {"/c/498-tictactoe", []string{"Grid"}},
{"/c/499-scoretrack", []string{"TAKE A TURN"}},
} {
a, tt := open(tc.path)
slug := tc.path[3:]
for _, step := range tc.steps {
before := len(a.logs[slug])
if k, ok := strings.CutPrefix(step, "key:"); ok {
tt.Key(0, letters[k])
} else if sq, ok := strings.CutPrefix(step, "square:"); ok {
// A chess square: 16 dots, two pixels a dot.
r, ok := tt.Find("CHESSBOARD")
i, err := strconv.Atoi(sq)
if !ok || err != nil {
t.Fatalf("no board, or square %q: %v", sq, err)
}
tt.ClickAt(r.X+float32(32*(i%8)+16), r.Y+float32(32*(i/8)+16))
} else {
click(t, tt, step)
}
if len(a.logs[slug]) <= before && !quiet[step] && step != "Card" {
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
}
}
}
}