aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go622
1 files changed, 11 insertions, 611 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 87b7176..a0cc675 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -1,211 +1,15 @@
package game
import (
- "errors"
"fmt"
- "os"
"strconv"
"strings"
- "unicode/utf8"
- "thehouseoficarus/internal/cardart"
"thehouseoficarus/internal/casino"
- "thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/persistence"
"thehouseoficarus/internal/player"
)
-type casinoWallet struct {
- g *Game
- p *player.Player
- // Optional per-wallet persistence seam for fault-injection tests.
- saveCharacter func(*player.Player) error
-}
-
-// save treats a completed replacement as applied, even if its durability is
-// uncertain. Reverting memory then would disagree with the visible file and
-// could overwrite a paid win or resurrect a resolved stake on the next save.
-// Report the storage fault, but do not tell the game to retry/refund an
-// already-applied mutation. Only pre-replacement failures require rollback.
-func (w casinoWallet) save(operation string) error {
- save := w.saveCharacter
- if save == nil {
- save = w.g.AccountStore.SaveCharacter
- }
- err := save(w.p)
- if errors.Is(err, persistence.ErrDurabilityUncertain) {
- fmt.Fprintf(os.Stderr, "casino %s: %s save durability uncertain: %v — keeping applied state\n", operation, w.p.Name, err)
- return nil
- }
- return err
-}
-
-// Wager debits the stake and records it in PendingWagers in the SAME atomic
-// character save. The entry stays until the round resolves: a settlement
-// removes the stakes it returns (PayCredits) or consumes them
-// (ResolveStake/ResolveStakes), and startup refunds anything still listed
-// (see recoverPendingWagers). A pre-replacement save failure reverts the
-// debit and ledger entry; an uncertain replacement keeps both (see save).
-func (w casinoWallet) Wager(amount int) (casino.Wager, error) {
- if amount <= 0 {
- return casino.Wager{}, casino.ErrInsufficientFunds
- }
- creditsBefore := w.p.Credits
- chips := countChips(w.p)
- if chips > amount {
- chips = amount
- }
- credits := amount - chips
- if creditsBefore < credits {
- return casino.Wager{}, casino.ErrInsufficientFunds
- }
- if chips > 0 {
- w.p.RemoveItem("chips", chips)
- }
- w.p.Credits -= credits
- pending := player.PendingWager{ID: newPendingWagerID(), Chips: chips, Credits: credits}
- w.p.PendingWagers = append(w.p.PendingWagers, pending)
- if err := w.save("wager"); err != nil {
- // Revert the in-memory mutation: the play must not proceed on a
- // stake that could not be made durable.
- if chips > 0 {
- if !w.g.giveChips(w.p, chips) {
- // Cannot happen (the debit just freed or reduced the chips
- // stack), but surface it instead of silently losing chips.
- fmt.Fprintf(os.Stderr, "casino wager revert: no inventory room for %d chips\n", chips)
- }
- }
- w.p.Credits = creditsBefore
- removePendingWager(w.p, pending.ID)
- return casino.Wager{}, casino.ErrWalletPersistence
- }
- return casino.Wager{Total: amount, Chips: chips, Credits: credits, ID: pending.ID}, nil
-}
-
-// PayCredits banks a payout durably and, in the SAME save, resolves the
-// listed stakes. stakeIDs may be empty for a payout that returns no stake (a
-// free-spin bank already refunded by RefundOutstanding). A table settlement
-// passes the hand's stake IDs so the credit and the stake return are one
-// atomic step; a slot win passes its triggering stake for the same reason.
-// Either way a crash can never pay the payout AND refund the same stake. A
-// pre-replacement failure reverts both the credit and the ledger and reports
-// failure. An uncertain replacement is applied and logged (see save).
-func (w casinoWallet) PayCredits(amount int, stakeIDs ...string) error {
- return w.payCredits(amount, stakeIDs)
-}
-
-// payCredits credits the payout and, when stakeIDs is non-empty, removes
-// those stakes from the pending ledger in the same atomic save. On a
-// pre-replacement failure the credit and the ledger are reverted together.
-func (w casinoWallet) payCredits(amount int, stakeIDs []string) error {
- if amount <= 0 && len(stakeIDs) == 0 {
- return nil
- }
- creditsBefore := w.p.Credits
- sum, capErr := addCreditsChecked(creditsBefore, amount)
- if capErr != nil {
- return capErr
- }
- var removed []player.PendingWager
- if len(stakeIDs) > 0 {
- removed = takePendingWagers(w.p, stakeIDs)
- }
- w.p.Credits = sum
- if err := w.save("payout"); err != nil {
- w.p.Credits = creditsBefore
- w.p.PendingWagers = append(w.p.PendingWagers, removed...)
- return casino.ErrWalletPersistence
- }
- return nil
-}
-
-// ResolveStake consumes ONE pending stake by wallet handle: its hand lost or
-// was paid by a cover, so it must never be refunded. A settlement that
-// already removed it (PayCredits) makes this a no-op, so no redundant
-// save is paid.
-func (w casinoWallet) ResolveStake(stakeID string) {
- if stakeID == "" {
- return
- }
- removed := takePendingWagers(w.p, []string{stakeID})
- if len(removed) == 0 {
- return
- }
- if err := w.save("resolve stake"); err != nil {
- // The durable file still lists the stake, so restore it in memory to
- // keep the two records consistent for a later retry/recovery.
- w.p.PendingWagers = append(w.p.PendingWagers, removed...)
- fmt.Fprintf(os.Stderr, "casino resolve stake: save for %s: %v (ledger restored in memory)\n", w.p.Name, err)
- }
-}
-
-// ResolveStakesByIDs consumes the listed pending stakes in one durable save.
-// A losing split/doubled hand used to call ResolveStake once per handle, each
-// with its own full character save; batching removes that repeated fsync and
-// the crash window between saves.
-func (w casinoWallet) ResolveStakesByIDs(stakeIDs ...string) {
- if len(stakeIDs) == 0 {
- return
- }
- removed := takePendingWagers(w.p, stakeIDs)
- if len(removed) == 0 {
- return
- }
- if err := w.save("resolve stakes"); err != nil {
- // The durable file still lists the stakes, so restore them in memory to
- // keep the two records consistent for a later retry/recovery.
- w.p.PendingWagers = append(w.p.PendingWagers, removed...)
- fmt.Fprintf(os.Stderr, "casino resolve stakes: save for %s: %v (ledger restored in memory)\n", w.p.Name, err)
- }
-}
-
-// RefundOutstanding returns every pending stake to the player — chips in
-// kind, credits as credits — and clears the ledger in one save. It is used
-// by graceful shutdown/eviction and by a pre-deal disconnect. A pre-replacement
-// failure reverts the return and ledger for retry; an uncertain replacement
-// keeps both applied (see save).
-func (w casinoWallet) RefundOutstanding() (chips, credits int, err error) {
- before := append([]player.PendingWager(nil), w.p.PendingWagers...)
- for _, pending := range before {
- chips += pending.Chips
- credits += pending.Credits
- }
- if chips == 0 && credits == 0 {
- return 0, 0, nil
- }
- creditsBefore := w.p.Credits
- w.p.PendingWagers = nil
- if chips > 0 {
- if !w.g.giveChips(w.p, chips) {
- w.p.PendingWagers = before
- return 0, 0, casino.ErrWalletPersistence
- }
- }
- sum, capErr := addCreditsChecked(creditsBefore, credits)
- if capErr != nil {
- if chips > 0 {
- w.p.RemoveItem("chips", chips)
- }
- w.p.PendingWagers = before
- return 0, 0, casino.ErrWalletPersistence
- }
- w.p.Credits = sum
- if err := w.save("refund"); err != nil {
- w.p.Credits = creditsBefore
- if chips > 0 {
- w.p.RemoveItem("chips", chips)
- }
- w.p.PendingWagers = before
- return 0, 0, casino.ErrWalletPersistence
- }
- return chips, credits, nil
-}
-
-// casinoAffordable is the most a player can wager: chips are spent first,
-// then credits.
-func casinoAffordable(p *player.Player) int { return p.Credits + countChips(p) }
-
// leaveCasinoGames departs every casino game the player participates in
// using the graceful leave policy each game defines for player-initiated
// exits (the same path the "stop" command takes): a table with a wager
@@ -349,18 +153,13 @@ func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) {
g.showCasinoChoices(sess, choices)
return
}
- if len(args) > 1 {
- amount, err := strconv.Atoi(args[1])
- if !choice.table || len(args) > 3 || (args[1] != "min" && args[1] != "max" && (err != nil || amount <= 0)) {
- sess.WriteLine("Use play GAME [N|min|max] [on BET]. No wager was placed.")
- return
- }
- }
g.startCasinoGame(sess, p, choice.address)
if len(args) > 1 {
- if t, joined := g.Casino.Participation(p.Name); joined && t != nil {
- g.executeBet(sess, args[1:], rawInput)
- }
+ // Let executeBet parse the wager/target tokens. Pre-validating here
+ // rejected the advertised "play GAME N on BET" form (len > 3) and any
+ // machine amount (choice.table is false for machines); executeBet's own
+ // parser already produces precise errors for each game's syntax.
+ g.executeBet(sess, args[1:], rawInput)
}
}
@@ -667,8 +466,8 @@ func (g *Game) emitCasinoEvents(events []casino.Event) {
// casinoMenuParts resolves a structured menu into the shared header/balance
// heading/commands used by both menu render paths: the balance line, the
-// heading (falling back to the event message), and a copy of the commands
-// annotated with an unavailability note for anything the player can't afford.
+// heading (falling back to the event message), and a copy of the commands.
+// Unaffordable commands are filtered at render time by casinoCommandColumns.
func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message string) (balance, heading string, commands []casino.MenuCommand) {
balance = ""
if menu.ShowBalance {
@@ -682,11 +481,9 @@ func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message
if !g.casinoHintsVisible(sess, menu) {
commands = nil
}
- for i := range commands {
- if commands[i].Cost > casinoAffordable(sess.Player) {
- commands[i].Desc += " [unavailable: insufficient funds]"
- }
- }
+ // Unaffordable commands are hidden by casinoCommandColumns; an
+ // "unavailable: insufficient funds" annotation here would be dead, since
+ // the renderer drops exactly those commands with the same test.
return balance, heading, commands
}
@@ -726,7 +523,7 @@ func (g *Game) casinoBoardReminder(sess *net.Session, category string, menu *cas
if !isCashHunt && !isPlinko && !isPoker && !isCraps && !isPaiGow && !isBigSix && (!isRoulette || s.Phase == "playing" || s.Phase == "result") {
return "", false
}
- if !isCashHunt && !isPlinko && !isPoker && !isCraps && !isPaiGow && !isBigSix && sess.Player.OptionString("casino_display") == "compact" && (menu.Repeat || !menu.ShowBalance) {
+ if isRoulette && sess.Player.OptionString("casino_display") == "compact" && (menu.Repeat || !menu.ShowBalance) {
// The compact reminder is already a short block; leave it intact.
return "", false
}
@@ -881,400 +678,3 @@ func (g *Game) casinoMenuHeader(sess *net.Session, balance, heading string) stri
func (g *Game) casinoMenuCommands(sess *net.Session, commands []casino.MenuCommand) string {
return g.casinoCommandColumns(sess, commands)
}
-
-func renderBlackjackSnapshotFor(snapshot *casino.BlackjackSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store, viewer string) string {
- if snapshot == nil {
- return ""
- }
- size = cardRenderSize(unicode, size)
- var lines []string
- dealerTitle := "Dealer:"
- if snapshot.DealerRevealed {
- dealerTitle = "Dealer (" + snapshot.DealerScore + "):"
- } else if snapshot.DealerShownScore != "" {
- dealerTitle = "Dealer (" + snapshot.DealerShownScore + "):"
- }
- if size == 1 {
- lines = append(lines, cardLine(mode, dealerTitle)+" "+strings.Join(cardsLine(mode, snapshot.Dealer, size, unicode, style, art), " "))
- } else {
- lines = append(lines, cardLine(mode, dealerTitle))
- lines = append(lines, cardsLine(mode, snapshot.Dealer, size, unicode, style, art)...)
- }
- for _, player := range snapshot.Players {
- name := player.Name
- if name == viewer {
- name = "You"
- }
- marker := " "
- if player.Active {
- marker = "> "
- }
- detail := ""
- if player.Hand > 0 {
- detail = fmt.Sprintf(" | Hand %d/%d | Wager %d | %s", player.Hand, player.Hands, player.Bet, player.Status)
- }
- if player.Active && player.Name != viewer {
- detail = strings.ReplaceAll(detail, "Your turn", "Playing")
- }
- title := fmt.Sprintf("%s%s (%s)%s:", marker, name, player.Score, detail)
- if size == 1 {
- lines = append(lines, cardLine(mode, title)+" "+strings.Join(cardsLine(mode, player.Cards, size, unicode, style, art), " "))
- } else {
- lines = append(lines, cardLine(mode, title))
- lines = append(lines, cardsLine(mode, player.Cards, size, unicode, style, art)...)
- }
- }
- return strings.Join(lines, "\n")
-}
-
-// renderBaccaratSnapshot draws the player and banker hands followed by the
-// list of wagers riding on the round.
-func renderBaccaratSnapshot(snapshot *casino.BaccaratSnapshot, mode string, unicode bool, size int, style string, art *cardart.Store) string {
- if snapshot == nil {
- return ""
- }
- size = cardRenderSize(unicode, size)
- var lines []string
- if size == 1 {
- lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):")+" "+strings.Join(cardsLine(mode, snapshot.Player, size, unicode, style, art), " "), cardLine(mode, "Banker ("+snapshot.BankerScore+"):")+" "+strings.Join(cardsLine(mode, snapshot.Banker, size, unicode, style, art), " "))
- } else {
- lines = append(lines, cardLine(mode, "Player ("+snapshot.PlayerScore+"):"))
- lines = append(lines, cardsLine(mode, snapshot.Player, size, unicode, style, art)...)
- lines = append(lines, cardLine(mode, "Banker ("+snapshot.BankerScore+"):"))
- lines = append(lines, cardsLine(mode, snapshot.Banker, size, unicode, style, art)...)
- }
- for _, bet := range snapshot.Bets {
- lines = append(lines, cardLine(mode, fmt.Sprintf("%s bets %d on %s.", bet.Name, bet.Amount, bet.Spot)))
- }
- return strings.Join(lines, "\n")
-}
-
-func cardLine(mode, text string) string {
- spec := color.NoColor()
- spec.Fg = 39
- return color.Render(mode, spec, text)
-}
-
-func cardsLine(mode string, cards []casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string {
- if len(cards) == 0 {
- return nil
- }
- if size == 1 {
- parts := make([]string, len(cards))
- for i, card := range cards {
- if !unicode {
- if card.Hidden {
- parts[i] = "[??]"
- } else {
- suit := map[string]string{"hearts": "H", "diamonds": "D", "spades": "S", "clubs": "C"}[card.Suit]
- parts[i] = card.Rank + suit
- }
- } else {
- parts[i] = colorizeCardSymbol(mode, card)
- }
- }
- return []string{strings.Join(parts, " ")}
- }
- artLines := make([][]string, len(cards))
- for i, card := range cards {
- artLines[i] = cardArtLines(card, size, unicode, style, art)
- }
- lines := make([]string, size)
- for row := 0; row < size; row++ {
- var lineParts []string
- for i, card := range artLines {
- lineParts = append(lineParts, colorizeCardLine(mode, card[row], cards[i], row, size, style))
- }
- lines[row] = strings.Join(lineParts, " ")
- }
- return lines
-}
-
-// renderCardRow lays cards side by side, each column padded to its own art
-// or label width, and appends an optional centered label row. minWidth lets
-// the caller force a floor (video poker's HELD labels need it) while Pai Gow
-// passes 0. It is the shared "cards with numbers underneath" presentation.
-func renderCardRow(cards []casino.CardView, labels []string, minWidth int, mode string, unicode bool, size int, style string, art *cardart.Store) []string {
- if len(cards) == 0 {
- return nil
- }
- artLines := make([][]string, len(cards))
- widths := make([]int, len(cards))
- height := 0
- for i, card := range cards {
- artLines[i] = cardsLine(mode, []casino.CardView{card}, size, unicode, style, art)
- height = max(height, len(artLines[i]))
- widths[i] = minWidth
- for _, line := range artLines[i] {
- widths[i] = max(widths[i], color.VisibleLen(line))
- }
- if i < len(labels) {
- widths[i] = max(widths[i], color.VisibleLen(labels[i]))
- }
- }
- lines := make([]string, 0, height+1)
- for row := 0; row < height; row++ {
- parts := make([]string, len(cards))
- for i := range cards {
- text := ""
- if row < len(artLines[i]) {
- text = artLines[i][row]
- }
- parts[i] = rouletteCenter(text, widths[i])
- }
- lines = append(lines, strings.Join(parts, " "))
- }
- if len(labels) > 0 {
- parts := make([]string, len(cards))
- for i := range cards {
- text := ""
- if i < len(labels) {
- text = labels[i]
- }
- parts[i] = rouletteCenter(text, widths[i])
- }
- lines = append(lines, strings.Join(parts, " "))
- }
- return lines
-}
-
-func cardRenderSize(unicode bool, size int) int {
- if size == 1 {
- return 1
- }
- if !unicode {
- return 5
- }
- if size == 1 || size == 5 || size == 7 || size == 9 {
- return size
- }
- return 7
-}
-
-var suitSymbols = map[string]string{"hearts": "♥", "spades": "♠", "diamonds": "♦", "clubs": "♣", "joker": "★"}
-
-func cardSymbol(card casino.CardView) string {
- if card.Hidden {
- return "?"
- }
- return card.Rank + suitSymbols[card.Suit]
-}
-
-func cardArtLines(card casino.CardView, size int, unicode bool, style string, art *cardart.Store) []string {
- if card.Hidden {
- return generatedCardArt("?", "", size, unicode, style)
- }
- key := card.Rank
- if card.Rank == "JK" {
- return jokerCardArt(size, unicode, style, art)
- }
- if card.Suit != "" {
- key += strings.ToUpper(card.Suit[:1])
- }
- if configured := art.Art(key, size); len(configured) == size {
- if unicode && style == "light" {
- // The shared pip art draws a dark box frame, which leaves the top
- // and bottom rows looking like the dark style. Replace only those
- // two rows with the half-block edges the white body meets.
- configured = append([]string(nil), configured...)
- configured[0] = strings.Repeat("▄", utf8.RuneCountInString(configured[0]))
- configured[size-1] = strings.Repeat("▀", utf8.RuneCountInString(configured[size-1]))
- }
- return configured
- }
- return generatedCardArt(card.Rank, card.Suit, size, unicode, style)
-}
-
-func generatedCardArt(rank, suit string, size int, unicode bool, style string) []string {
- if size < 5 {
- size = 5
- }
- border := "-"
- left, right := "+", "+"
- if unicode {
- border, left, right = "─", "┌", "┐"
- }
- bottomLeft, bottomRight := left, right
- if unicode {
- bottomLeft, bottomRight = "└", "┘"
- }
- vertical := "|"
- if unicode {
- vertical = "│"
- }
- if unicode && style == "light" {
- lines := []string{strings.Repeat("▄", size)}
- lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...)
- return append(lines, strings.Repeat("▀", size))
- }
- lines := []string{left + strings.Repeat(border, size-2) + right}
- lines = append(lines, generatedCardBody(rank, suit, size, unicode, vertical)...)
- lines = append(lines, bottomLeft+strings.Repeat(border, size-2)+bottomRight)
- return lines
-}
-
-func generatedCardBody(rank, suit string, size int, unicode bool, vertical string) []string {
- inner := size - 2
- lines := make([]string, 0, size-2)
- for row := 1; row < size-1; row++ {
- content := strings.Repeat(" ", inner)
- if row == 1 {
- content = rank + strings.Repeat(" ", inner-len(rank))
- }
- if row == (size-1)/2 {
- suitText := ""
- if suit != "" {
- suitText = suitLetter(suit, unicode)
- }
- suitWidth := utf8.RuneCountInString(suitText)
- leftPadding := (inner - suitWidth) / 2
- content = strings.Repeat(" ", leftPadding) + suitText + strings.Repeat(" ", inner-leftPadding-suitWidth)
- }
- if row == size-2 {
- content = strings.Repeat(" ", inner-len(rank)) + rank
- }
- lines = append(lines, vertical+content+vertical)
- }
- return lines
-}
-
-func suitLetter(suit string, unicode bool) string {
- if !unicode {
- return strings.ToUpper(suit[:1])
- }
- return suitSymbols[suit]
-}
-
-func colorizeCardLine(mode, line string, card casino.CardView, row, size int, style string) string {
- red := card.Suit == "hearts" || card.Suit == "diamonds"
- rankColor := 27
- if red {
- rankColor = 196
- }
- suitColor := 27
- if red {
- suitColor = 196
- }
- rankLine := row == 1 || row == size-2
- light := style == "light"
- bodyBackground := light && row > 0 && row < size-1
- var builder strings.Builder
- for _, r := range line {
- if strings.ContainsRune("┌─┐└┘│┬┴├┤┼╔╗╚╝═║╦╩╠╣╬▀▄+|-", r) {
- spec := color.ColorSpec{Fg: 15}
- if bodyBackground {
- spec.Bg = 15
- }
- builder.WriteString(color.Render(mode, spec, string(r)))
- } else if r == ' ' {
- if bodyBackground {
- builder.WriteString(color.Render(mode, color.ColorSpec{Fg: 15, Bg: 15}, " "))
- } else {
- builder.WriteRune(r)
- }
- } else {
- value := suitColor
- if rankLine {
- value = rankColor
- }
- spec := color.ColorSpec{Fg: value}
- if bodyBackground {
- spec.Bg = 15
- }
- builder.WriteString(color.Render(mode, spec, string(r)))
- }
- }
- return builder.String()
-}
-
-func colorizeCardSymbol(mode string, card casino.CardView) string {
- if card.Hidden {
- return color.Render(mode, color.ColorSpec{Fg: 15}, "?")
- }
- rankColor, suitColor := 27, 27
- if card.Suit == "hearts" || card.Suit == "diamonds" {
- rankColor, suitColor = 196, 196
- }
- runes := []rune(cardSymbol(card))
- rankLength := len([]rune(card.Rank))
- return color.Render(mode, color.ColorSpec{Fg: rankColor}, string(runes[:rankLength])) +
- color.Render(mode, color.ColorSpec{Fg: suitColor}, string(runes[rankLength:]))
-}
-
-func renderSlotSnapshot(snapshot *casino.SlotSnapshot, mode string, unicode bool) string {
- if snapshot == nil {
- return ""
- }
- left, junction, right, horizontal, vertical := '+', '+', '+', '-', '|'
- bottomLeft, bottomJunction, bottomRight := '+', '+', '+'
- if unicode {
- left, junction, right, horizontal, vertical = '\u250c', '\u252c', '\u2510', '\u2500', '\u2502'
- bottomLeft, bottomJunction, bottomRight = '\u2514', '\u2534', '\u2518'
- }
- separator := string(left)
- for reel := 0; reel < 5; reel++ {
- if reel > 0 {
- separator += string(junction)
- }
- separator += strings.Repeat(string(horizontal), 9)
- }
- separator += string(right)
- lines := []string{casinoFrame(mode, separator)}
- frameVertical := casinoFrame(mode, string(vertical))
- for row := 0; row < 3; row++ {
- line := frameVertical
- for reel := 0; reel < 5; reel++ {
- value := "..."
- if reel < snapshot.StoppedReels {
- value = snapshot.Grid[row][reel]
- }
- // Pad to the fixed 7-cell reel width; a malformed profile could
- // otherwise make this negative and panic. Validation caps the id
- // length, so this is a defence-in-depth backstop.
- pad := 7 - len(value)
- if pad < 0 {
- pad = 0
- }
- line += " " + colorSlotSymbol(mode, value) + strings.Repeat(" ", pad) + " " + frameVertical
- }
- lines = append(lines, line)
- }
- bottom := string(bottomLeft)
- for reel := 0; reel < 5; reel++ {
- if reel > 0 {
- bottom += string(bottomJunction)
- }
- bottom += strings.Repeat(string(horizontal), 9)
- }
- bottom += string(bottomRight)
- lines = append(lines, casinoFrame(mode, bottom))
- return strings.Join(lines, "\n")
-}
-
-func casinoFrame(mode, text string) string {
- spec := color.NoColor()
- spec.Fg = 244
- return color.Render(mode, spec, text)
-}
-
-func colorSlotSymbol(mode, symbol string) string {
- spec := color.NoColor()
- spec.Fg = 250
- switch symbol {
- case "straw":
- spec.Fg = 220
- case "stick":
- spec.Fg = 46
- case "brick":
- spec.Fg = 196
- case "hat":
- spec.Fg = 201
- case "wolf":
- spec.Fg = 39
- case "scatter":
- spec.Fg = 226
- default:
- spec.Dim = true
- }
- return color.Render(mode, spec, symbol)
-}