package logic import ( "fmt" "testing" "wuziqi-server/characters" "wuziqi-server/core" "wuziqi-server/items" ) // ======================================== // 游戏场景测试框架 // ======================================== // GameScenario 定义一个游戏测试场景 type GameScenario struct { Name string Players []PlayerSetup Grid []CellSetup Actions []GameAction Checks []ScenarioCheck } // PlayerSetup 玩家配置 type PlayerSetup struct { ID string Character string HP int // 可选状态 Shield bool Poisoned bool Curse bool Revive bool SkipTurn bool TimeBomb int } // CellSetup 格子配置 type CellSetup struct { Index int Type string // "empty", "bomb", "item" ItemID string // 如果是 item NeighborBombs int } // GameAction 游戏动作 type GameAction struct { Type string // "move", "damage", "heal", "use_item" PlayerID string Value int // 格子索引或伤害值 ItemID string // 如果是 use_item } // ScenarioCheck 场景检查点 type ScenarioCheck struct { PlayerID string Field string // "hp", "shield", "poisoned", "revive", "alive" Expected interface{} Message string } // RunScenario 运行一个游戏场景 func RunScenario(t *testing.T, scenario GameScenario) { t.Run(scenario.Name, func(t *testing.T) { engine, state := createScenarioState(scenario) // 执行动作 for i, action := range scenario.Actions { executeAction(t, engine, state, action, i) } // 验证结果 for _, check := range scenario.Checks { verifyCheck(t, state, check) } }) } func createScenarioState(scenario GameScenario) (*GameEngine, *core.GameState) { logger := &MockLogger{} dispatcher := &MockDispatcher{} charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil, "test-match") // 创建玩家 players := make(map[string]*core.Player) turnOrder := []string{} for _, ps := range scenario.Players { hp := ps.HP if hp == 0 { hp = charMgr.GetInitialHP(ps.Character, 4) } p := &core.Player{ UserID: ps.ID, Username: ps.ID, Character: ps.Character, HP: hp, MaxHP: hp, Shield: ps.Shield, Poisoned: ps.Poisoned, Curse: ps.Curse, Revive: ps.Revive, SkipTurn: ps.SkipTurn, TimeBombTurns: ps.TimeBomb, RevealedCells: make(map[int]string), } players[ps.ID] = p turnOrder = append(turnOrder, ps.ID) } // 创建网格 (默认 10x10) gridSize := 10 grid := make([]*core.GridCell, gridSize*gridSize) for i := range grid { grid[i] = &core.GridCell{Type: "empty", Revealed: false} } // 应用自定义格子设置 for _, cs := range scenario.Grid { if cs.Index < len(grid) { grid[cs.Index].Type = cs.Type grid[cs.Index].ItemID = cs.ItemID grid[cs.Index].NeighborBombs = cs.NeighborBombs } } state := &core.GameState{ Players: players, Grid: grid, GridSize: gridSize, TurnOrder: turnOrder, CurrentTurnIndex: 0, GameStarted: true, } return engine, state } func executeAction(t *testing.T, engine *GameEngine, state *core.GameState, action GameAction, index int) { player := state.Players[action.PlayerID] if player == nil && action.PlayerID != "" { t.Fatalf("Action %d: Player %s not found", index, action.PlayerID) } switch action.Type { case "move": // 设置当前回合为该玩家 for i, uid := range state.TurnOrder { if uid == action.PlayerID { state.CurrentTurnIndex = i break } } engine.HandleMove(state, action.PlayerID, action.Value) case "damage": engine.ApplyDamage(state, player, action.Value, false) case "heal": engine.HealPlayer(player, action.Value) case "use_item": ctx := items.ItemContext{ Logger: engine.Logger, Dispatcher: engine.Dispatcher, Logic: engine, } engine.ItemManager.UseItem(state, player, action.ItemID, ctx) case "advance_turn": engine.AdvanceTurn(state) case "check_game_over": engine.CheckGameOver(state) } } func verifyCheck(t *testing.T, state *core.GameState, check ScenarioCheck) { player := state.Players[check.PlayerID] if player == nil { t.Fatalf("Check failed: Player %s not found", check.PlayerID) } var actual interface{} switch check.Field { case "hp": actual = player.HP case "shield": actual = player.Shield case "poisoned": actual = player.Poisoned case "curse": actual = player.Curse case "revive": actual = player.Revive case "alive": actual = player.HP > 0 case "skip_turn": actual = player.SkipTurn case "current_turn": // 获取当前回合玩家的ID currentUID := state.TurnOrder[state.CurrentTurnIndex] actual = (currentUID == check.PlayerID) default: t.Fatalf("Unknown check field: %s", check.Field) } if actual != check.Expected { msg := check.Message if msg == "" { msg = fmt.Sprintf("Player %s.%s", check.PlayerID, check.Field) } t.Errorf("%s: expected %v, got %v", msg, check.Expected, actual) } } // ======================================== // 具体测试场景 // ======================================== func TestScenario_SlothBombDamageReduction(t *testing.T) { RunScenario(t, GameScenario{ Name: "树懒踩炸弹只受1点伤害", Players: []PlayerSetup{ {ID: "sloth1", Character: "sloth", HP: 4}, {ID: "dog1", Character: "dog", HP: 4}, }, Grid: []CellSetup{ {Index: 0, Type: "bomb"}, {Index: 1, Type: "bomb"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "sloth1", Value: 0}, // 树懒踩炸弹 {Type: "move", PlayerID: "dog1", Value: 1}, // 狗踩炸弹 }, Checks: []ScenarioCheck{ {PlayerID: "sloth1", Field: "hp", Expected: 3, Message: "树懒踩炸弹应该只受1点伤害"}, {PlayerID: "dog1", Field: "hp", Expected: 2, Message: "狗踩炸弹应该受2点伤害"}, }, }) } func TestScenario_CatDamageCap(t *testing.T) { RunScenario(t, GameScenario{ Name: "猫咪所有伤害强制为1", Players: []PlayerSetup{ {ID: "cat1", Character: "cat", HP: 3}, {ID: "tiger1", Character: "tiger", HP: 4}, }, Grid: []CellSetup{ {Index: 0, Type: "bomb"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "cat1", Value: 0}, // 猫踩炸弹(2伤害->1) }, Checks: []ScenarioCheck{ {PlayerID: "cat1", Field: "hp", Expected: 2, Message: "猫咪受到炸弹伤害应该被限制为1"}, }, }) } func TestScenario_CatWithCurse(t *testing.T) { RunScenario(t, GameScenario{ Name: "猫咪带诅咒也只受1点伤害", Players: []PlayerSetup{ {ID: "cat1", Character: "cat", HP: 3, Curse: true}, }, Grid: []CellSetup{ {Index: 0, Type: "bomb"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "cat1", Value: 0}, }, Checks: []ScenarioCheck{ {PlayerID: "cat1", Field: "hp", Expected: 2, Message: "猫咪带诅咒也只受1点伤害"}, {PlayerID: "cat1", Field: "curse", Expected: false, Message: "诅咒应该被消耗"}, }, }) } func TestScenario_ElephantItemRestriction(t *testing.T) { RunScenario(t, GameScenario{ Name: "大象无法使用医疗包/好人卡/复活甲", Players: []PlayerSetup{ {ID: "elephant1", Character: "elephant", HP: 4}, // 大象5血,先扣1测试 }, Grid: []CellSetup{ {Index: 0, Type: "item", ItemID: "medkit"}, {Index: 1, Type: "item", ItemID: "skip"}, {Index: 2, Type: "item", ItemID: "revive"}, }, Actions: []GameAction{ {Type: "damage", PlayerID: "elephant1", Value: 1}, // 先扣1血 {Type: "move", PlayerID: "elephant1", Value: 0}, // 尝试使用医疗包 }, Checks: []ScenarioCheck{ {PlayerID: "elephant1", Field: "hp", Expected: 3, Message: "大象无法使用医疗包,HP应该保持3"}, }, }) } func TestScenario_TigerKnifeAOE(t *testing.T) { RunScenario(t, GameScenario{ Name: "老虎在场时飞刀变为全体2点伤害", Players: []PlayerSetup{ {ID: "dog1", Character: "dog", HP: 4}, {ID: "tiger1", Character: "tiger", HP: 4}, {ID: "cat1", Character: "cat", HP: 3}, }, Grid: []CellSetup{ {Index: 0, Type: "item", ItemID: "knife"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "dog1", Value: 0}, // 狗使用飞刀 }, Checks: []ScenarioCheck{ // 老虎在场,飞刀变为全体2点伤害 {PlayerID: "tiger1", Field: "hp", Expected: 2, Message: "老虎应该受到2点伤害"}, {PlayerID: "cat1", Field: "hp", Expected: 2, Message: "猫咪应该受到1点伤害(猫咪技能)"}, {PlayerID: "dog1", Field: "hp", Expected: 4, Message: "使用者不受伤害"}, }, }) } func TestScenario_HippoCannotPickItems(t *testing.T) { RunScenario(t, GameScenario{ Name: "河马无法拾取道具", Players: []PlayerSetup{ {ID: "hippo1", Character: "hippo", HP: 4}, }, Grid: []CellSetup{ {Index: 0, Type: "item", ItemID: "shield"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "hippo1", Value: 0}, }, Checks: []ScenarioCheck{ {PlayerID: "hippo1", Field: "shield", Expected: false, Message: "河马无法获得护盾"}, }, }) } func TestScenario_SlothPoisonImmune(t *testing.T) { RunScenario(t, GameScenario{ Name: "树懒免疫毒药", Players: []PlayerSetup{ {ID: "sloth1", Character: "sloth", HP: 4}, {ID: "dog1", Character: "dog", HP: 4}, }, Grid: []CellSetup{ {Index: 0, Type: "item", ItemID: "poison"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "dog1", Value: 0}, // 狗使用毒药,目标随机 }, Checks: []ScenarioCheck{ {PlayerID: "sloth1", Field: "poisoned", Expected: false, Message: "树懒应该免疫毒药"}, }, }) } func TestScenario_ReviveOnDeath(t *testing.T) { RunScenario(t, GameScenario{ Name: "复活甲免疫死亡", Players: []PlayerSetup{ {ID: "dog1", Character: "dog", HP: 1, Revive: true}, }, Grid: []CellSetup{ {Index: 0, Type: "bomb"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "dog1", Value: 0}, // 踩炸弹,本应死亡 }, Checks: []ScenarioCheck{ {PlayerID: "dog1", Field: "hp", Expected: 1, Message: "复活甲应该保留1点HP"}, {PlayerID: "dog1", Field: "revive", Expected: false, Message: "复活甲应该被消耗"}, {PlayerID: "dog1", Field: "alive", Expected: true, Message: "玩家应该存活"}, }, }) } func TestScenario_GameOver(t *testing.T) { RunScenario(t, GameScenario{ Name: "只剩一人时游戏结束", Players: []PlayerSetup{ {ID: "p1", Character: "dog", HP: 4}, {ID: "p2", Character: "cat", HP: 1}, }, Grid: []CellSetup{ {Index: 0, Type: "bomb"}, }, Actions: []GameAction{ {Type: "move", PlayerID: "p2", Value: 0}, // p2 踩炸弹死亡 {Type: "check_game_over"}, }, Checks: []ScenarioCheck{ {PlayerID: "p2", Field: "alive", Expected: false, Message: "p2应该死亡"}, {PlayerID: "p1", Field: "alive", Expected: true, Message: "p1应该存活"}, }, }) } func TestScenario_SafeAreaExpansion(t *testing.T) { // 测试安全区扩散 logger := &MockLogger{} dispatcher := &MockDispatcher{} charMgr := characters.NewCharacterManager(nil) itemMgr := items.NewItemManager() engine := NewGameEngine(logger, dispatcher, charMgr, itemMgr, nil, nil, "test-match") // 创建简单网格测试扩散 // 布局 (3x3): // [0:空] [1:空] [2:空] // [3:空] [4:空] [5:数字1] // [6:空] [7:数字1] [8:炸弹] gridSize := 3 grid := make([]*core.GridCell, gridSize*gridSize) for i := range grid { grid[i] = &core.GridCell{Type: "empty", Revealed: false, NeighborBombs: 0} } grid[8].Type = "bomb" // 计算邻居炸弹数 grid[5].NeighborBombs = 1 // 邻居有 [8:炸弹] grid[7].NeighborBombs = 1 // 邻居有 [8:炸弹] grid[4].NeighborBombs = 1 // 邻居有 [8:炸弹] player := &core.Player{UserID: "p1", Username: "P1", HP: 4, MaxHP: 4, Character: "dog", RevealedCells: make(map[int]string)} state := &core.GameState{ Players: map[string]*core.Player{"p1": player}, Grid: grid, GridSize: gridSize, TurnOrder: []string{"p1"}, CurrentTurnIndex: 0, GameStarted: true, } // 点击左上角 (0),应该扩散到所有空白格和边界数字格 engine.HandleMove(state, "p1", 0) // 检查揭示情况 revealed := []int{} for i, cell := range grid { if cell.Revealed { revealed = append(revealed, i) } } t.Logf("揭示的格子: %v", revealed) // 应该揭示: 0, 1, 2, 3, 4, 5, 6, 7 (除了炸弹8) if len(revealed) < 5 { t.Errorf("安全区扩散应该揭示更多格子,只揭示了 %d 个", len(revealed)) } // 炸弹不应该被揭示 if grid[8].Revealed { t.Error("炸弹不应该被揭示") } } // ======================================== // 运行所有场景测试 // ======================================== func TestAllScenarios(t *testing.T) { t.Log("运行所有游戏场景测试...") // 各个场景测试函数会被 Go test 自动发现和运行 }