diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino.go | 19 | ||||
| -rw-r--r-- | internal/game/game.go | 8 | ||||
| -rw-r--r-- | internal/game/render_roulette.go | 27 | ||||
| -rw-r--r-- | internal/game/render_roulette_test.go | 5 | ||||
| -rw-r--r-- | internal/game/render_video_poker.go | 14 | ||||
| -rw-r--r-- | internal/game/roulette_animation_test.go | 102 | ||||
| -rw-r--r-- | internal/game/video_poker_alignment_test.go | 35 | ||||
| -rw-r--r-- | internal/game/video_poker_render_test.go | 2 |
8 files changed, 200 insertions, 12 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go index 5e09d48..0b16114 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -539,6 +539,20 @@ func (g *Game) executeTableAction(sess *net.Session, action string) { g.emitCasinoEvents(g.Casino.Action(table.RoomID, table.Config.ID, p.Name, action)) } +// Keep the automatic post-command prompt out of a seated player's spin +// animation. Settlement menus resume prompting once the table leaves playing. +func (g *Game) rouletteSpinning(sess *net.Session) bool { + if g.Casino == nil || sess == nil || sess.Player == nil { + return false + } + table, playing := g.Casino.Participation(sess.Player.Name) + if !playing || table == nil || table.Config.Game != casino.GameRoulette { + return false + } + s, ok := table.Board(sess.Player.Name).(*casino.RouletteSnapshot) + return ok && s.Phase == "playing" +} + func (g *Game) CasinoTick() { if g.Casino != nil { g.emitCasinoEvents(g.Casino.Tick()) @@ -569,7 +583,10 @@ func (g *Game) emitCasinoEvents(events []casino.Event) { message := g.casinoProse(sess, category, g.casinoTimeText(event.Message)) if details { if rendered := g.renderCasinoSnapshot(sess, event.Snapshot); rendered != "" { - message += "\n" + rendered + if message != "" { + message += "\n" + } + message += rendered } } if message != "" { diff --git a/internal/game/game.go b/internal/game/game.go index 3357077..e5192c0 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -573,11 +573,15 @@ func playerDataFileName(relPath string) (name, kind string, ok bool) { } func (g *Game) writePrompt(sess *net.Session) { - sess.WritePrompt(g.promptStr(sess)) + if !g.rouletteSpinning(sess) { + sess.WritePrompt(g.promptStr(sess)) + } } func (g *Game) reprompt(sess *net.Session) { - sess.Reprompt(g.promptStr(sess)) + if !g.rouletteSpinning(sess) { + sess.Reprompt(g.promptStr(sess)) + } } func (g *Game) handleGameCommand(sess *net.Session, input string) { diff --git a/internal/game/render_roulette.go b/internal/game/render_roulette.go index 218a91f..a0f1e04 100644 --- a/internal/game/render_roulette.go +++ b/internal/game/render_roulette.go @@ -16,8 +16,10 @@ import ( func renderRouletteSnapshot(s *casino.RouletteSnapshot, mode string, unicode bool, size int) string { switch s.Phase { case "playing": - // A blank line on either side keeps the wheel from crowding the prose. - return "\n" + renderRouletteWheel(s.Wheel, mode, unicode) + "\n" + if s.SpinFrame == 0 { + return "" + } + return renderRouletteSpin(s, mode, unicode) case "result": return "\n" + renderRouletteResult(s.Pockets, mode, unicode, size) + "\n" default: @@ -27,6 +29,27 @@ func renderRouletteSnapshot(s *casino.RouletteSnapshot, mode string, unicode boo } } +// A scrolling strip works in telnet scrollback without cursor-control escapes. +func renderRouletteSpin(s *casino.RouletteSnapshot, mode string, unicode bool) string { + var line strings.Builder + for i, pocket := range s.SpinPockets { + fg, _ := roulettePocketColor(pocket) + if fg == 0 { + fg = 8 // visible charcoal rather than invisible black on a dark terminal + } + text := "█" + if !unicode { + text = "#" + } + if i == s.BallPosition { + line.WriteString(roulettePaint(mode, ".", fg)) + } else { + line.WriteString(color.Render(mode, color.ColorSpec{Fg: fg, Bg: -1}, text)) + } + } + return line.String() +} + func rouletteInk(mode, text string) string { return color.Render(mode, color.ColorSpec{Fg: 15, Bg: -1}, text) } diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go index 90cee0c..e4fe650 100644 --- a/internal/game/render_roulette_test.go +++ b/internal/game/render_roulette_test.go @@ -25,7 +25,7 @@ func TestRouletteArtModes(t *testing.T) { t.Run(fmt.Sprintf("%s/unicode=%v/double=%v", mode, unicode, double), func(t *testing.T) { wheelOrder := casino.WheelOrder(double) s := &casino.RouletteSnapshot{DoubleZero: double, Wheel: wheelOrder} - for _, phase := range []string{"betting", "playing", "result"} { + for _, phase := range []string{"betting", "result"} { s.Phase = phase s.Pockets = [5]string{"25", "26", "0", "32", "15"} art := renderRouletteSnapshot(s, mode, unicode, 7) @@ -377,7 +377,7 @@ func TestRouletteEventColorsDoNotWrapArt(t *testing.T) { // Use an emphatic blue background to detect whole-message color wrapping. sess.Account = &player.Account{Colors: map[string]string{"casino_action": "0F bg:04 bold"}} g.Casino.Join(1, casino.TableConfig{ID: "r", Game: casino.GameRoulette}, p.Name) - e := casino.Event{RoomID: 1, TableID: "r", Message: "Spinning now", Color: "casino_action", Public: public, Snapshot: &casino.RouletteSnapshot{Phase: "playing", Players: []string{p.Name}}} + e := casino.Event{RoomID: 1, TableID: "r", Message: "Spinning now", Color: "casino_action", Public: public, Snapshot: &casino.RouletteSnapshot{Phase: "playing", SpinFrame: 1, Players: []string{p.Name}}} if !public { e.PrivateTo = p.Name } @@ -575,7 +575,6 @@ func TestRouletteWheelBlackAndSpacing(t *testing.T) { phase string want string }{ - {"playing", renderRouletteWheel(casino.WheelOrder(false), "none", true)}, {"result", renderRouletteResult([5]string{"25", "26", "0", "32", "15"}, "none", true, 7)}, } { s := &casino.RouletteSnapshot{Wheel: casino.WheelOrder(false), Phase: tc.phase, Pockets: [5]string{"25", "26", "0", "32", "15"}} diff --git a/internal/game/render_video_poker.go b/internal/game/render_video_poker.go index 719b36a..045bdc7 100644 --- a/internal/game/render_video_poker.go +++ b/internal/game/render_video_poker.go @@ -161,8 +161,16 @@ func renderVideoPoker(s *casino.VideoPokerSnapshot, mode string, unicode bool, s if i == 0 && len(art) > 0 && decorated { art[0] += " " + badge + bonus } - if len(s.Lines) > 1 && len(art) > 0 { - art[0] = fmt.Sprintf("%3d: ", i+1) + art[0] + indent := "" + if len(s.Lines) > 1 { + indent = " " + for row := range art { + prefix := indent + if row == 0 { + prefix = fmt.Sprintf("%3d: ", i+1) + } + art[row] = prefix + art[row] + } } lines = append(lines, art...) text := "No win" @@ -178,7 +186,7 @@ func renderVideoPoker(s *casino.VideoPokerSnapshot, mode string, unicode bool, s text += fmt.Sprintf(" | earns %dx", s.Next[i]) } // Results always follow the cards, including compact hundred-play. - lines = append(lines, color.Render(mode, color.ColorSpec{Fg: fg, Bg: -1, Bold: true}, text)) + lines = append(lines, indent+color.Render(mode, color.ColorSpec{Fg: fg, Bg: -1, Bold: true}, text)) } if s.Result != "" { fg := pokerYellow diff --git a/internal/game/roulette_animation_test.go b/internal/game/roulette_animation_test.go new file mode 100644 index 0000000..96f14bb --- /dev/null +++ b/internal/game/roulette_animation_test.go @@ -0,0 +1,102 @@ +package game + +import ( + "strings" + "testing" + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" +) + +func TestRouletteSpinOutputHasNoPromptOrBlankLines(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + g.Hub = net.NewHub() + g.Hub.EnterRoom(sess, 1) + sess.Player.Options = map[string]any{"color": "none", "unicode": true} + g.emitCasinoEvents(g.Casino.Join(1, casino.TableConfig{ID: "roulette", Game: casino.GameRoulette}, sess.Player.Name)) + sess.ClearPrompt() + conn.data = nil + g.executeBet(sess, []string{"2", "on", "2"}, "bet 2 on 2") + g.writePrompt(sess) // the command queue's post-command prompt + text := strings.ReplaceAll(string(conn.data), "\r\n", "\n") + if !strings.HasSuffix(text, "The roulette wheel spins...\n") { + t.Fatalf("prompt after spin announcement: %q", text) + } + before := len(conn.data) + g.reprompt(sess) + if len(conn.data) != before { + t.Fatal("reprompt interrupted animation") + } + for tick := 1; tick <= 5; tick++ { + conn.data = nil + g.CasinoTick() + text = strings.ReplaceAll(string(conn.data), "\r\n", "\n") + if strings.Count(text, "\n") != 1 || !strings.HasSuffix(text, "\n") || color.VisibleLen(strings.TrimSuffix(text, "\n")) != 10 { + t.Fatalf("tick %d: expected exactly one strip: %q", tick, text) + } + if tick == 5 && strings.Count(text, ".") != 1 { + t.Fatal("final strip missing ball") + } + } + for tick := 6; tick <= 7; tick++ { + conn.data = nil + g.CasinoTick() + if len(conn.data) != 0 { + t.Fatalf("tick %d: expected silence: %q", tick, conn.data) + } + } + g.CasinoTick() + if !strings.Contains(string(conn.data), "The ball lands in") || g.rouletteSpinning(sess) { + t.Fatal("result did not end animation") + } + sess.ClearPrompt() + conn.data = nil + g.writePrompt(sess) + if len(conn.data) == 0 { + t.Fatal("prompt did not resume") + } +} + +func TestRouletteSpinStrip(t *testing.T) { + for _, mode := range []string{"none", "ansi", "xterm256"} { + for _, unicode := range []bool{false, true} { + for _, ball := range []int{-1, 5} { + s := &casino.RouletteSnapshot{Phase: "playing", SpinFrame: 1, BallPosition: ball, SpinPockets: [10]string{"1", "2", "1", "2", "1", "26", "0", "32", "15", "19"}} + art := renderRouletteSnapshot(s, mode, unicode, 7) + plain := plainRoulette(art) + dots := 0 + if ball >= 0 { + dots = 1 + } + if color.VisibleLen(art) != 10 || strings.Count(plain, ".") != dots || strings.Contains(plain, "\n") { + t.Fatalf("bad strip: %q", art) + } + block := "█" + if !unicode { + block = "#" + } + if strings.Count(plain, block) != 10-dots { + t.Fatalf("bad blocks: %q", plain) + } + if mode != "none" && ball >= 0 && !strings.Contains(art, roulettePaint(mode, ".", 8)) { + t.Fatalf("26 ball is not black: %q", art) + } + } + for _, pocket := range []string{"0", "00", "26", "32"} { + s := &casino.RouletteSnapshot{Phase: "playing", SpinFrame: 5, BallPosition: 4} + s.SpinPockets[4] = pocket + art := renderRouletteSnapshot(s, mode, unicode, 7) + fg, _ := roulettePocketColor(pocket) + if fg == 0 { + fg = 8 + } + if !strings.Contains(art, roulettePaint(mode, ".", fg)) { + t.Fatalf("wrong ball color for %s: %q", pocket, art) + } + } + } + } + if got := renderRouletteSnapshot(&casino.RouletteSnapshot{Phase: "playing"}, "none", true, 7); got != "" { + t.Fatalf("old wheel still shown: %q", got) + } +} diff --git a/internal/game/video_poker_alignment_test.go b/internal/game/video_poker_alignment_test.go new file mode 100644 index 0000000..0163fac --- /dev/null +++ b/internal/game/video_poker_alignment_test.go @@ -0,0 +1,35 @@ +package game + +import ( + "fmt" + "strings" + "testing" + + "thehouseoficarus/internal/casino" +) + +func TestPokerMultiHandCardRowsAlignWithLabels(t *testing.T) { + cards := []casino.CardView{{Rank: "A", Suit: "hearts"}, {Rank: "K", Suit: "spades"}, {Rank: "Q", Suit: "diamonds"}, {Rank: "J", Suit: "clubs"}, {Rank: "10", Suit: "hearts"}} + for _, size := range []int{1, 3, 5} { + t.Run(fmt.Sprint(size), func(t *testing.T) { + hand := casino.PokerLine{Cards: cards, Category: "Straight", Coins: 20, Multiplier: 1} + snap := &casino.VideoPokerSnapshot{Config: (casino.VideoPokerConfig{Cabinet: casino.CabinetClassic}).Normalize(), Hands: 3, Lines: []casino.PokerLine{hand, hand, hand}} + out := renderVideoPoker(snap, "none", true, size, "", nil) + art := renderPokerCards(cards, nil, "none", true, min(5, cardRenderSize(true, size)), "", nil) + for n := 1; n <= 3; n++ { + rows := append([]string(nil), art...) + for i := range rows { + prefix := " " + if i == 0 { + prefix = fmt.Sprintf("%3d: ", n) + } + rows[i] = prefix + rows[i] + } + want := strings.Join(rows, "\n") + "\n Straight" + if !strings.Contains(out, want) { + t.Fatalf("hand %d is misaligned:\n%s", n, out) + } + } + }) + } +} diff --git a/internal/game/video_poker_render_test.go b/internal/game/video_poker_render_test.go index 88c8474..1f76d60 100644 --- a/internal/game/video_poker_render_test.go +++ b/internal/game/video_poker_render_test.go @@ -100,7 +100,7 @@ func TestPokerResultsBelowCardsAndHoldStyle(t *testing.T) { } result := strings.Index(plain, text) // The result starts its own line and follows the card/badge line. - if result < 0 || result == 0 || plain[result-1] != '\n' { + if result <= 0 || strings.TrimSpace(plain[strings.LastIndex(plain[:result], "\n")+1:result]) != "" { t.Fatalf("result not below cards: %s", plain) } if hands == 100 && !strings.Contains(plain, "♥") { |
