package logic import ( "testing" "wuziqi-server/characters" "wuziqi-server/core" "wuziqi-server/items" "github.com/heroiclabs/nakama-common/runtime" ) // --- Mocks --- type MockLogger struct { runtime.Logger } func (m *MockLogger) Info(format string, v ...interface{}) {} func (m *MockLogger) Warn(format string, v ...interface{}) {} func (m *MockLogger) Error(format string, v ...interface{}) {} func (m *MockLogger) Debug(format string, v ...interface{}) {} type MockDispatcher struct { runtime.MatchDispatcher LastOpCode int64 LastData []byte } func (m *MockDispatcher) BroadcastMessage(opCode int64, data []byte, presences []runtime.Presence, sender runtime.Presence, reliable bool) error { m.LastOpCode = opCode m.LastData = data return nil } func createTestEngine() (*GameEngine, *core.GameState) { logger := &MockLogger{} dispatcher := &MockDispatcher{} charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil) // Create simplified state p1 := &core.Player{UserID: "p1", Username: "P1", HP: 4, MaxHP: 4, Character: "dog", RevealedCells: make(map[int]string)} p2 := &core.Player{UserID: "p2", Username: "P2", HP: 4, MaxHP: 4, Character: "cat", RevealedCells: make(map[int]string)} // Grid grid := make([]*core.GridCell, 10) for i := range grid { grid[i] = &core.GridCell{Type: "empty", Revealed: false} } state := &core.GameState{ Players: map[string]*core.Player{"p1": p1, "p2": p2}, Grid: grid, GridSize: 5, // irrelevant for simplified test TurnOrder: []string{"p1", "p2"}, CurrentTurnIndex: 0, GameStarted: true, } return engine, state } // --- Tests --- func TestApplyDamage(t *testing.T) { engine, state := createTestEngine() target := state.Players["p1"] // 1. Basic Damage engine.ApplyDamage(state, target, 1, false) if target.HP != 3 { t.Errorf("Expected 3 HP, got %d", target.HP) } // 2. Shield Block target.Shield = true engine.ApplyDamage(state, target, 10, false) if target.HP != 3 { t.Error("Shield should block damage") } if target.Shield { t.Error("Shield should be consumed") } // 3. Curse Damage (Non-Cat) target.Curse = true engine.ApplyDamage(state, target, 1, false) // (3 - 1*2) = 1 if target.HP != 1 { t.Errorf("Curse should double dmg, expected 1 HP, got %d", target.HP) } if target.Curse { t.Error("Curse should be consumed") } // 4. Cat Damage Cap cat := state.Players["p2"] // cat engine.ApplyDamage(state, cat, 100, false) if cat.HP != 3 { t.Errorf("Cat should take 1 dmg (4->3), got %d HP", cat.HP) } // 5. Revive target.Revive = true engine.ApplyDamage(state, target, 1000, false) // Kill if target.HP != 1 { t.Errorf("Revive should set HP to 1, got %d", target.HP) } if target.Revive { t.Error("Revive should be consumed") } } func TestAdvanceTurn(t *testing.T) { engine, state := createTestEngine() // Order: p1, p2 // Current: 0 (p1) // 1. Simple Advance engine.AdvanceTurn(state) if state.CurrentTurnIndex != 1 { // Should be p2 t.Errorf("Expected turn index 1 (p2), got %d", state.CurrentTurnIndex) } // 2. Wrap Around engine.AdvanceTurn(state) if state.CurrentTurnIndex != 0 { // Should be p1 t.Errorf("Expected turn index 0 (p1), got %d", state.CurrentTurnIndex) } // 3. Skip Turn state.Players["p2"].SkipTurn = true // Current p1. Advance -> p2 (skipped) -> p1 engine.AdvanceTurn(state) if state.CurrentTurnIndex != 0 { t.Errorf("Should skip p2 and return to p1, got index %d", state.CurrentTurnIndex) } if state.Players["p2"].SkipTurn { t.Error("SkipTurn flag should be cleared") } // 4. Time Bomb Logic p2 := state.Players["p2"] p2.TimeBombTurns = 1 // Verify p2 takes damage on next turn // Manually set turn to p1 so next is p2 state.CurrentTurnIndex = 0 engine.AdvanceTurn(state) // Move to p2. Bomb explodes. // Bomb is 2 damage. p2 started with 3 HP (cat took 1 earlier test? No new state here) // New state created fresh in this test func. p2 is fresh 4 HP Cat. // Wait, Cat takes 1 dmg from bomb too? // Logic says: `dmg := 2`. Then ApplyDamage. // Cat ApplyDamage -> 1. // So p2 should have 3 HP. // Wait, BombTimer logic in AdvanceTurn sets dmg=2. // ApplyDamage calls OnDamageTaken. // CharacterManager.OnDamageTaken for "cat" returns 1. // So Cat takes 1. if p2.HP != 3 { t.Errorf("Cat p2 should take 1 dmg from bomb, got HP %d", p2.HP) } if state.CurrentTurnIndex != 1 { t.Error("Should still be p2's turn after bomb (unless died)") } // 5. Poison Logic p1 := state.Players["p1"] p1.Poisoned = true p1.PoisonSteps = 0 state.CurrentTurnIndex = 1 // Set to p2 so next is p1 engine.AdvanceTurn(state) // Move to p1 // Poison steps increments to 1. No dmg (mod 2 == 0?) // Code: `PoisonSteps++` (becomes 1). `if PoisonSteps%2 == 0`. // 1 % 2 != 0. No damage. if p1.HP != 4 { // Fresh p1 has 4 t.Error("Poison should not trigger on step 1") } // Loop back to p1 again state.CurrentTurnIndex = 1 engine.AdvanceTurn(state) // Move to p1. Steps becomes 2. 2%2==0. Dmg 1. if p1.HP != 3 { t.Errorf("Poison should trigger on step 2, got HP %d", p1.HP) } } func TestHandleMove(t *testing.T) { engine, state := createTestEngine() // Setup grid state.Grid[0].Type = "bomb" state.Grid[1].Type = "empty" state.Grid[2].Type = "item" state.Grid[2].ItemID = "medkit" p1 := state.Players["p1"] state.CurrentTurnIndex = 0 // p1 turn // 1. Hit Bomb engine.HandleMove(state, "p1", 0) // Bomb dmg 2. p1 HP 4->2. if p1.HP != 2 { t.Errorf("Hit bomb, expected 2 HP, got %d", p1.HP) } // Turn should advance if state.CurrentTurnIndex != 1 { t.Error("Turn should advance after bomb") } // 2. Hit Item state.CurrentTurnIndex = 0 // Force p1 turn again engine.HandleMove(state, "p1", 2) // Medkit self heal logic in engine->ItemManager->Strategy // p1 HP 2->3. if p1.HP != 3 { t.Errorf("Item medkit should heal, expected 3 HP, got %d", p1.HP) } }