game/server/logic/scenario_test.go
2026-04-20 16:07:22 +08:00

469 lines
13 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 自动发现和运行
}