Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions games/bcook/blackjack-challenge/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,13 +293,12 @@ func pairsMult(kind string) int {
// seatPairRow below the seat's hand, showing the win label (e.g. "COLORED 8:1")
// or a quiet "pairs lost".
func (rm *room) drawPairsLine(f *kit.Frame, slot int, s *seat, own bool) {
ch := kit.CharacterCell(s.p.Character) // the placing player's face, beside their side bet
if rm.phase == phBetting {
if s.pairsBet > 0 && (s.placed || own) {
// Match the seat's bet line (stDim) rather than the bright own-seat
// cyan — the side stake reads as part of the same quiet bet block,
// not a highlight competing with the active-seat and prompt colours.
centerSlotChar(f, seatCardRow+2, slot, ch, fmt.Sprintf("+pairs %d", s.pairsBet), stDim)
centerSlot(f, seatCardRow+2, slot, fmt.Sprintf("+pairs %d", s.pairsBet), stDim)
}
return
}
Expand All @@ -314,10 +313,14 @@ func (rm *room) drawPairsLine(f *kit.Frame, slot int, s *seat, own bool) {
return
}
if s.pairsKind != "" {
centerSlotChar(f, seatPairRow, slot, ch, fmt.Sprintf("%s %d:1", strings.ToUpper(s.pairsKind), pairsMult(s.pairsKind)), stWin)
centerSlot(f, seatPairRow, slot, fmt.Sprintf("%s %d:1", strings.ToUpper(s.pairsKind), pairsMult(s.pairsKind)), stWin)
return
}
centerSlotChar(f, seatPairRow, slot, ch, "pairs lost", stDim)
// A lost side bet is the seat owner's quiet news, not a table event: only
// the viewer's own seat says "pairs lost" (wins above broadcast to all).
if own {
centerSlot(f, seatPairRow, slot, "pairs lost", stDim)
}
}

// drawBackersLine renders, on a seat's dedicated backers row, a token per player
Expand Down Expand Up @@ -807,21 +810,6 @@ func centerSlot(f *kit.Frame, row, slot int, s string, st kit.Style) {
f.Text(row, slot+(slotW-n)/2, s, st)
}

// centerSlotChar centres "<character tile> <text>" within a slotW-wide column:
// the styled character cell (width 1) plus a space precede the text, tying the
// line to a specific player by face. The text is clamped so the tile + text
// never overflow the slot.
func centerSlotChar(f *kit.Frame, row, slot int, ch kit.Cell, text string, st kit.Style) {
tr := []rune(text)
if len(tr) > slotW-2 {
tr = tr[:slotW-2]
}
w := 2 + len(tr)
col := slot + (slotW-w)/2
f.Set(row, col, ch)
f.Text(row, col+2, string(tr), st)
}

func (rm *room) remaining() int {
if rm.deadline.IsZero() || rm.lastNow.IsZero() {
return 0
Expand Down
52 changes: 42 additions & 10 deletions games/bcook/blackjack-challenge/room_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1380,10 +1380,11 @@ func TestBettingShowsPairsSideBet(t *testing.T) {
}
}

// TestPairsLineCarriesCharacterTile asserts the Star Pairs side-bet line is
// prefixed with the placing player's arcade character tile, so whose side bet is
// whose reads from the face beside it, not just the column.
func TestPairsLineCarriesCharacterTile(t *testing.T) {
// TestPairsLineCarriesNoCharacterTile asserts the Star Pairs side-bet line is
// bare text: it sits directly under its seat's own name row (which already
// carries the tile), so a face there restates the obvious. Tiles remain on
// the backers line, where they identify OTHER players.
func TestPairsLineCarriesNoCharacterTile(t *testing.T) {
a := mkPlayer("alice")
a.Character = kit.Character{Glyph: "λ", InkR: 0x39, InkG: 0xFF, InkB: 0x14, Fallback: 'L'}
rm, tr := newGame(t, a)
Expand All @@ -1394,14 +1395,45 @@ func TestPairsLineCarriesCharacterTile(t *testing.T) {

row := kittest.String(f, seatCardRow+2)
idx := colIndex(row, "+pairs 25")
if idx < 2 {
t.Fatalf("pairs line not found (or no room for a tile) on row %d: %q", seatCardRow+2, row)
if idx < 0 {
t.Fatalf("pairs line not found on row %d: %q", seatCardRow+2, row)
}
if got := f.Cells[seatCardRow+2][idx-2]; got == kit.CharacterCell(a.Character) {
t.Errorf("pairs line still carries the character tile: %+v", got)
}
}

// TestPairsLossIsSelfOnly asserts a lost side bet is the owner's quiet news:
// the "pairs lost" note renders on the viewer's own seat only, while a win
// broadcasts to every viewer.
func TestPairsLossIsSelfOnly(t *testing.T) {
a, b := mkPlayer("alice"), mkPlayer("bob")
rm, tr := newGame(t, a, b)
rm.what = pendNone
rm.OnJoin(tr, a)
rm.OnJoin(tr, b)
sa, sb := rm.seats[a.AccountID], rm.seats[b.AccountID]
// Alice lost her pairs (no pair dealt); bob won his (a paid kind is set).
sa.placed, sa.pairsBet, sa.pairsKind = true, 25, ""
sa.hands = []*phand{{cards: hand{{9, suitHeart}, {5, suitSpade}}, bet: 50}}
sb.placed, sb.pairsBet, sb.pairsKind = true, 25, "mixed"
sb.hands = []*phand{{cards: hand{{8, suitHeart}, {8, suitSpade}}, bet: 50}}
rm.dealer = hand{{10, suitClub}, {7, suitDiamond}}
rm.phase = phTurns
rm.render(tr)

// Alice sees her own loss and bob's win.
rowA := kittest.String(tr.LastFrame(a), seatPairRow)
if !strings.Contains(rowA, "pairs lost") || !strings.Contains(rowA, "MIXED") {
t.Fatalf("owner's view should carry its loss and the win: %q", rowA)
}
if got, want := f.Cells[seatCardRow+2][idx-2], kit.CharacterCell(a.Character); got != want {
t.Errorf("cell before the pairs bet = %+v, want the character tile %+v", got, want)
// Bob sees his win but NOT alice's loss.
rowB := kittest.String(tr.LastFrame(b), seatPairRow)
if strings.Contains(rowB, "pairs lost") {
t.Fatalf("another viewer should not see alice's pairs loss: %q", rowB)
}
if sp := f.Cells[seatCardRow+2][idx-1].Rune; sp != ' ' && sp != 0 {
t.Errorf("no space between the character tile and the pairs bet (got %q)", sp)
if !strings.Contains(rowB, "MIXED") {
t.Fatalf("wins should broadcast to every viewer: %q", rowB)
}
}

Expand Down
66 changes: 43 additions & 23 deletions games/bcook/blackjack/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,12 @@ func (rm *room) drawSeat(f *kit.Frame, slot int, s *seat, v kit.Player, own, act
// Split aces take exactly one card each and stand — so both hands lock the
// moment they're split and the turn passes on. Name the rule beneath them,
// so a locked "can't hit, turn moved on" reads as intended, not broken.
// Otherwise the freed value row keeps a riding insurance stake visible
// for a seat that insured before splitting.
if splitAces(s) {
centerSlot(f, seatValRow, slot, "aces: 1 card", stDim)
} else if tag := rm.insTag(s); tag != "" {
centerSlot(f, seatValRow, slot, strings.TrimSpace(tag), stDim)
}
if rm.phase == phResults && s.result != "" {
centerSlot(f, seatChipRow, slot, s.result, resultStyle(s.result))
Expand All @@ -258,12 +262,20 @@ func (rm *room) drawSeat(f *kit.Frame, slot int, s *seat, v kit.Player, own, act
}
drawCardsAnim(f, seatCardRow, col, h.cards, -1, rm.seatResolver(s.p, hi, h))
col += w + 1
// A surrendered hand reads SURR where its total would sit — the fold
// (half the stake back) must be unmistakable on the felt, not a hand
// that quietly stopped playing.
if h.surrendered {
vals = append(vals, "SURR")
continue
}
vals = append(vals, valueLabel(h.cards, h.fromSplit)+dblTag(h))
}
// During results the value line doubles as the ready indicator: a readied
// seat shows READY where its hand total was, so who's holding up the table
// reads at a glance.
valStr, valSt := strings.Join(vals, " "), valueStyle(s)
// reads at a glance. The insurance tag (ins? / INS) rides the value line
// so a bought stake stays visible through the round.
valStr, valSt := strings.Join(vals, " ")+rm.insTag(s), valueStyle(s)
if rm.phase == phResults && s.ready {
valStr, valSt = "READY", stWin
}
Expand Down Expand Up @@ -298,15 +310,17 @@ func pairsMult(kind string) int {
// ambiguous; it shows only once placed, or for the seat's owner, matching how
// the main bet stays private until placed. Once the cards are dealt it moves to
// seatPairRow below the seat's hand, showing the win label (e.g. "COLORED 12:1")
// or a quiet "pairs lost".
// or, on the viewer's own seat only, a quiet "pairs lost". The line carries no
// character tile: it sits directly under the seat's own name row, so a face
// there restates the obvious (backs on OTHER seats keep tiles — see
// drawBackersLine, where the face is the information).
func (rm *room) drawPairsLine(f *kit.Frame, slot int, s *seat, own bool) {
ch := kit.CharacterCell(s.p.Character) // the placing player's face, beside their side bet
if rm.phase == phBetting {
if s.pairsBet > 0 && (s.placed || own) {
// Match the seat's bet line (stDim) rather than the bright own-seat
// cyan — the side stake reads as part of the same quiet bet block,
// not a highlight competing with the active-seat and prompt colours.
centerSlotChar(f, seatCardRow+2, slot, ch, fmt.Sprintf("+pairs %d", s.pairsBet), stDim)
centerSlot(f, seatCardRow+2, slot, fmt.Sprintf("+pairs %d", s.pairsBet), stDim)
}
return
}
Expand All @@ -321,10 +335,14 @@ func (rm *room) drawPairsLine(f *kit.Frame, slot int, s *seat, own bool) {
return
}
if s.pairsKind != "" {
centerSlotChar(f, seatPairRow, slot, ch, fmt.Sprintf("%s %d:1", strings.ToUpper(s.pairsKind), pairsMult(s.pairsKind)), stWin)
centerSlot(f, seatPairRow, slot, fmt.Sprintf("%s %d:1", strings.ToUpper(s.pairsKind), pairsMult(s.pairsKind)), stWin)
return
}
centerSlotChar(f, seatPairRow, slot, ch, "pairs lost", stDim)
// A lost side bet is the seat owner's quiet news, not a table event: only
// the viewer's own seat says "pairs lost" (wins above broadcast to all).
if own {
centerSlot(f, seatPairRow, slot, "pairs lost", stDim)
}
}

// drawBackersLine renders, on a seat's dedicated backers row, a token per player
Expand Down Expand Up @@ -858,21 +876,6 @@ func centerSlot(f *kit.Frame, row, slot int, s string, st kit.Style) {
f.Text(row, slot+(slotW-n)/2, s, st)
}

// centerSlotChar centres "<character tile> <text>" within a slotW-wide column:
// the styled character cell (width 1) plus a space precede the text, tying the
// line to a specific player by face. The text is clamped so the tile + text
// never overflow the slot.
func centerSlotChar(f *kit.Frame, row, slot int, ch kit.Cell, text string, st kit.Style) {
tr := []rune(text)
if len(tr) > slotW-2 {
tr = tr[:slotW-2]
}
w := 2 + len(tr)
col := slot + (slotW-w)/2
f.Set(row, col, ch)
f.Text(row, col+2, string(tr), st)
}

func (rm *room) remaining() int {
if rm.deadline.IsZero() || rm.lastNow.IsZero() {
return 0
Expand Down Expand Up @@ -926,6 +929,20 @@ func dblTag(h *phand) string {
return ""
}

// insTag marks the seat's insurance state on its value row: " ins?" while the
// offer is open on this seat, " INS" once the stake is riding (kept through
// turns and results, so who insured stays readable), and nothing for a
// declined offer.
func (rm *room) insTag(s *seat) string {
switch {
case s.insurance > 0:
return " INS"
case rm.phase == phInsurance && s.placed && !s.insuranceDecided:
return " ins?"
}
return ""
}

// valueLabel formats a hand's total for the felt. Only a NATURAL two-card 21
// (not one formed by splitting) reads as "BJ"; a split two-card 21 — the kind a
// split ace hitting a ten makes — reads as a plain "21", since it is a plain 21
Expand All @@ -948,6 +965,9 @@ func valueLabel(h hand, fromSplit bool) string {

func valueStyle(s *seat) kit.Style {
for _, h := range s.hands {
if h.surrendered {
return stDim // a folded hand reads quiet, not like a live total
}
if h.cards.isBust() {
return stLose
}
Expand All @@ -965,6 +985,6 @@ func resultStyle(result string) kit.Style {
case strings.HasPrefix(result, "LOSE"), strings.HasPrefix(result, "BUST"):
return stLose
default:
return stDim
return stDim // PUSH, and the quiet SURR fold (half the stake back)
}
}
8 changes: 8 additions & 0 deletions games/bcook/blackjack/room.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"sort"
"strconv"
"time"
Expand Down Expand Up @@ -1209,6 +1210,13 @@ func (rm *room) settle(r kit.Room) {
// (clamped to the payout ceiling), then feed the board on a new peak.
net := rm.settleOpenStake(s)
s.result = resultText(net)
// A surrendered round says so: "SURR -13" explains the odd half-stake
// number where "LOSE -13" would read like a played-out defeat. Only a
// lone hand can surrender, so the seat-level summary is unambiguous
// (side bets still fold into the net, signed either way).
if len(s.hands) == 1 && s.hands[0].surrendered {
s.result = fmt.Sprintf("SURR %+d", net)
}
if s.bal > s.highScore {
s.highScore = s.bal
}
Expand Down
Loading
Loading