diff options
| author | historia <[not public]> | 2026-09-16 21:35:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-16 21:35:55 -0400 |
| commit | fea2cc6839ec3ee17940a2dea69622dc6f4deabf (patch) | |
| tree | 9d893525ca20d2cc920e9e866f84f71b605d8ae9 /internal/game/production_combine_regression_test.go | |
| parent | 35ddd0ad2215760bb1d3dc7724480f179ea33fa6 (diff) | |
| download | thehouseoficarus-fea2cc6839ec3ee17940a2dea69622dc6f4deabf.tar.gz | |
fix: unify crafting ingredient resolution; fix superheat alternatives, quantity gates, and dropped combine byproducts
Diffstat (limited to 'internal/game/production_combine_regression_test.go')
| -rw-r--r-- | internal/game/production_combine_regression_test.go | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/internal/game/production_combine_regression_test.go b/internal/game/production_combine_regression_test.go new file mode 100644 index 0000000..c617565 --- /dev/null +++ b/internal/game/production_combine_regression_test.go @@ -0,0 +1,154 @@ +package game + +// End-to-end regression: a combine craft with alternative ingredients must +// consume the alternative the availability check verified. Before the fix, +// combine consumption debited the first alternative with ANY stock and +// ignored a failed RemoveItem: a player holding a short first alternative +// and a full second one produced output while keeping the second +// alternative and losing the short first one. + +import ( + "os" + "path/filepath" + "testing" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +const testDoughYAML = `id: test_dough +name: test dough +stackable: false +value: 2 +craft: + - type: combine + ingredients: + - items: + - pot_of_flour + quantity: 1 + - items: + - bucket_of_water + - jug_of_water + quantity: 2 + byproducts: + - empty_bucket + - empty_jug + output_qty: 1 +` + +// Minimal ingredient definitions so startup validation resolves the recipe. +var testDoughIngredients = map[string]string{ + "pot_of_flour": "pot of flour", + "bucket_of_water": "bucket of water", + "jug_of_water": "jug of water", + "empty_bucket": "empty bucket", + "empty_jug": "empty jug", +} + +func seedTestDoughWorld(t *testing.T, dir string) { + t.Helper() + seedWorld(t, dir) + for id, name := range testDoughIngredients { + path := filepath.Join(dir, "items", id+".yaml") + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + data := "id: " + id + "\nname: " + name + "\nstackable: false\nvalue: 1\n" + if err := os.WriteFile(path, []byte(data), 0644); err != nil { + t.Fatal(err) + } + } + path := filepath.Join(dir, "items", "test_dough.yaml") + if err := os.WriteFile(path, []byte(testDoughYAML), 0644); err != nil { + t.Fatal(err) + } +} + +func TestCombineAlternativesConsumeVerifiedSource(t *testing.T) { + dir := t.TempDir() + seedTestDoughWorld(t, dir) + + h := harnessFromDir(t, dir) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + + // Ingredients and byproducts were seeded as real item definitions. + h.g.RunOnOwner(func() { + p, err := h.g.AccountStore.LoadCharacter("alice") + if err != nil { + t.Error(err) + return + } + p.SetInvSlot(0, &player.InventorySlot{ItemID: "pot_of_flour", Quantity: 1}) + p.SetInvSlot(1, &player.InventorySlot{ItemID: "bucket_of_water", Quantity: 1}) + p.SetInvSlot(2, &player.InventorySlot{ItemID: "jug_of_water", Quantity: 2}) + dough, _ := h.g.ItemStore.Load("test_dough") + if dough == nil || dough.FirstCraft() == nil { + t.Error("test_dough recipe missing from the craft index") + return + } + + conn := newChanConn() + sess := &net.Session{Conn: conn, Player: p} + sess.SetState(net.StateGame) + + h.g.startCombine(sess, p, dough, dough.FirstCraft(), 1) + if p.Action == nil { + t.Error("combine action did not start") + return + } + // Phase 0 announces, phase 1 completes steps, phase 2 consumes and + // produces. + h.g.advanceCombine(sess, p) // phase 0 -> 1 + h.g.advanceCombine(sess, p) // phase 1 -> 2 + h.g.advanceCombine(sess, p) // phase 2: consume + output + + if got := p.CountItem("test_dough"); got != 1 { + t.Errorf("test_dough = %d, want 1", got) + } + // The verified alternative (the jug, 2 >= 2) is consumed in full; + // the short bucket stays untouched. + if got := p.CountItem("jug_of_water"); got != 0 { + t.Errorf("jug_of_water = %d, want 0", got) + } + if got := p.CountItem("bucket_of_water"); got != 1 { + t.Errorf("bucket_of_water = %d, want 1 (short alternative untouched)", got) + } + if got := p.CountItem("pot_of_flour"); got != 0 { + t.Errorf("pot_of_flour = %d, want 0", got) + } + // The byproduct matches the consumed alternative. + if got := p.CountItem("empty_jug"); got != 1 { + t.Errorf("empty_jug = %d, want 1", got) + } + }) +} + +func TestCombineAlternativesRefuseShortStock(t *testing.T) { + dir := t.TempDir() + seedTestDoughWorld(t, dir) + + h := harnessFromDir(t, dir) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + + h.g.RunOnOwner(func() { + p, err := h.g.AccountStore.LoadCharacter("alice") + if err != nil { + t.Error(err) + return + } + // 1 bucket + 1 jug: neither alternative covers quantity 2. + p.SetInvSlot(0, &player.InventorySlot{ItemID: "pot_of_flour", Quantity: 1}) + p.SetInvSlot(1, &player.InventorySlot{ItemID: "bucket_of_water", Quantity: 1}) + p.SetInvSlot(2, &player.InventorySlot{ItemID: "jug_of_water", Quantity: 1}) + dough, _ := h.g.ItemStore.Load("test_dough") + if dough == nil { + t.Error("test_dough missing") + return + } + if craftHasAllItemsQty(dough.FirstCraft(), p.CountItem) { + t.Error("1 bucket + 1 jug must not satisfy a quantity-2 water entry") + } + }) +} |
