diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino.go | 7 | ||||
| -rw-r--r-- | internal/game/casino_ux.go | 8 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 13 | ||||
| -rw-r--r-- | internal/game/coin_flip_test.go | 162 | ||||
| -rw-r--r-- | internal/game/look_casino.go | 2 | ||||
| -rw-r--r-- | internal/game/look_target.go | 2 | ||||
| -rw-r--r-- | internal/game/render_coin_flip.go | 238 |
7 files changed, 429 insertions, 3 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go index fa78c2a..2d8024c 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -277,7 +277,7 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) { continue } if strings.EqualFold(arg, "max") { - amount = min(table.Config.MaxBet, casinoAffordable(p)) + amount = min(table.Config.MaxBet, casinoAffordable(p)/table.StakeMultiple()) if amount < 1 { sess.WriteLine("You don't have any chips or credits to bet.") return @@ -519,11 +519,12 @@ func (g *Game) casinoBoardReminder(sess *net.Session, category string, menu *cas _, isPlinko := snapshot.(*casino.PlinkoSnapshot) _, isPachinko := snapshot.(*casino.PachinkoSnapshot) _, isCashHunt := snapshot.(*casino.CashHuntSnapshot) + _, isCoinFlip := snapshot.(*casino.CoinFlipSnapshot) _, isPaiGow := snapshot.(*casino.PaiGowSnapshot) b, isBigSix := snapshot.(*casino.BigSixSnapshot) isBigSix = isBigSix && b.Phase != "playing" && b.Phase != "result" s, isRoulette := snapshot.(*casino.RouletteSnapshot) - if !isCashHunt && !isPlinko && !isPachinko && !isPoker && !isCraps && !isPaiGow && !isBigSix && (!isRoulette || s.Phase == "playing" || s.Phase == "result") { + if !isCashHunt && !isCoinFlip && !isPlinko && !isPachinko && !isPoker && !isCraps && !isPaiGow && !isBigSix && (!isRoulette || s.Phase == "playing" || s.Phase == "result") { return "", false } if isRoulette && sess.Player.OptionString("casino_display") == "compact" && (menu.Repeat || !menu.ShowBalance) { @@ -618,6 +619,8 @@ func (g *Game) renderCasinoSnapshot(sess *net.Session, snapshot casino.Snapshot) rtp = casinoRTP(s.Edge, precision) } return renderCashHunt(s, mode, unicode, rtp) + case *casino.CoinFlipSnapshot: + return renderCoinFlip(s, mode, unicode) case *casino.PlinkoSnapshot: return renderPlinko(s, mode, unicode) case *casino.PachinkoSnapshot: diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go index 7eb10dd..bdc3add 100644 --- a/internal/game/casino_ux.go +++ b/internal/game/casino_ux.go @@ -68,6 +68,8 @@ func casinoTableAliases(cfg casino.TableConfig) []string { switch cfg.Game { case casino.GameCashHunt: names = append(names, "cash hunt", "cashhunt", "cash-hunt") + case casino.GameCoinFlip: + names = append(names, "coin flip", "coinflip", "coin-flip", "coin") case casino.GamePachinko: names = append(names, "pachinko") case casino.GameBigSix: @@ -174,6 +176,12 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { } return } + // Coin Flip has no felt or board: the spin and the coin are the whole + // game, so the table command answers with a one-liner instead of nothing. + if t.Config.Game == casino.GameCoinFlip { + sess.WriteLine("Coin Flip has no table. Use bet N to spin and flip the coin.") + return + } // The table command shows only the table: the live board for roulette, // craps and video poker, or the printed felt for blackjack, baccarat and // Pai Gow. No balance, seat state, command menu, or reminder rides along. diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index ef17925..c23f89b 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -254,6 +254,8 @@ func (g *Game) casinoOwnedInput(sess *net.Session, cmd string) bool { return isAllDigits(cmd) || cmd == "settle" case casino.GamePachinko: return isAllDigits(cmd) + case casino.GameCoinFlip: + return cmd == "settle" case casino.GamePaiGow: return isAllDigits(cmd) || cmd == "set" || cmd == "house" || cmd == "autoset" || cmd == "settle" } @@ -292,6 +294,7 @@ var casinoCommonVerbs = []string{"watch", "game", "table", "list", "stop", "rule // because "split" is not a Big 6 verb and "spin" is not a blackjack verb. var casinoVerbsByGame = map[casino.GameName][]string{ casino.GameCashHunt: {"bet", "rebet", "difficulty", "paytable", "pick", "settle"}, + casino.GameCoinFlip: {"bet", "rebet", "settle"}, casino.GamePlinko: {"bet", "rebet", "difficulty", "balls", "paytable"}, casino.GamePachinko: {"bet", "rebet", "table", "drop"}, casino.GameBlackjack: {"bet", "rebet", "unbet", "hit", "stand", "double", "split", "insurance", "surrender", "decline"}, @@ -420,6 +423,16 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI } } + // Coin Flip owns the retry verb for a payout that could not be persisted. + if p != nil && g.Casino != nil { + if t, playing := g.Casino.Participation(p.Name); playing && t != nil && t.Config.Game == casino.GameCoinFlip { + if cmd == "settle" { + g.executeTableAction(sess, strings.Join(append([]string{cmd}, args...), " ")) + return + } + } + } + // Pachinko owns bare slot numbers and "drop N" while seated; a "drop" // with any other argument still drops an item from the world. if p != nil && g.Casino != nil { diff --git a/internal/game/coin_flip_test.go b/internal/game/coin_flip_test.go new file mode 100644 index 0000000..c8a5372 --- /dev/null +++ b/internal/game/coin_flip_test.go @@ -0,0 +1,162 @@ +package game + +import ( + "strings" + "testing" + + "thehouseoficarus/internal/casino" +) + +func coinFlipRoom() string { + return "name: Two Moons Flip\ncasino_tables:\n - id: coin_flip\n game: coin_flip\n min_bet: 1\n max_bet: 100\n" +} + +func TestCoinFlipPlayAndFlip(t *testing.T) { + g, s, conn := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none", "casino_hints": "on"} + g.executePlay(s, []string{"coin", "flip"}, "play coin flip") + table, ok := g.Casino.Participation(s.Player.Name) + if !ok || table == nil || table.Config.Game != casino.GameCoinFlip { + t.Fatal("Coin Flip alias did not seat the player") + } + if table.StakeMultiple() != 100 { + t.Fatalf("stake multiple %d, want 100", table.StakeMultiple()) + } + // Sitting down must not print a coin or a multiplier. + if text := string(conn.data); strings.Contains(text, "▐") || strings.Contains(text, "BLUE") { + t.Fatalf("join printed a coin: %s", text) + } + // The player only holds 100 credits, so a stake of 1 reserves 100. + conn.data = nil + g.executeBet(s, []string{"1"}, "bet 1") + if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 { + t.Fatalf("bet did not reserve 100x: %+v", s.Player.PendingWagers) + } + if !strings.Contains(string(conn.data), "Stake 1") { + t.Fatalf("bet did not print the stake line: %s", conn.data) + } + // The first spin frame is held back one tick. + conn.data = nil + g.CasinoTick() + if !strings.Contains(string(conn.data), "▼") { + t.Fatalf("first tick did not start the red spin: %s", conn.data) + } + // Advance until the round lands (the spin + flip plan is at most ~40 frames). + for i := 0; i < 200 && len(s.Player.PendingWagers) > 0; i++ { + conn.data = nil + g.CasinoTick() + } + if len(s.Player.PendingWagers) != 0 { + t.Fatalf("round never settled: %+v", s.Player.PendingWagers) + } + if !strings.Contains(stripANSI(string(conn.data)), "Returned:") { + t.Fatalf("settlement receipt missing: %s", conn.data) + } +} + +func TestCoinFlipStakeLimit(t *testing.T) { + g, s, _ := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + // 100 credits cannot cover a stake of 2 (200 reserved). + g.executeBet(s, []string{"2"}, "bet 2") + if len(s.Player.PendingWagers) != 0 { + t.Fatalf("accepted a stake the reserve cannot cover: %+v", s.Player.PendingWagers) + } +} + +func TestCoinFlipBetMaxUsesReserve(t *testing.T) { + g, s, _ := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + // 100 credits with a 100x reserve means the largest stake is 1. + g.executeBet(s, []string{"max"}, "bet max") + if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 { + t.Fatalf("bet max did not reserve 100x the affordable stake: %+v", s.Player.PendingWagers) + } +} + +func TestCoinFlipSettleReachesGame(t *testing.T) { + g, s, conn := casinoCrapsGame(t, coinFlipRoom()) + s.Player.Options = map[string]any{"color": "none"} + g.executePlay(s, []string{"coin flip"}, "play coin flip") + conn.data = nil + g.handleGameCommand(s, "settle") + if strings.Contains(string(conn.data), "Unknown command") { + t.Fatalf("settle was rejected before reaching Coin Flip: %s", conn.data) + } +} + +func TestCoinFlipWheelInk(t *testing.T) { + if ink, bold := coinFlipWheelInk(30, casino.CoinFlipSideRed); ink != -1 || bold { + t.Fatalf("30x = %d,%v; want plain", ink, bold) + } + if ink, bold := coinFlipWheelInk(60, casino.CoinFlipSideRed); ink != coinFlipRedInk || bold { + t.Fatalf("60x = %d,%v; want red not bold", ink, bold) + } + if ink, bold := coinFlipWheelInk(80, casino.CoinFlipSideBlue); ink != coinFlipBlueInk || !bold { + t.Fatalf("80x = %d,%v; want blue bold", ink, bold) + } + if ink, bold := coinFlipWheelInk(-90, casino.CoinFlipSideRed); ink != coinFlipRedInk || !bold { + t.Fatalf("-90x = %d,%v; want red bold", ink, bold) + } +} + +func TestCoinFlipRendering(t *testing.T) { + for _, mode := range []string{"none", "ansi", "xterm256"} { + for _, unicode := range []bool{false, true} { + spin := &casino.CoinFlipSnapshot{ + Phase: casino.CoinFlipPhaseRedSpin, + SpinSide: casino.CoinFlipSideRed, + SpinValues: []int{-10, -20, -30, -40, -50, -60, -70}, + } + rendered := renderCoinFlip(spin, mode, unicode) + if mode == "none" && strings.Contains(rendered, "\x1b") { + t.Fatalf("plain spin has ANSI escapes: %q", rendered) + } + if !strings.Contains(rendered, "▼") && unicode { + t.Fatalf("spin strip missing its pointer: %q", rendered) + } + + set := &casino.CoinFlipSnapshot{ + Phase: casino.CoinFlipPhaseRedSpin, + SpinSide: casino.CoinFlipSideRed, + SpinValues: []int{-10, -20, -30, -40, -50, -60, -70}, + Set: true, + } + if !strings.Contains(renderCoinFlip(set, mode, unicode), "SET") { + t.Fatalf("landed spin missing SET: %q", renderCoinFlip(set, mode, unicode)) + } + + results := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResults, Red: -61, Blue: 85} + rendered = renderCoinFlip(results, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "-61x") || !strings.Contains(plain, "+85x") { + t.Fatalf("results panel missing a face: %q", plain) + } + + coin := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseFlipping, Side: casino.CoinFlipSideBlue, Multiplier: 42} + rendered = renderCoinFlip(coin, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "+42x") { + t.Fatalf("coin missing its multiplier: %q", plain) + } + if strings.Contains(rendered, "COIN FLIP") || strings.Contains(rendered, "stake") { + t.Fatalf("flip printed chrome instead of just the coin: %q", rendered) + } + + settled := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResult, Side: casino.CoinFlipSideRed, Multiplier: -50, Result: true} + rendered = renderCoinFlip(settled, mode, unicode) + if plain := stripANSI(rendered); !strings.Contains(plain, "-50x") { + t.Fatalf("settled coin missing its multiplier: %q", plain) + } + if mode == "xterm256" && !strings.Contains(rendered, "48;5;196") { + t.Fatalf("settled red coin has no red fill: %q", rendered) + } + if mode == "ansi" && !strings.Contains(rendered, "\x1b[41m") { + t.Fatalf("settled red coin has no red fill: %q", rendered) + } + } + } + if got := renderCoinFlip(&casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseIdle}, "none", true); got != "" { + t.Fatalf("idle coin rendered %q", got) + } +} diff --git a/internal/game/look_casino.go b/internal/game/look_casino.go index e349386..500bbfb 100644 --- a/internal/game/look_casino.go +++ b/internal/game/look_casino.go @@ -105,6 +105,8 @@ func casinoTableSummary(cfg casino.TableConfig) string { switch cfg.Game { case casino.GameCashHunt: return "100 squares; three difficulties; 98% RTP; prizes up to 50000x" + case casino.GameCoinFlip: + return "fair 50/50; red 0 to -100x, blue 0 to +100x; 98% RTP; reserves 100x the stake" case casino.GamePlinko: return fmt.Sprintf("16 rows; four risks; 1-10 balls; near %.4g%% target RTP", cfg.Plinko.Normalize().RTP) case casino.GamePachinko: diff --git a/internal/game/look_target.go b/internal/game/look_target.go index 97d7ecb..b467ae9 100644 --- a/internal/game/look_target.go +++ b/internal/game/look_target.go @@ -50,7 +50,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { // Felt tables resolve through one data-driven loop; the video-poker cabinet // has its own cabinet-id/pay-table handling. - for _, game := range []casino.GameName{casino.GameCashHunt, casino.GamePlinko, casino.GamePachinko, casino.GameBigSix, casino.GameBlackjack, casino.GameBaccarat, casino.GameRoulette, casino.GameCraps, casino.GamePaiGow} { + for _, game := range []casino.GameName{casino.GameCashHunt, casino.GameCoinFlip, casino.GamePlinko, casino.GamePachinko, casino.GameBigSix, casino.GameBlackjack, casino.GameBaccarat, casino.GameRoulette, casino.GameCraps, casino.GamePaiGow} { if g.lookCasinoTable(sess, lower, game) { return } diff --git a/internal/game/render_coin_flip.go b/internal/game/render_coin_flip.go new file mode 100644 index 0000000..6408b26 --- /dev/null +++ b/internal/game/render_coin_flip.go @@ -0,0 +1,238 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/color" +) + +// Coin Flip inks. The two faces are deliberately far apart on the xterm +// wheel: bright red for the loss face and a deep sky blue for the win face. +const ( + coinFlipRedInk = 196 + coinFlipBlueInk = 45 + coinFlipDimInk = 245 +) + +// coinFlipLabel renders a signed net multiplier, e.g. "+50x" or "-100x". +func coinFlipLabel(multiplier int) string { + return fmt.Sprintf("%+dx", multiplier) +} + +// coinFlipWheelInk tiers a wheel value: under 50x is plain, 50-74x takes the +// face colour, and 75x and up is bold as well. +func coinFlipWheelInk(value int, side string) (int, bool) { + magnitude := value + if magnitude < 0 { + magnitude = -magnitude + } + if magnitude < 50 { + return -1, false + } + ink := coinFlipRedInk + if side == casino.CoinFlipSideBlue { + ink = coinFlipBlueInk + } + return ink, magnitude >= 75 +} + +// coinFlipRow is one scanline of the coin: padding, an outline run, the +// interior (which alone is filled when the coin settles), the other outline +// run, and trailing padding. +type coinFlipRow struct { + pre, left, interior, right, post string +} + +// coinFlipArt draws one medium coin: a nine-column, five-row circle with the +// signed multiplier centered inside. A settled coin fills only the interior +// cells, so the partial outline blocks keep the circle's round edge instead of +// growing into a square. A spinning coin gets whoosh marks on either side. +func coinFlipArt(multiplier int, side string, settled, alt bool, mode string, unicode bool) []string { + const inner = 7 + label := coinFlipLabel(multiplier) + if len(label) > inner { + label = label[:inner] + } + content := rouletteCenter(label, inner) + ink := coinFlipRedInk + if side == casino.CoinFlipSideBlue { + ink = coinFlipBlueInk + } + plain := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: -1, Bg: -1}, text) } + outline := func(text string) string { + if text == "" { + return "" + } + return color.Render(mode, color.ColorSpec{Fg: ink, Bg: -1, Bold: true}, text) + } + interior := func(text string) string { + if text == "" { + return "" + } + bg, fg := -1, ink + if settled { + // Fill only the interior: the outline's partial cells stay open. + bg, fg = ink, 16 + } + var b strings.Builder + for _, r := range text { + if r == ' ' { + b.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bg: bg}, " ")) + } else { + b.WriteString(color.Render(mode, color.ColorSpec{Fg: fg, Bg: bg, Bold: true}, string(r))) + } + } + return b.String() + } + var rows []coinFlipRow + if unicode { + rows = []coinFlipRow{ + {" ", strings.Repeat("▄", inner-2), "", "", " "}, + {" ", "▟", strings.Repeat(" ", inner-2), "▙", " "}, + {"", "▐", content, "▌", ""}, + {" ", "▜", strings.Repeat(" ", inner-2), "▛", " "}, + {" ", strings.Repeat("▀", inner-2), "", "", " "}, + } + } else { + rows = []coinFlipRow{ + {" ", strings.Repeat("_", inner-2), "", "", " "}, + {" ", "/", strings.Repeat(" ", inner-2), "\\", " "}, + {"", "|", content, "|", ""}, + {" ", "\\", strings.Repeat(" ", inner-2), "/", " "}, + {" ", strings.Repeat("-", inner-2), "", "", " "}, + } + } + lines := make([]string, 0, len(rows)) + for _, r := range rows { + lines = append(lines, plain(r.pre)+outline(r.left)+interior(r.interior)+outline(r.right)+plain(r.post)) + } + if !settled { + // Only the two flank rows carry motion marks, and they swap the whoosh + // and arrow each frame so the coin reads as tumbling. + arrow := color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, "↺") + whoosh := color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, "~") + for i := range lines { + var left, right string + switch i { + case 1: + if alt { + left, right = arrow, whoosh + } else { + left, right = whoosh, arrow + } + case 3: + if alt { + left, right = whoosh, arrow + } else { + left, right = arrow, whoosh + } + default: + continue + } + lines[i] = left + " " + lines[i] + " " + right + } + } + return lines +} + +// coinFlipWheelChar is one character of the scrolling multiplier strip. +type coinFlipWheelChar struct { + ch rune + value int + sep bool + label bool +} + +// coinFlipWheel builds the full strip: a separator, then each value centred in +// a fixed cell. The engine and renderer share the geometry constants, so the +// landing offset and the drawn pointer always agree. +func coinFlipWheel(values []int, unicode bool) []coinFlipWheelChar { + sep := '│' + if !unicode { + sep = '|' + } + chars := make([]coinFlipWheelChar, 0, len(values)*casino.CoinFlipPitch+1) + for _, v := range values { + chars = append(chars, coinFlipWheelChar{ch: sep, sep: true}) + for _, r := range rouletteCenter(coinFlipLabel(v), casino.CoinFlipCellWidth) { + chars = append(chars, coinFlipWheelChar{ch: r, value: v, label: r != ' '}) + } + } + chars = append(chars, coinFlipWheelChar{ch: sep, sep: true}) + return chars +} + +// renderCoinFlipSpin draws the scrolling multiplier strip, a fixed pointer, and +// a bold SET under the landed value. +func renderCoinFlipSpin(s *casino.CoinFlipSnapshot, mode string, unicode bool) string { + pointer := "▼" + if !unicode { + pointer = "V" + } + wheel := coinFlipWheel(s.SpinValues, unicode) + var out strings.Builder + out.WriteString(strings.Repeat(" ", casino.CoinFlipPointerCol)) + out.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bold: true}, pointer)) + out.WriteByte('\n') + for i := 0; i < casino.CoinFlipViewWidth; i++ { + idx := s.SpinOffset + i + cell := coinFlipWheelChar{ch: ' '} + if idx >= 0 && idx < len(wheel) { + cell = wheel[idx] + } + spec := color.ColorSpec{Fg: coinFlipDimInk} + switch { + case cell.label: + ink, bold := coinFlipWheelInk(cell.value, s.SpinSide) + spec = color.ColorSpec{Fg: ink, Bold: bold} + case cell.sep: + spec = color.ColorSpec{Fg: coinFlipDimInk} + } + out.WriteString(color.Render(mode, spec, string(cell.ch))) + } + out.WriteByte('\n') + if s.Set { + out.WriteString(strings.Repeat(" ", casino.CoinFlipPointerCol-1)) + out.WriteString(color.Render(mode, color.ColorSpec{Fg: -1, Bold: true}, "SET")) + } + return strings.TrimRight(out.String(), " \n") +} + +// coinFlipResultCell centers a coloured label/value pair inside one panel cell. +func coinFlipResultCell(label, value string, ink, width int, mode string) string { + text := label + " " + value + pad := max(0, width-len(text)) + left := pad / 2 + right := pad - left + return strings.Repeat(" ", left) + + color.Render(mode, color.ColorSpec{Fg: ink, Bold: true}, label) + " " + + color.Render(mode, color.ColorSpec{Fg: 250, Bold: true}, value) + + strings.Repeat(" ", right) +} + +// renderCoinFlipResults draws the two landed faces as a compact coloured panel. +func renderCoinFlipResults(s *casino.CoinFlipSnapshot, mode string, unicode bool) string { + const cell = 15 + b := rouletteBorders(unicode) + dim := func(text string) string { return color.Render(mode, color.ColorSpec{Fg: coinFlipDimInk}, text) } + top := dim(b.tl + strings.Repeat(b.h, cell) + b.teeTop + strings.Repeat(b.h, cell) + b.tr) + mid := dim(b.v) + + coinFlipResultCell("RED", coinFlipLabel(s.Red), coinFlipRedInk, cell, mode) + dim(b.v) + + coinFlipResultCell("BLUE", coinFlipLabel(s.Blue), coinFlipBlueInk, cell, mode) + dim(b.v) + bottom := dim(b.bl + strings.Repeat(b.h, cell) + b.teeBottom + strings.Repeat(b.h, cell) + b.br) + return top + "\n" + mid + "\n" + bottom +} + +func renderCoinFlip(s *casino.CoinFlipSnapshot, mode string, unicode bool) string { + switch s.Phase { + case casino.CoinFlipPhaseRedSpin, casino.CoinFlipPhaseBlueSpin: + return renderCoinFlipSpin(s, mode, unicode) + case casino.CoinFlipPhaseResults: + return renderCoinFlipResults(s, mode, unicode) + case casino.CoinFlipPhaseFlipping, casino.CoinFlipPhaseResult: + return strings.Join(coinFlipArt(s.Multiplier, s.Side, s.Result, s.FlipAlt, mode, unicode), "\n") + } + return "" +} |
