469 lines
13 KiB
Go
Executable File
469 lines
13 KiB
Go
Executable File
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 自动发现和运行
|
||
}
|