package game import ( "testing" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/prediction" ) // Regression: gather verbs must normalize to the gather action type, otherwise // startAction's bhType != verb gate rejects mine/chop/fish/cut outright. func TestGatherVerbsNormalizeToGatherAction(t *testing.T) { for _, verb := range []string{"mine", "chop", "fish", "cut", "shear"} { if got := normalizeVerb(verb); got != string(behavior.TypeGather) { t.Errorf("normalizeVerb(%q) = %q, want %q", verb, got, behavior.TypeGather) } } } // Regression: an unbounded step count must be rejected before it allocates. func TestParseWalkDirectionsRejectsHugeCount(t *testing.T) { if _, err := parseWalkDirections("999999999n"); err == nil { t.Fatal("expected an error for an unbounded step count") } dirs, err := parseWalkDirections("3n2e") if err != nil { t.Fatalf("parseWalkDirections: %v", err) } if len(dirs) != 5 { t.Fatalf("len(dirs) = %d, want 5", len(dirs)) } } // Regression: collectPredictions must not mark resolved contracts collected // when the credit cap blocks the payout, or the winnings are lost forever. func TestCollectPredictionsKeepsContractsWhenCreditCapBlocks(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") sess, _ := h.connect("alice") sess.Player.PredictionContracts = []prediction.Contract{ {MarketID: "m1", Status: "resolved", Proceeds: 1000}, } sess.Player.Credits = casino.MaxCredits h.g.RunOnOwner(func() { h.g.collectPredictions(sess) }) if got := sess.Player.PredictionContracts[0].Status; got != "resolved" { t.Fatalf("credit-cap collect changed status to %q; the payout would be lost", got) } // With room under the cap, the same contract must still be collectable. sess.Player.Credits = 0 h.g.RunOnOwner(func() { h.g.collectPredictions(sess) }) if got := sess.Player.PredictionContracts[0].Status; got != "collected" { t.Fatalf("status = %q, want collected", got) } if sess.Player.Credits != 1000 { t.Fatalf("credits = %d, want 1000", sess.Player.Credits) } } // Regression: a craft with no ingredient entries must be unsatisfiable, not // "always satisfied" (which would loop producing output for free). func TestPlanIngredientsRejectsEmptyCraft(t *testing.T) { c := &item.CraftDef{Type: "fletch"} if plan := planIngredients(c, func(string) int { return 999 }); plan != nil { t.Fatalf("empty craft produced a plan: %+v", plan) } } // Regression: a command reached through a prefix/abbreviation must not have its // arguments lowercased (global/symbol/prompt/addlocalobj read rawInput). func TestGlobalCommandPreservesArgumentCase(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") h.seedAccount("bob") h.seedCharacter("bob", "bob") alice, _ := h.connect("alice") _, bobConn := h.connect("bob") h.g.RunOnOwner(func() { h.g.HandleSessionOnOwner(alice, "glo Hello World") }) if !bobConn.Contains("Hello World") { t.Fatalf("global broadcast lost argument case: %s", bobConn.Bytes()) } } // Regression: `get 5 credits` used to sweep the whole purse headroom. func TestPickupCreditsHonorsRequestedQuantity(t *testing.T) { h := newHarness(t) h.seedAccount("alice") p := h.seedCharacter("alice", "alice") h.g.RunOnOwner(func() { p.Credits = 0 h.g.World.AddGroundItem(p.RoomID, "credits", 10) sess := &net.Session{Player: p, Conn: newChanConn()} h.g.pickupCredits(sess, p, "credits", 5) if p.Credits != 5 { t.Errorf("credits = %d, want 5 (output: %s)", p.Credits, sess.Conn.(*chanConn).Bytes()) } }) } // Regression: the player-file classifier must canonicalize repeated "/" and // "data/" prefixes so a raw-editor path cannot dodge the online check. // Regression: availability scans every recipe, but execution used Craft[0], so // a player satisfying a later recipe (e.g. oak arrow shafts) got a start-then- // abort. usableCraftIndex must pick the satisfied recipe. func TestUsableCraftIndexSelectsSatisfiedRecipe(t *testing.T) { g := &Game{} p := player.New("alice") def := &item.ItemDef{ ID: "arrow_shaft", Craft: item.CraftList{ {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"logs"}, Quantity: 1}}}, {Type: "fletching", Level: 1, Ingredients: []item.IngredientEntry{{Items: []string{"oak_logs"}, Quantity: 1}}}, }, } p.SetInvSlot(0, &player.InventorySlot{ItemID: "oak_logs", Quantity: 5}) if idx := g.usableCraftIndex(p, def); idx != 1 { t.Fatalf("usableCraftIndex = %d, want 1 (the satisfied oak recipe)", idx) } } func TestPlayerDataFileNameCanonicalizesPrefixes(t *testing.T) { for _, in := range []string{ "players/characters/alice.yaml", "//players/characters/alice.yaml", "data/data/players/characters/alice.yaml", "/data/players/characters/alice.yaml", } { name, kind, ok := playerDataFileName(in) if !ok || name != "alice" || kind != "characters" { t.Errorf("playerDataFileName(%q) = (%q, %q, %v), want (alice, characters, true)", in, name, kind, ok) } } }