aboutsummaryrefslogtreecommitdiff
path: root/internal/game/production_combine_regression_test.go
blob: c617565768b56f848410eecb35656f30d80fc06a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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")
		}
	})
}