aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-31 17:44:10 -0400
committerhistoria <[not public]>2026-07-31 17:44:10 -0400
commitf51424c90dbce7191a31b6e675818c985f0f4539 (patch)
treea7845a2e09bd6e83d2d034f5e702a4940f38126a /internal/game
parent58758d41488a777d2bc0e787be655363bdd9eb60 (diff)
downloadthehouseoficarus-f51424c90dbce7191a31b6e675818c985f0f4539.tar.gz
feat: casino framework and basic slot machine
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_effects.go7
-rw-r--r--internal/game/casino.go319
-rw-r--r--internal/game/casino_test.go29
-rw-r--r--internal/game/cmd_move.go4
-rw-r--r--internal/game/cmd_quit.go4
-rw-r--r--internal/game/cmd_registry.go5
-rw-r--r--internal/game/cmd_stop.go12
-rw-r--r--internal/game/core_login_char.go1
-rw-r--r--internal/game/game.go27
-rw-r--r--internal/game/look_entities.go37
-rw-r--r--internal/game/render_help.go4
-rw-r--r--internal/game/tick_systems.go3
-rw-r--r--internal/game/tick_systems_test.go3
13 files changed, 442 insertions, 13 deletions
diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go
index cadd05e..d1effac 100644
--- a/internal/game/act_effects.go
+++ b/internal/game/act_effects.go
@@ -177,7 +177,12 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
}
}
- // 11/12. spawn_mob / despawn_mob (player-owned / owner-filtered).
+ // 11. casino game join. The table is resolved from the player's current room.
+ if step.StartGame != "" {
+ g.startCasinoGame(sess, p, step.StartGame)
+ }
+
+ // 12/13. spawn_mob / despawn_mob (player-owned / owner-filtered).
if step.SpawnMob != nil {
g.spawnTriggerMob(sess, p, step.SpawnMob, roomID)
}
diff --git a/internal/game/casino.go b/internal/game/casino.go
new file mode 100644
index 0000000..c6fb63b
--- /dev/null
+++ b/internal/game/casino.go
@@ -0,0 +1,319 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+type casinoWallet struct {
+ g *Game
+ p *player.Player
+}
+
+func (w casinoWallet) Wager(amount int) (casino.Wager, bool) {
+ chips := countChips(w.p)
+ if chips > amount {
+ chips = amount
+ }
+ credits := amount - chips
+ if w.p.Credits < credits {
+ return casino.Wager{}, false
+ }
+ if chips > 0 {
+ w.p.RemoveItem("chips", chips)
+ }
+ w.p.Credits -= credits
+ w.g.AccountStore.SaveCharacter(w.p)
+ return casino.Wager{Total: amount, Chips: chips, Credits: credits}, true
+}
+
+func (w casinoWallet) PayCredits(amount int) {
+ w.p.Credits += amount
+ w.g.AccountStore.SaveCharacter(w.p)
+}
+
+func (g *Game) casinoTable(roomID int, gameName string) (*casino.Table, bool) {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ for _, cfg := range room.CasinoTables {
+ if strings.EqualFold(string(cfg.Game), gameName) {
+ return g.Casino.EnsureTable(roomID, cfg), true
+ }
+ }
+ return nil, false
+}
+
+func (g *Game) casinoMachine(roomID int, gameName string) (*casino.Machine, bool) {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil {
+ return nil, false
+ }
+ for _, cfg := range room.CasinoMachines {
+ if strings.EqualFold(string(cfg.Game), gameName) || strings.EqualFold(cfg.Variant, gameName) {
+ return g.Casino.EnsureMachine(roomID, cfg), true
+ }
+ }
+ // Migrate older room definitions that declared slots under casino_tables;
+ // slots are still always instantiated as one-player machines.
+ if strings.EqualFold(gameName, "slots") {
+ for _, cfg := range room.CasinoTables {
+ if strings.EqualFold(string(cfg.Game), "slots") {
+ return g.Casino.EnsureMachine(roomID, casino.MachineConfig{
+ ID: cfg.ID, Game: casino.GameSlots, MinBet: cfg.MinBet, MaxBet: cfg.MaxBet,
+ }), true
+ }
+ }
+ }
+ return nil, false
+}
+
+func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil || len(args) == 0 {
+ sess.WriteLine("Play what?")
+ return
+ }
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.")
+ return
+ }
+ g.startCasinoGame(sess, p, args[0])
+}
+
+func (g *Game) startCasinoGame(sess *net.Session, p *player.Player, gameName string) {
+ if p == nil || sess == nil {
+ return
+ }
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine("You're already playing a game. Type \"stop\" between rounds to leave it.")
+ return
+ }
+ if machine, ok := g.casinoMachine(p.RoomID, gameName); ok {
+ g.emitCasinoEvents(g.Casino.JoinMachine(machine.RoomID, machine.Config, p.Name))
+ return
+ }
+ table, ok := g.casinoTable(p.RoomID, gameName)
+ if !ok {
+ sess.WriteLine(fmt.Sprintf("There is no %s table here.", gameName))
+ return
+ }
+ g.emitCasinoEvents(g.Casino.Join(table.RoomID, table.Config, p.Name))
+}
+
+func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ sess.WriteLine("Bet how much?")
+ return
+ }
+ if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ if len(args) == 0 {
+ g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ if len(args) == 1 && args[0] == "max" {
+ g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, machine.Config.MaxBet))
+ return
+ }
+ if len(args) == 1 {
+ if amount, err := strconv.Atoi(args[0]); err == nil && amount > 0 {
+ g.emitCasinoEvents(g.Casino.SetMachineBet(machine.RoomID, machine.Config.ID, p.Name, amount))
+ return
+ }
+ }
+ g.emitCasinoEvents(g.Casino.StartMachine(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ if len(args) == 0 {
+ sess.WriteLine("Bet how much?")
+ return
+ }
+ amount, err := strconv.Atoi(args[0])
+ if err != nil || amount <= 0 {
+ sess.WriteLine("Your bet must be a positive whole number.")
+ return
+ }
+ table, playing := g.Casino.Participation(p.Name)
+ if !playing {
+ sess.WriteLine("You're not playing a game.")
+ return
+ }
+ g.emitCasinoEvents(g.Casino.Bet(table.RoomID, table.Config.ID, p.Name, amount, casinoWallet{g: g, p: p}))
+}
+
+func (g *Game) executeAutoBet(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ g.emitCasinoEvents(g.Casino.EnableMachineAutoBet(machine.RoomID, machine.Config.ID, p.Name, casinoWallet{g: g, p: p}))
+ return
+ }
+ sess.WriteLine("Autobet is only available at games without player choices.")
+}
+
+func (g *Game) CasinoTick() {
+ if g.Casino != nil {
+ g.emitCasinoEvents(g.Casino.Tick())
+ }
+}
+
+func (g *Game) emitCasinoEvents(events []casino.Event) {
+ for _, event := range events {
+ if event.Message == "" {
+ continue
+ }
+ category := event.Color
+ if category == "" {
+ category = casinoEventColor(event.Type)
+ }
+ if event.Public && g.Hub != nil {
+ for _, sess := range g.Hub.PlayersInRoom(event.RoomID) {
+ if event.PrivateTo != "" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
+ continue
+ }
+ message := event.Message
+ if category == "casino_menu" && sess.Player != nil && sess.Player.Name == event.PrivateTo {
+ message = g.casinoMenuBalance(sess, message)
+ }
+ if event.Slot != nil && sess.Player != nil {
+ message += "\n" + renderSlotSnapshot(event.Slot, g.colorMode(sess), sess.Player.OptionBool("unicode"))
+ }
+ sess.WriteLine(g.colorize(sess, category, message))
+ }
+ }
+ if event.PrivateMessage != "" && event.PrivateTo != "" {
+ g.charsMu.Lock()
+ sess := g.loggedInChars[event.PrivateTo]
+ g.charsMu.Unlock()
+ if sess != nil {
+ privateMessage := event.PrivateMessage
+ if category == "casino_menu" {
+ privateMessage = g.casinoMenuBalance(sess, privateMessage)
+ }
+ sess.WriteLine(g.colorize(sess, category, privateMessage))
+ if category == "casino_menu" {
+ g.writePrompt(sess)
+ }
+ }
+ }
+ if event.PrivateTo != "" {
+ g.charsMu.Lock()
+ sess := g.loggedInChars[event.PrivateTo]
+ g.charsMu.Unlock()
+ if sess != nil && event.PrivateMessage == "" {
+ privateMessage := event.Message
+ if category == "casino_menu" {
+ privateMessage = g.casinoMenuBalance(sess, privateMessage)
+ }
+ sess.WriteLine(g.colorize(sess, category, privateMessage))
+ if category == "casino_menu" {
+ g.writePrompt(sess)
+ }
+ }
+ }
+ }
+}
+
+func casinoEventColor(eventType casino.EventType) string {
+ if eventType == casino.EventPayout {
+ return "casino_win"
+ }
+ return "casino_action"
+}
+
+func (g *Game) casinoMenuBalance(sess *net.Session, message string) string {
+ if sess == nil || sess.Player == nil {
+ return message
+ }
+ chips := g.colorize(sess, "casino_chips", fmt.Sprintf("%d", countChips(sess.Player)))
+ credits := g.colorize(sess, "casino_credits", fmt.Sprintf("%d", sess.Player.Credits))
+ line := fmt.Sprintf("You have %s chips and %s credits.", chips, credits)
+ return strings.Replace(message, "Slot machine commands:", line+"\n\nSlot machine commands:", 1)
+}
+
+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]
+ }
+ line += " " + colorSlotSymbol(mode, value) + strings.Repeat(" ", 7-len(value)) + " " + 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 = color.NoColor()
+ spec.Gradient = []int{220, 226}
+ case "stick":
+ spec = color.NoColor()
+ spec.Gradient = []int{34, 46}
+ case "brick":
+ spec = color.NoColor()
+ spec.Gradient = []int{160, 196}
+ case "hat":
+ spec = color.NoColor()
+ spec.Gradient = []int{129, 201}
+ case "wolf":
+ spec = color.NoColor()
+ spec.Gradient = []int{27, 39}
+ case "scatter":
+ spec = color.NoColor()
+ spec.Gradient = []int{196, 220, 46, 51, 129, 201}
+ default:
+ spec.Dim = true
+ }
+ return color.Render(mode, spec, symbol)
+}
diff --git a/internal/game/casino_test.go b/internal/game/casino_test.go
new file mode 100644
index 0000000..b2b37c4
--- /dev/null
+++ b/internal/game/casino_test.go
@@ -0,0 +1,29 @@
+package game
+
+import (
+ "strings"
+ "testing"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/color"
+)
+
+func TestRenderSlotSnapshotColorsWordSymbols(t *testing.T) {
+ snapshot := &casino.SlotSnapshot{StoppedReels: 1}
+ snapshot.Grid[0][0] = "scatter"
+ if !strings.Contains(renderSlotSnapshot(snapshot, "none", false), "scatter") {
+ t.Fatal("expected word-based slot symbols")
+ }
+ output := renderSlotSnapshot(snapshot, "xterm256", true)
+ if !strings.Contains(output, "\033[38;5;") {
+ t.Fatalf("expected colored slot symbols, got %q", output)
+ }
+ for _, line := range strings.Split(output, "\n") {
+ if color.VisibleLen(line) != 51 {
+ t.Fatalf("line visible width = %d, want 51: %q", color.VisibleLen(line), line)
+ }
+ }
+ if !strings.Contains(output, "┌") || !strings.Contains(output, "└") {
+ t.Fatalf("slot output lacks the Unicode frame: %q", output)
+ }
+}
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index 761db2f..4772a2c 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -13,6 +13,10 @@ import (
func (g *Game) doMove(sess *net.Session, dir string, isWalk bool) {
p := sess.Player
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine("You're playing a game. Type \"stop\" between rounds before moving.")
+ return
+ }
exitDir := g.World.ResolveExit(dir)
if exitDir == "" {
sess.WriteLine("Go where?")
diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go
index 12d3879..b7ec972 100644
--- a/internal/game/cmd_quit.go
+++ b/internal/game/cmd_quit.go
@@ -11,6 +11,10 @@ func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) {
func (g *Game) doQuit(sess *net.Session) {
p := sess.Player
+ if _, playing := g.Casino.Participation(p.Name); playing {
+ sess.WriteLine(g.colorize(sess, "warning", "WARNING: You're in the middle of a game!"))
+ g.emitCasinoEvents(g.Casino.MarkDisconnected(p.Name))
+ }
if g.Combat.Get(p.Name) != nil {
sess.WriteLine("You can't rest during combat!")
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index d03993a..4c41216 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -23,6 +23,11 @@ var commandRegistry = map[string]commandDef{
"attack": {(*Game).executeAttack, ClassActive},
"kill": {(*Game).executeAttack, ClassActive},
"work": {(*Game).executeWork, ClassActive},
+ "play": {(*Game).executePlay, ClassActive},
+ "bet": {(*Game).executeBet, ClassActive},
+ "spin": {(*Game).executeBet, ClassActive},
+ "autobet": {(*Game).executeAutoBet, ClassActive},
+ "autospin": {(*Game).executeAutoBet, ClassActive},
"style": {(*Game).executeStyle, ClassInstant},
"look": {(*Game).executeLook, ClassInstant},
"l": {(*Game).executeLook, ClassInstant},
diff --git a/internal/game/cmd_stop.go b/internal/game/cmd_stop.go
index d1b8faa..5c347e6 100644
--- a/internal/game/cmd_stop.go
+++ b/internal/game/cmd_stop.go
@@ -6,6 +6,18 @@ import (
func (g *Game) executeStop(sess *net.Session, args []string, rawInput string) {
p := sess.Player
+ if table, playing := g.Casino.Participation(p.Name); playing {
+ if table != nil {
+ g.emitCasinoEvents(g.Casino.Leave(table.RoomID, table.Config.ID, p.Name))
+ } else if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ if events, stopped := g.Casino.StopMachineAutoBet(machine.RoomID, machine.Config.ID, p.Name); stopped {
+ g.emitCasinoEvents(events)
+ return
+ }
+ g.emitCasinoEvents(g.Casino.LeaveMachine(machine.RoomID, machine.Config.ID, p.Name))
+ }
+ return
+ }
ss, _ := g.safespot.Get(p.Name)
if p.Action == nil && p.BackgroundAction == nil && g.Combat.Get(p.Name) == nil && p.MoveTicks == 0 && len(p.WalkSequence) == 0 && !ss.Active {
sess.WriteLine("You're not doing anything.")
diff --git a/internal/game/core_login_char.go b/internal/game/core_login_char.go
index 38499e2..4f59f1b 100644
--- a/internal/game/core_login_char.go
+++ b/internal/game/core_login_char.go
@@ -108,6 +108,7 @@ func (g *Game) connectCharacter(sess *net.Session, name string) {
if g.Hub != nil {
g.Hub.EnterRoom(sess, p.RoomID)
}
+ g.Casino.MarkConnected(name)
g.doLook(sess)
g.checkAggro(sess)
diff --git a/internal/game/game.go b/internal/game/game.go
index 700877b..cac0ec9 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -7,6 +7,7 @@ import (
"sync"
"time"
+ "thehouseoficarus/internal/casino"
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/config"
"thehouseoficarus/internal/engine"
@@ -31,13 +32,13 @@ const (
// constructed once at startup and never replaced. Deps is embedded in Game, so
// game code accesses them directly (e.g. g.World, g.ItemStore).
type Deps struct {
- World *world.World
- ObjectStore *object.ObjectStore
- ItemStore *item.ItemStore
- AccountStore *player.AccountStore
- MobStore *world.MobStore
- CraftIndex *CraftIndex
- CourseStore *CourseStore
+ World *world.World
+ ObjectStore *object.ObjectStore
+ ItemStore *item.ItemStore
+ AccountStore *player.AccountStore
+ MobStore *world.MobStore
+ CraftIndex *CraftIndex
+ CourseStore *CourseStore
Ticks *engine.Engine
ColorConfig *config.ColorsConfig
ConstantColorConfig *config.ColorsConfig
@@ -53,6 +54,7 @@ type Game struct {
Hub *net.Hub
GlobalFlags *GlobalFlagStore
Combat *combat.Tracker
+ Casino *casino.Manager
flagIndex *flagTriggerIndex
queue *CommandQueue
safespot *SafespotManager
@@ -83,7 +85,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *
ObjectStore: object.NewObjectStore(dataDir),
ItemStore: item.NewItemStore(dataDir),
AccountStore: player.NewAccountStore(dataDir),
- MobStore: world.NewMobStore(dataDir),
+ MobStore: world.NewMobStore(dataDir),
CraftIndex: NewCraftIndex(),
CourseStore: NewCourseStore(dataDir),
Ticks: engine.New(),
@@ -93,6 +95,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *
},
GlobalFlags: NewGlobalFlagStore(),
Combat: combat.NewTracker(),
+ Casino: casino.NewManager(time.Now().UnixNano()),
flagIndex: newFlagTriggerIndex(),
queue: NewCommandQueue(),
safespot: NewSafespotManager(),
@@ -131,6 +134,11 @@ func (g *Game) SetHub(hub *net.Hub) {
}
}
})
+ hub.OnDisconnect(func(sess *net.Session) {
+ if sess.Player != nil {
+ g.emitCasinoEvents(g.Casino.MarkDisconnected(sess.Player.Name))
+ }
+ })
g.GlobalFlags.OnChange(func(name string, value any) {
g.fireGlobalFlagTriggers(name, value)
})
@@ -339,8 +347,9 @@ func (g *Game) ProcessQueuedCommands() {
ss, _ := g.safespot.Get(p.Name)
isHiding := ss.Active
isBusy := p.Action != nil || len(p.WalkSequence) > 0 || g.Combat.Get(p.Name) != nil || p.MoveTicks > 0 || isHiding
+ isCasinoSpin := g.Casino != nil && g.Casino.MachineBusy(p.Name)
_, isResting := g.restTimers[p.Name]
- if !isResting && !isBusy && qc.Session.State == net.StateGame {
+ if !isResting && !isBusy && !isCasinoSpin && qc.Session.State == net.StateGame {
g.writePrompt(qc.Session)
}
}
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index dd2cdf0..4ed47c9 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -5,6 +5,7 @@ import (
"sort"
"strings"
+ "thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
@@ -73,7 +74,7 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo
func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) []string {
objs := g.World.AllObjInstances(p.RoomID)
if len(objs) == 0 {
- return g.showFarmPatches(sess, p)
+ return appendRoomCasinos(g.showFarmPatches(sess, p), g.showRoomCasinos(sess, room))
}
var lines []string
lines = append(lines, "")
@@ -220,9 +221,43 @@ func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.
}
lines = append(lines, g.showFarmPatches(sess, p)...)
+ return appendRoomCasinos(lines, g.showRoomCasinos(sess, room))
+}
+
+func (g *Game) showRoomCasinos(sess *net.Session, room *world.Room) []string {
+ if room == nil || (len(room.CasinoTables) == 0 && len(room.CasinoMachines) == 0) {
+ return nil
+ }
+ var lines []string
+ for _, table := range room.CasinoTables {
+ name := casinoGameLabel(table.Game) + " table"
+ lines = append(lines, fmt.Sprintf("A %s is here.", g.colorize(sess, "casino_action", name)))
+ }
+ for _, machine := range room.CasinoMachines {
+ name := casinoGameLabel(machine.Game) + " machine"
+ lines = append(lines, fmt.Sprintf("A %s is here.", g.colorize(sess, "casino_action", name)))
+ }
return lines
}
+func appendRoomCasinos(lines, casinos []string) []string {
+ if len(casinos) == 0 {
+ return lines
+ }
+ if len(lines) == 0 || lines[len(lines)-1] != "" {
+ lines = append(lines, "")
+ }
+ return append(lines, casinos...)
+}
+
+func casinoGameLabel(game casino.GameName) string {
+ label := strings.ReplaceAll(string(game), "_", " ")
+ if label == "" {
+ return "casino"
+ }
+ return label
+}
+
func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string {
ground := g.World.GroundItemsDetailed(p.RoomID)
if len(ground) == 0 {
diff --git a/internal/game/render_help.go b/internal/game/render_help.go
index 7b7efa2..8af3096 100644
--- a/internal/game/render_help.go
+++ b/internal/game/render_help.go
@@ -53,6 +53,10 @@ var commandList = []cmdEntry{
{"jack / jackin", "Active", "Jack into a terminal (Hacking)"},
{"look / l", "Instant", "Look around or examine things"},
{"map", "Instant", "Display an ASCII map of the area"},
+ {"play", "Active", "Join a casino game in the current room"},
+ {"bet", "Active", "Place a casino wager"},
+ {"autobet / autospin", "Active", "Automatically repeat a casino wager"},
+ {"spin", "Active", "Spin the current casino wager"},
{"mine", "Active", "Mine rocks (Mining)"},
{"mix", "Active", "Mix potions (Pharmacy)"},
{"mods / modlist", "Instant", "List available science modules"},
diff --git a/internal/game/tick_systems.go b/internal/game/tick_systems.go
index 4d8d4b6..b819454 100644
--- a/internal/game/tick_systems.go
+++ b/internal/game/tick_systems.go
@@ -27,6 +27,7 @@ func TickSystemOrder() []struct{ Name string } {
var tickSystemOrder = []tickSystem{
{"ProcessQueuedCommands", (*Game).ProcessQueuedCommands},
+ {"CasinoTick", (*Game).CasinoTick},
{"MoveTick", (*Game).MoveTick},
{"SequenceTick", (*Game).SequenceTick},
{"TransientMobTick", (*Game).TransientMobTick},
@@ -57,4 +58,4 @@ func (g *Game) RunTickSystems() {
for _, s := range tickSystemOrder {
s.Run(g)
}
-} \ No newline at end of file
+}
diff --git a/internal/game/tick_systems_test.go b/internal/game/tick_systems_test.go
index 6b44e91..2fa67ac 100644
--- a/internal/game/tick_systems_test.go
+++ b/internal/game/tick_systems_test.go
@@ -22,6 +22,7 @@ import (
// contract.
var expectedTickSystemOrder = []string{
"ProcessQueuedCommands",
+ "CasinoTick",
"MoveTick",
"SequenceTick",
"TransientMobTick",
@@ -144,4 +145,4 @@ func newRunTickSystemsTestGame() *Game {
hackingStates: map[string]*hacking.Session{},
sequences: map[string]*sequence{},
}
-} \ No newline at end of file
+}