package game import ( "strings" "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func TestRouletteNumericTargetsAndRendering(t *testing.T) { for _, double := range []bool{false, true} { g := &Game{Casino: casino.NewManager(1)} cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{DoubleZero: double}} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} for _, target := range []string{"0", "17", "00", "red", "row2"} { conn.data = nil g.executeBet(sess, []string{"10", "on", target}, "bet 10 on "+target) expected := "You don't have enough chips and credits" if target == "00" && !double { expected = "Unknown roulette target" } if !strings.Contains(string(conn.data), expected) { t.Fatalf("target=%s double=%v: %s", target, double, conn.data) } } conn.data = nil g.executeTableStatus(sess, []string{"rules"}, "table rules") if !strings.Contains(string(conn.data), "Straight numbers pay 35:1") || strings.Contains(string(conn.data), "Blackjack") { t.Fatal("wrong rules", string(conn.data)) } snapshot := &casino.RouletteSnapshot{DoubleZero: double, Phase: "playing", Players: []string{"alice"}} if got := g.renderCasinoSnapshot(sess, snapshot); got != renderRouletteSnapshot(snapshot, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size")) { t.Fatal("snapshot not rendered", got) } if !g.seesCasinoDetails(sess, casino.Event{RoomID: 1, TableID: "roulette", Snapshot: snapshot}) { t.Fatal("player cannot see wheel") } } } func TestRouletteCombinationTargetParsing(t *testing.T) { for _, double := range []bool{false, true} { g := &Game{Casino: casino.NewManager(1)} cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{DoubleZero: double}} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} // Multi-token targets are joined for roulette, with or without "on". valid := [][]string{ {"10", "on", "split", "1", "2"}, {"10", "on", "split", "1-2"}, {"10", "split", "1-2"}, {"10", "split", "1", "2"}, {"10", "on", "street", "1", "2", "3"}, {"10", "on", "street", "3", "1", "2"}, {"10", "on", "corner", "1", "2", "4", "5"}, {"10", "on", "corner", "5", "1", "4", "2"}, {"10", "on", "line", "1", "2", "3", "4", "5", "6"}, {"10", "on", "trio", "0", "1", "2"}, {"10", "on", "basket"}, } if double { valid = append(valid, []string{"10", "on", "split", "0", "00"}) } for _, args := range valid { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "You don't have enough chips and credits") { t.Fatalf("double=%v args=%v: expected wallet failure, got %q", double, args, conn.data) } } // Non-adjacent or wrong-shaped combinations must not reach the wallet. invalid := [][]string{ {"10", "on", "split", "1", "5"}, {"10", "on", "street", "2", "3", "4"}, {"10", "on", "corner", "1", "2", "3", "4"}, {"10", "on", "line", "1", "2", "3", "4", "5", "7"}, {"10", "on", "trio", "0", "1", "5"}, } if !double { invalid = append(invalid, []string{"10", "on", "split", "0", "00"}) } for _, args := range invalid { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "Unknown roulette target") { t.Fatalf("double=%v args=%v: expected target rejection, got %q", double, args, conn.data) } } } } func TestRouletteRacetrackTargetParsing(t *testing.T) { // A racetrack single-zero table accepts the call and neighbours syntaxes. g := &Game{Casino: casino.NewManager(1)} cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{Racetrack: true}} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} valid := [][]string{ {"10", "on", "voisins"}, {"10", "on", "voisins", "du", "zero"}, {"10", "on", "tiers"}, {"10", "on", "orphelins"}, {"10", "on", "zero"}, {"10", "on", "jeu", "zero"}, {"10", "on", "jeuzero"}, {"10", "on", "0"}, {"10", "on", "neighbors", "17"}, {"10", "on", "neighbors", "of", "17"}, } for _, args := range valid { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "You don't have enough chips and credits") { t.Fatalf("args=%v: expected wallet failure, got %q", args, conn.data) } } // The same names are unknown on a single-zero table without the option. plain := &Game{Casino: casino.NewManager(1)} plain.Casino.Join(1, casino.TableConfig{ID: "roulette", Game: casino.GameRoulette}, "bob") conn2 := &casinoTestConn{} sess2 := &net.Session{Player: player.New("bob"), Conn: conn2} plain.loggedInChars = map[string]*net.Session{"bob": sess2} for _, args := range [][]string{{"10", "on", "voisins"}, {"10", "on", "neighbors", "17"}} { conn2.data = nil plain.executeBet(sess2, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn2.data), "Unknown roulette target") { t.Fatalf("non-racetrack args=%v: expected rejection, got %q", args, conn2.data) } } } func TestCasinoTestPortalAuthoredContract(t *testing.T) { g, rooms, _ := loadVesta(t) found := false for _, obj := range rooms[1001].Objects { found = found || obj.ID == "casino_test_portal" } if !found { t.Fatal("portal missing from starting room") } obj, err := g.ObjectStore.Load("casino_test_portal") if err != nil { t.Fatal(err) } if len(obj.OnUse) != 1 || len(obj.OnUse[0].Steps) != 2 { t.Fatal("unexpected portal sequence") } steps := obj.OnUse[0].Steps if len(steps[0].DropTable) != 1 || steps[0].DropTable[0].ItemID != "chips" || steps[0].DropTable[0].Quantity != 1000 { t.Fatal("portal does not grant 1000 chips") } if steps[1].Teleport != 6100 || rooms[6100] == nil { t.Fatal("wrong casino destination") } for _, id := range []int{6182, 6183, 6184} { cfg := rooms[id].CasinoTables[0] if err := cfg.Validate(); err != nil { t.Fatal(err) } if cfg.Game != casino.GameRoulette || cfg.Roulette.DoubleZero != (id == 6183) { t.Fatal("wrong roulette rules", id) } if cfg.Roulette.Racetrack != (id == 6184) { t.Fatal("wrong racetrack setting", id) } } } // A wager must not re-send the betting board; the board arrives on join, // after settlement via the next-spin menu, or on an explicit "table". func TestRouletteBetDoesNotResendBoard(t *testing.T) { g, sess, conn := uxCasinoGame(t) cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette} g.emitCasinoEvents(g.Casino.Join(1, cfg, sess.Player.Name)) if !strings.Contains(string(conn.data), "1to12") { t.Fatalf("join did not draw the board: %q", conn.data) } conn.data = nil g.executeBet(sess, []string{"10", "on", "red"}, "bet 10 on red") text := string(conn.data) if strings.Contains(text, "1to12") { t.Fatalf("placing a wager redrew the board: %q", text) } if !strings.Contains(text, "You bet 10 on red") { t.Fatalf("missing wager confirmation: %q", text) } }