PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/tester/test_engine.go

https://bitbucket.org/lanior/mysterious-bomberman
Go | 916 lines | 790 code | 124 blank | 2 comment | 102 complexity | e431e17f6dd0bd1f5b948a0bf0ad0290 MD5 | raw file
  1. package main
  2. import (
  3. "fmt"
  4. "github.com/lanior/mysterious-bomberman/actor"
  5. "github.com/lanior/mysterious-bomberman/engine"
  6. . "github.com/lanior/mysterious-bomberman/server"
  7. . "github.com/lanior/mysterious-bomberman/tester/testlib"
  8. "regexp"
  9. "strings"
  10. "time"
  11. "unicode"
  12. )
  13. type gameHelper struct {
  14. Id int
  15. Settings *GameSettings
  16. Players []*actor.Actor
  17. Field []string
  18. State *WSGameState
  19. tick int
  20. playersMap []int
  21. }
  22. var emptyMap = []string{
  23. "............",
  24. "............",
  25. "....0>......",
  26. "......1.....",
  27. "............",
  28. "............",
  29. "............",
  30. }
  31. var playerPosRe = regexp.MustCompile(`\d`)
  32. func getInitialMapLine(line string) string {
  33. result := make([]byte, len(line)/2)
  34. for i := 0; i < len(line); i += 2 {
  35. if line[i] >= 'A' && line[i] <= 'Z' {
  36. result[i/2] = '%'
  37. } else if line[i] >= '0' && line[i] <= '9' {
  38. result[i/2] = byte(line[i+1])
  39. } else {
  40. result[i/2] = byte(line[i])
  41. }
  42. }
  43. return string(result)
  44. }
  45. var startedGameId int
  46. func newStartedGame(api *actor.Actor, seed int, field []string, settings *GameSettings) *gameHelper {
  47. playerCount := 0
  48. for _, row := range field {
  49. playerCount += len(playerPosRe.FindAllString(row, -1))
  50. }
  51. game := &gameHelper{
  52. Field: field,
  53. Settings: settings,
  54. Players: make([]*actor.Actor, playerCount),
  55. }
  56. startedGameId++
  57. playerNames := make([]string, playerCount)
  58. players := make([]*actor.Actor, playerCount)
  59. for i := 0; i < playerCount; i++ {
  60. playerNames[i] = fmt.Sprintf("gamer%d%d", i, startedGameId)
  61. players[i] = createUserAndLogin(api, playerNames[i], "asd")
  62. players[i].WSAuth()
  63. }
  64. host := players[0]
  65. mapId := host.Ok(&UploadMap{Name: "test map", Field: field, Settings: *settings}).(*UploadMapResponse).MapId
  66. game.Id = host.Ok(&CreateGame{
  67. Name: "test_game",
  68. Password: "",
  69. MapId: mapId,
  70. Settings: *settings,
  71. MaxPlayerCount: playerCount,
  72. }).(*CreateGameResponse).GameId
  73. host.Ok(&GameSetDebugMode{RandomSeed: seed})
  74. host.Ok(&StartGame{})
  75. for i := 1; i < playerCount; i++ {
  76. players[i].Ok(&JoinGame{GameId: game.Id})
  77. }
  78. for i := 1; i < playerCount; i++ {
  79. players[i].Ok(&StartGame{})
  80. }
  81. gameStartedEvents := make([]WSGameStarted, playerCount)
  82. playersByIndex := make([]int, playerCount)
  83. for i := 0; i < playerCount; i++ {
  84. players[i].ExpectEvent(&gameStartedEvents[i])
  85. playersByIndex[gameStartedEvents[i].CurrentPlayerIndex] = i
  86. }
  87. assert(len(gameStartedEvents[0].Players) == playerCount, "invalid player count")
  88. for _, playerName := range playerNames {
  89. found := false
  90. for _, name := range gameStartedEvents[0].Players {
  91. if name == playerName {
  92. found = true
  93. break
  94. }
  95. }
  96. assert(found, "player `%s` assumed to be in game", playerName)
  97. }
  98. game.State = host.Ok(&GameSetTick{Tick: 0}).(*GameSetTickResponse).GameState
  99. assert(game.State.Tick == 0, "expected tick 0")
  100. assert(len(game.State.Field) == len(field), "field height mismatch")
  101. for i, line := range game.State.Field {
  102. expected := getInitialMapLine(field[i])
  103. assert(line == expected, "field [line %d]: expected `%s`, got `%s`", i+1, expected, line)
  104. }
  105. game.playersMap = make([]int, playerCount)
  106. for i, player := range game.State.Players {
  107. index := -1
  108. if player.X%cellSize == 0 && player.Y%cellSize == 0 {
  109. x := player.X / cellSize
  110. y := player.Y / cellSize
  111. if x >= 0 && x < len(field[0]) && y >= 0 && y < len(field) {
  112. index = int(field[y][x*2] - '0')
  113. }
  114. }
  115. assert(index >= 0, "invalid player %d position", i)
  116. game.playersMap[index] = i
  117. game.Players[index] = players[playersByIndex[i]]
  118. }
  119. return game
  120. }
  121. func (game *gameHelper) Action(player int, dir, action string) {
  122. game.Player(player).Ok(
  123. &DebugGameAction{
  124. GameAction: WSGameAction{
  125. Direction: dir,
  126. Action: action,
  127. },
  128. },
  129. )
  130. }
  131. func (game *gameHelper) Tick(tickCount int) *WSGameState {
  132. game.tick += tickCount
  133. game.State = game.Player(0).Ok(&GameSetTick{Tick: game.tick}).(*GameSetTickResponse).GameState
  134. assert(game.State.Tick == game.tick, "game_set_tick: expected tick %d, got %d", game.tick, game.State.Tick)
  135. return game.State
  136. }
  137. func (game *gameHelper) GetMappedPlayerId(player int) int {
  138. return game.playersMap[player]
  139. }
  140. func (game *gameHelper) Player(player int) *actor.Actor {
  141. return game.Players[player]
  142. }
  143. func (game *gameHelper) PlayerState(player int) *GamePlayerState {
  144. return &game.State.Players[game.GetMappedPlayerId(player)]
  145. }
  146. func (game *gameHelper) PlayerStates() []*GamePlayerState {
  147. states := make([]*GamePlayerState, len(game.State.Players))
  148. for i := 0; i < len(states); i++ {
  149. states[i] = game.PlayerState(i)
  150. }
  151. return states
  152. }
  153. func (game *gameHelper) PlayerPos(player int) engine.Vector2 {
  154. return engine.Vector2{
  155. X: game.State.Players[game.GetMappedPlayerId(player)].X,
  156. Y: game.State.Players[game.GetMappedPlayerId(player)].Y,
  157. }
  158. }
  159. func (game *gameHelper) AssertPlayerPos(player int, expected engine.Vector2, format string, args ...interface{}) {
  160. assertPos(expected, game.PlayerPos(player), format, args...)
  161. }
  162. func assertPos(expected, got engine.Vector2, format string, args ...interface{}) {
  163. msg := fmt.Sprintf(format, args...)
  164. assert(expected == got, fmt.Sprintf("%s: expected pos (%d, %d), got (%d, %d)", msg, expected.X, expected.Y, got.X, got.Y))
  165. }
  166. func assertEqual(field string, expected, got int) {
  167. assert(expected == got, "expected %s = %d, got %d", field, expected, got)
  168. }
  169. func assertBombState(expected, got BombState) {
  170. assertPos(engine.Vector2{expected.X, expected.Y}, engine.Vector2{got.X, got.Y}, "bomb")
  171. assertEqual("created_at", expected.CreatedAt, got.CreatedAt)
  172. assertEqual("destroy_at", expected.DestroyAt, got.DestroyAt)
  173. assertEqual("flame_length", expected.FlameLength, got.FlameLength)
  174. // assertEqual("direction", expected.MovementDirection, got.MovementDirection)
  175. // assertEqual("speed", expected.Speed, got.Speed)
  176. }
  177. func assertExplosionState(expected, got ExplosionState) {
  178. assertPos(engine.Vector2{expected.X, expected.Y}, engine.Vector2{got.X, got.Y}, "explosion")
  179. assertEqual("created_at", expected.CreatedAt, got.CreatedAt)
  180. assertEqual("destroy_at", expected.DestroyAt, got.DestroyAt)
  181. assertEqual("up_length", expected.UpLength, got.UpLength)
  182. assertEqual("down_length", expected.DownLength, got.DownLength)
  183. assertEqual("left_length", expected.LeftLength, got.LeftLength)
  184. assertEqual("right_length", expected.RightLength, got.RightLength)
  185. }
  186. func testAllDirectionsWithPerpendiculars(fn func(dir, perpendicular engine.Vector2)) {
  187. forAllDirections(func(dir engine.Vector2) {
  188. fn(dir, dir.Perpendicular())
  189. fn(dir, dir.Perpendicular().ScaleBy(-1))
  190. })
  191. }
  192. func forAllDirections(fn func(dir engine.Vector2)) {
  193. fn(engine.Left)
  194. fn(engine.Right)
  195. fn(engine.Up)
  196. fn(engine.Down)
  197. }
  198. func cloneSettings(gs GameSettings) GameSettings {
  199. newSettings := gs
  200. newSettings.Items = map[string]ItemDescription{}
  201. for key, item := range gs.Items {
  202. newSettings.Items[key] = item
  203. }
  204. return newSettings
  205. }
  206. type obstacle struct {
  207. Code string
  208. Destructible bool
  209. }
  210. func generateExplosionTestMap(size, radius int, obs obstacle, direction engine.Vector2) (testMap []string) {
  211. obsCell := obs.Code
  212. if obs.Destructible {
  213. obsCell += "."
  214. } else {
  215. obsCell += obs.Code
  216. }
  217. lines := make([][]string, size)
  218. for j := range lines {
  219. lines[j] = make([]string, size)
  220. for i := range lines[j] {
  221. lines[j][i] = ".."
  222. }
  223. }
  224. lines[size/2][size/2] = "0."
  225. lines[0][0] = "1."
  226. center := engine.Vector2{X: size / 2, Y: size / 2}
  227. placeObstacle := func(dir engine.Vector2) {
  228. o := center.Add(dir.ScaleBy(radius))
  229. lines[o.Y][o.X] = obsCell
  230. }
  231. if direction == engine.None {
  232. forAllDirections(placeObstacle)
  233. } else {
  234. placeObstacle(direction)
  235. }
  236. testMap = make([]string, size)
  237. for i := range testMap {
  238. testMap[i] = strings.Join(lines[i], "")
  239. }
  240. return
  241. }
  242. func placeBomb(game *gameHelper, player int, blow bool) {
  243. ticksPerCell := cellSize / game.Settings.PlayerBaseSpeed
  244. startTick := game.tick
  245. game.Action(player, "none", "place_bomb")
  246. game.Tick(1)
  247. game.Action(player, "right", "none")
  248. game.Tick(ticksPerCell)
  249. game.Action(player, "down", "none")
  250. game.Tick(ticksPerCell)
  251. game.Action(player, "none", "none")
  252. if blow {
  253. game.Tick(game.Settings.BombDelay + startTick - 2*ticksPerCell)
  254. }
  255. }
  256. func EngineSpec(c Context, api *actor.Actor) {
  257. api.Ok(Reset{})
  258. c.Test("Movement", func(c Context) {
  259. c.Test("simple movement", func(c Context) {
  260. game := newStartedGame(api, 42, emptyMap, &gameSettings)
  261. pos := game.PlayerPos(0)
  262. testDirection := func(dir engine.Vector2) {
  263. dirName := TranslateDirection(dir)
  264. game.Action(0, dirName, "none")
  265. for i := 0; i < 10; i++ {
  266. pos = pos.Add(dir.ScaleBy(gameSettings.PlayerBaseSpeed))
  267. game.Tick(1)
  268. game.AssertPlayerPos(0, pos, "move %s", dirName)
  269. }
  270. game.Action(0, "none", "none")
  271. for i := 0; i < 3; i++ {
  272. game.Tick(1)
  273. game.AssertPlayerPos(0, pos, "move %s, stop", dirName)
  274. }
  275. }
  276. forAllDirections(testDirection)
  277. })
  278. c.Test("player cannot cross map borders", func(c Context) {
  279. var x, y, newX, newY int
  280. game := newStartedGame(api, 42, emptyMap, &gameSettings)
  281. maxDim := len(emptyMap[0]) / 2
  282. if len(emptyMap) > maxDim {
  283. maxDim = len(emptyMap)
  284. }
  285. ticks := maxDim*cellSize/gameSettings.PlayerBaseSpeed + 1
  286. game.Action(0, "left", "none")
  287. game.Tick(ticks)
  288. x, newX = 0, game.PlayerPos(0).X
  289. assert(newX == x, "move left: player 0: expected X = %d, got %d", x, newX)
  290. game.Action(0, "right", "none")
  291. game.Tick(ticks)
  292. x, newX = (len(emptyMap[0])/2-1)*cellSize, game.PlayerPos(0).X
  293. assert(newX == x, "move right: player 0: expected X = %d, got %d", x, newX)
  294. game.Action(0, "up", "none")
  295. game.Tick(ticks)
  296. y, newY = 0, game.PlayerPos(0).Y
  297. assert(newY == y, "move up: player 0: expected Y = %d, got %d", y, newY)
  298. game.Action(0, "down", "none")
  299. game.Tick(ticks)
  300. y, newY = (len(emptyMap)-1)*cellSize, game.PlayerPos(0).Y
  301. assert(newY == y, "move down: player 0: expected Y = %d, got %d", y, newY)
  302. })
  303. c.Test("players can cross each other", func(c Context) {
  304. game := newStartedGame(api, 42, []string{
  305. "........",
  306. "..0.1...",
  307. "........",
  308. }, &gameSettings)
  309. ticks := cellSize / gameSettings.PlayerBaseSpeed
  310. pos0 := game.PlayerPos(0).Add(engine.Right.ScaleBy(game.Settings.PlayerBaseSpeed * ticks))
  311. pos1 := game.PlayerPos(1).Add(engine.Left.ScaleBy(game.Settings.PlayerBaseSpeed * ticks))
  312. game.Action(0, "right", "none")
  313. game.Action(1, "left", "none")
  314. game.Tick(ticks)
  315. game.AssertPlayerPos(0, pos0, "cross player")
  316. game.AssertPlayerPos(1, pos1, "cross player")
  317. })
  318. c.Test("players cannot cross obstacles", func(c Context) {
  319. game := newStartedGame(api, 42, []string{
  320. "######%.%.%.",
  321. "##0.##%.1.%.",
  322. "######%.%.%.",
  323. }, &gameSettings)
  324. testDirection := func(dir engine.Vector2) {
  325. dirName := TranslateDirection(dir)
  326. for i := range game.Players {
  327. pos := game.PlayerPos(i)
  328. game.Action(i, dirName, "none")
  329. game.Tick(1)
  330. game.AssertPlayerPos(i, pos, "move %s, obstacle", dirName)
  331. }
  332. }
  333. forAllDirections(testDirection)
  334. })
  335. slideSettings := cloneSettings(gameSettings)
  336. slideSettings.MaxSlideLength = 3000
  337. slideSettings.PlayerBaseSpeed = 1000
  338. slideSettings.BombDelay = 10000
  339. moveToObstacle := func(game *gameHelper, dir, slideDir engine.Vector2, offset int) (pos engine.Vector2) {
  340. dirName := TranslateDirection(dir)
  341. slideDirName := TranslateDirection(slideDir)
  342. speed := game.Settings.PlayerBaseSpeed
  343. pos = game.PlayerPos(0)
  344. game.Action(0, slideDirName, "none")
  345. game.Tick(offset)
  346. pos = pos.Add(slideDir.ScaleBy(offset * speed))
  347. game.Action(0, dirName, "none")
  348. game.Tick(10)
  349. pos = pos.Add(dir.ScaleBy(10 * speed))
  350. game.AssertPlayerPos(0, pos, "player hits the wall while moving %s", dirName)
  351. return
  352. }
  353. testNoSlide := func(game *gameHelper, offset int) {
  354. testDirectionForProjection := func(dir, slideDir engine.Vector2) {
  355. dirName := TranslateDirection(dir)
  356. c.Test(fmt.Sprintf("move %s, no slide", dirName), func(c Context) {
  357. pos := moveToObstacle(game, dir, slideDir, offset)
  358. game.Tick(1)
  359. game.AssertPlayerPos(0, pos, "player doesn't slide while moving %s", dirName)
  360. })
  361. }
  362. testAllDirectionsWithPerpendiculars(testDirectionForProjection)
  363. }
  364. testSlide := func(game *gameHelper) {
  365. testDirectionForProjection := func(dir, slideDir engine.Vector2) {
  366. dirName := TranslateDirection(dir)
  367. slideDirName := TranslateDirection(slideDir)
  368. speed := game.Settings.PlayerBaseSpeed
  369. c.Test(fmt.Sprintf("move %s, slide %s, move %s", dirName, slideDirName, dirName), func(c Context) {
  370. pos := moveToObstacle(game, dir, slideDir, 8)
  371. game.Tick(1)
  372. pos = pos.Add(slideDir.ScaleBy(speed))
  373. game.AssertPlayerPos(0, pos, "player slides %s while moving %s", slideDirName, dirName)
  374. game.Tick(2)
  375. pos = pos.Add(slideDir.ScaleBy(speed)).Add(dir.ScaleBy(speed))
  376. game.AssertPlayerPos(0, pos, "player walks %s after sliding %s", dirName, slideDirName)
  377. })
  378. }
  379. testAllDirectionsWithPerpendiculars(testDirectionForProjection)
  380. }
  381. slideMap := []string{
  382. "....##....",
  383. "..........",
  384. "##..0...##",
  385. "..........",
  386. "....##....",
  387. "....1.....",
  388. }
  389. createMapWithBombs := func() (game *gameHelper) {
  390. bombMap := []string{
  391. "..........",
  392. "..........",
  393. "....0.....",
  394. "..........",
  395. "......1...",
  396. "..........",
  397. }
  398. game = newStartedGame(api, 42, bombMap, &slideSettings)
  399. ticksPerCell := cellSize / gameSettings.PlayerBaseSpeed
  400. forAllDirections(func(dir engine.Vector2) {
  401. dirName := TranslateDirection(dir)
  402. game.Action(1, dirName, "none")
  403. game.Tick(ticksPerCell)
  404. game.Action(1, "none", "place_bomb")
  405. game.Tick(1)
  406. game.Action(1, dirName, "none")
  407. game.Tick(2 * ticksPerCell)
  408. game.Action(1, TranslateDirection(dir.Perpendicular()), "none")
  409. game.Tick(ticksPerCell)
  410. })
  411. game.Action(1, "down", "none")
  412. game.Tick(ticksPerCell)
  413. return
  414. }
  415. c.Test("players slide", func(c Context) {
  416. testSlide(newStartedGame(api, 42, slideMap, &slideSettings))
  417. })
  418. c.Test("players slide with bombs", func(c Context) {
  419. testSlide(createMapWithBombs())
  420. })
  421. c.Test("players do not slide if projection > max_slide_length", func(c Context) {
  422. game := newStartedGame(api, 42, slideMap, &slideSettings)
  423. testNoSlide(game, 5)
  424. })
  425. c.Test("players do not slide if they cannot move", func(c Context) {
  426. noSlideMap := []string{
  427. "..######..",
  428. "##......##",
  429. "##..0...##",
  430. "##......##",
  431. "..######..",
  432. "....1.....",
  433. }
  434. game := newStartedGame(api, 42, noSlideMap, &slideSettings)
  435. testNoSlide(game, 8)
  436. })
  437. })
  438. c.Test("Bombs", func(c Context) {
  439. c.Test("player can place bomb", func(c Context) {
  440. game := newStartedGame(api, 42, emptyMap, &gameSettings)
  441. game.Action(0, "none", "place_bomb")
  442. game.Tick(1)
  443. pos := game.PlayerPos(0)
  444. assert(len(game.State.Bombs) == 1, "bomb at pos (%d, %d) expected, no bombs found", pos.X, pos.Y)
  445. bomb := game.State.Bombs[0]
  446. expectedState := BombState{
  447. X: pos.X,
  448. Y: pos.Y,
  449. MovementDirection: "none",
  450. Speed: game.Settings.BombSpeed,
  451. FlameLength: game.PlayerState(0).FlameLength,
  452. CreatedAt: 1,
  453. DestroyAt: 1 + game.Settings.BombDelay,
  454. }
  455. assertBombState(expectedState, bomb)
  456. })
  457. bombySettings := cloneSettings(gameSettings)
  458. bombySettings.PlayerBaseSpeed = 1000
  459. bombySettings.Items["bomb"] = ItemDescription{
  460. BombBonus: 1,
  461. MaxCount: 9,
  462. StartCount: 4,
  463. Weight: 18,
  464. }
  465. game := newStartedGame(api, 42, emptyMap, &bombySettings)
  466. c.Test("player can place one bomb at a cell", func(c Context) {
  467. bombs := game.PlayerState(0).RemainingBombs
  468. assert(bombs > 1, "more than one remaining bombs required for the test")
  469. game.Action(0, "right", "place_bomb")
  470. game.Tick(3)
  471. assert(len(game.State.Bombs) == 1, "one bomb expected, %d bombs found", len(game.State.Bombs))
  472. assertEqual("remaining_bombs", game.PlayerState(0).RemainingBombs, bombs-1)
  473. })
  474. c.Test("player places bomb when cancelling place_bomb at the same tick", func(c Context) {
  475. game.Action(0, "left", "place_bomb")
  476. game.Action(0, "left", "none")
  477. game.Tick(1)
  478. assert(len(game.State.Bombs) == 1, "one bomb expected, %d bombs found", len(game.State.Bombs))
  479. })
  480. ticksPerHalfCell := cellSize / game.Settings.PlayerBaseSpeed / 2
  481. game.Action(0, "right", "place_bomb")
  482. c.Test("player places bombs while walking", func(c Context) {
  483. game.Tick(3*ticksPerHalfCell + 1)
  484. assert(len(game.State.Bombs) == 3, "three bombs expected, %d bombs found", len(game.State.Bombs))
  485. })
  486. c.Test("player stops placing bombs", func(c Context) {
  487. game.Tick(2 * ticksPerHalfCell)
  488. game.Action(0, "right", "none")
  489. game.Tick(2 * ticksPerHalfCell)
  490. assert(len(game.State.Bombs) == 2, "two bombs expected, %d bombs found", len(game.State.Bombs))
  491. })
  492. })
  493. c.Test("Explosions", func(c Context) {
  494. obstacles := make([]obstacle, len(engine.ItemTypes))
  495. for i := range engine.ItemTypes {
  496. obstacles[i] = obstacle{string(engine.ItemTypes[i].Code), true}
  497. }
  498. wall := obstacle{"#", false}
  499. floor := obstacle{".", false}
  500. destructableWall := obstacle{"%", true}
  501. obstacles = append(obstacles, wall, floor, destructableWall)
  502. explosionSettings := cloneSettings(gameSettings)
  503. explosionSettings.PlayerBaseSpeed = 1000
  504. makeExplosionWithSettings := func(
  505. radius, blastAddition int, obs obstacle, dir engine.Vector2,
  506. ) (game *gameHelper, pos engine.Vector2) {
  507. explosionSettings.Items["flame"] = ItemDescription{
  508. FlameBonus: 1,
  509. MaxCount: 12,
  510. StartCount: radius + blastAddition,
  511. Weight: 18,
  512. }
  513. explosionMap := generateExplosionTestMap(2*radius+3, radius, obs, dir)
  514. game = newStartedGame(api, 42, explosionMap, &explosionSettings)
  515. pos = game.PlayerPos(0)
  516. placeBomb(game, 0, true)
  517. return
  518. }
  519. testExplosionState := func(game *gameHelper, radius, blastAddition int, blocksExp bool, dir, pos engine.Vector2) {
  520. assertEqual("explosion count", 1, len(game.State.Explosions))
  521. blastTime := game.Settings.BombDelay + 1
  522. length := radius + blastAddition
  523. if dir == engine.None && blocksExp && length > radius-1 {
  524. length = radius - 1
  525. }
  526. expectedState := ExplosionState{
  527. X: pos.X,
  528. Y: pos.Y,
  529. CreatedAt: blastTime,
  530. DestroyAt: blastTime + game.Settings.BlastWaveDuration,
  531. UpLength: length,
  532. DownLength: length,
  533. LeftLength: length,
  534. RightLength: length,
  535. }
  536. if dir != engine.None && blocksExp && length > radius-1 {
  537. if dir == engine.Up {
  538. expectedState.UpLength = radius - 1
  539. } else if dir == engine.Down {
  540. expectedState.DownLength = radius - 1
  541. } else if dir == engine.Left {
  542. expectedState.LeftLength = radius - 1
  543. } else if dir == engine.Right {
  544. expectedState.RightLength = radius - 1
  545. }
  546. }
  547. assertExplosionState(expectedState, game.State.Explosions[0])
  548. }
  549. c.Test("explosion with different obstacles (circular)", func(c Context) {
  550. for _, obstacle := range obstacles {
  551. c.Test(fmt.Sprintf("obstacle %s", obstacle.Code), func(c Context) {
  552. game, pos := makeExplosionWithSettings(2, 1, obstacle, engine.None)
  553. testExplosionState(game, 2, 1, obstacle.Code != ".", engine.None, pos)
  554. })
  555. }
  556. })
  557. c.Test("explosion with different blast radiuses (circular)", func(c Context) {
  558. for radius := 2; radius < 5; radius++ {
  559. for blastAddition := -1; blastAddition < 2; blastAddition++ {
  560. game, pos := makeExplosionWithSettings(radius, blastAddition, destructableWall, engine.None)
  561. testExplosionState(game, radius, blastAddition, true, engine.None, pos)
  562. }
  563. }
  564. })
  565. c.Test("explosion with asymmetrical obstacles", func(c Context) {
  566. forAllDirections(func(dir engine.Vector2) {
  567. game, pos := makeExplosionWithSettings(2, 1, destructableWall, dir)
  568. testExplosionState(game, 2, 1, true, dir, pos)
  569. })
  570. })
  571. c.Test("explosion ends in time", func(c Context) {
  572. game, _ := makeExplosionWithSettings(2, 1, destructableWall, engine.None)
  573. game.Tick(game.Settings.BlastWaveDuration - 1)
  574. assertEqual("explosion count", 1, len(game.State.Explosions))
  575. game.Tick(1)
  576. assertEqual("explosion count", 0, len(game.State.Explosions))
  577. })
  578. c.Test("explosion is limited by map borders", func(c Context) {
  579. explosionSettings.Items["flame"] = ItemDescription{FlameBonus: 1, MaxCount: 12, StartCount: 3, Weight: 18}
  580. game := newStartedGame(api, 42, []string{
  581. "......",
  582. "..0...",
  583. "....1.",
  584. }, &explosionSettings)
  585. pos := game.PlayerPos(0)
  586. placeBomb(game, 0, true)
  587. testExplosionState(game, 2, 0, true, engine.None, pos)
  588. })
  589. c.Test("explosion interaction with obstacles", func(c Context) {
  590. testSymbol := func(obs obstacle, beforeDecay, afterDecay rune) {
  591. game, pos := makeExplosionWithSettings(2, 1, obs, engine.Left)
  592. assertCellOnObstaclePos := func(msg string, expected rune) {
  593. assert(rune(game.State.Field[pos.Y/cellSize][1]) == expected,
  594. "%s: %c expected, %c found",
  595. msg,
  596. expected,
  597. rune(game.State.Field[pos.Y/cellSize][1]),
  598. )
  599. }
  600. assertCellOnObstaclePos("obstacle on explosion start", beforeDecay)
  601. game.Tick(game.Settings.CellDecayDuration - 1)
  602. assertCellOnObstaclePos("obstacle at the end of decay period", beforeDecay)
  603. game.Tick(1)
  604. assertCellOnObstaclePos("obstacle after decay period", afterDecay)
  605. }
  606. testSymbol(destructableWall, '$', '.')
  607. testSymbol(wall, '#', '#')
  608. testSymbol(floor, '.', '.')
  609. testSymbol(obstacle{"i", true}, '.', '.')
  610. })
  611. c.Test("explosion interaction with players", func(c Context) {
  612. explosionSettings.Items["flame"] = ItemDescription{FlameBonus: 1, MaxCount: 12, StartCount: 2, Weight: 18}
  613. game := newStartedGame(api, 42, []string{
  614. "....1.....",
  615. "..........",
  616. "2...0...3.",
  617. "..........",
  618. "....4.....",
  619. }, &explosionSettings)
  620. pos := game.PlayerPos(0)
  621. placeBomb(game, 0, true)
  622. c.Test("players limit flame length", func(c Context) {
  623. testExplosionState(game, 2, 0, true, engine.None, pos)
  624. })
  625. c.Test("explosion kills players", func(c Context) {
  626. for i, player := range game.PlayerStates() {
  627. if i != 0 {
  628. assert(player.Dead, "player %d wasn't killed by explosion", i)
  629. }
  630. }
  631. })
  632. })
  633. c.Test("explosion interaction with bombs", func(c Context) {
  634. ticksPerCell := cellSize / explosionSettings.PlayerBaseSpeed
  635. blastTime := explosionSettings.BombDelay + 1
  636. createGameWithExplosion := func(chainReactionDelay int) (game *gameHelper) {
  637. explosionSettings.Items["flame"] = ItemDescription{FlameBonus: 1, MaxCount: 12, StartCount: 2, Weight: 18}
  638. explosionSettings.BombChainReactionDelay = chainReactionDelay
  639. game = newStartedGame(api, 42, []string{
  640. "0...1...",
  641. "........",
  642. "........",
  643. }, &explosionSettings)
  644. placeBomb(game, 0, false)
  645. placeBomb(game, 1, false)
  646. game.Tick(game.Settings.BombDelay - 4*ticksPerCell)
  647. return
  648. }
  649. c.Test("bomb limits flame length", func(c Context) {
  650. game := createGameWithExplosion(3)
  651. assertEqual("explosion count", 1, len(game.State.Explosions))
  652. assertExplosionState(ExplosionState{
  653. CreatedAt: blastTime,
  654. DestroyAt: blastTime + game.Settings.BlastWaveDuration,
  655. DownLength: 2,
  656. RightLength: 1,
  657. }, game.State.Explosions[0])
  658. })
  659. c.Test("next bomb explodes sooner", func(c Context) {
  660. game := createGameWithExplosion(3)
  661. assertEqual("bomb count", 1, len(game.State.Bombs))
  662. assertEqual(
  663. "bomb destroy time",
  664. game.tick+game.Settings.BombChainReactionDelay-1,
  665. game.State.Bombs[0].DestroyAt,
  666. )
  667. })
  668. c.Test("next bombs explodes as soon as possible", func(c Context) {
  669. game := createGameWithExplosion(30)
  670. assertEqual("bomb count", 1, len(game.State.Bombs))
  671. assertEqual(
  672. "bomb destroy time",
  673. 2+2*ticksPerCell+game.Settings.BombDelay,
  674. game.State.Bombs[0].DestroyAt,
  675. )
  676. })
  677. })
  678. })
  679. c.Test("Non-random items drop", func(c Context) {
  680. itemSettings := cloneSettings(gameSettings)
  681. itemSettings.PlayerBaseSpeed = 1000
  682. ticksPerCell := cellSize / itemSettings.PlayerBaseSpeed
  683. itemMap := []string{
  684. "B.F.G.S.K.T.I.C.A.N.U.1.",
  685. "0.......................",
  686. "........................",
  687. }
  688. game := newStartedGame(api, 45, itemMap, &itemSettings)
  689. for i := 0; i < 11; i++ {
  690. x := game.PlayerPos(0).X / cellSize
  691. y := game.PlayerPos(0).Y/cellSize - 1
  692. placeBomb(game, 0, true)
  693. game.Tick(game.Settings.CellDecayDuration)
  694. expected := unicode.ToLower(rune(itemMap[y][x*2]))
  695. got := rune(game.State.Field[y][x])
  696. assert(expected == got, "bonus at pos (%d, %d): %c expected, %c found", x, y, expected, got)
  697. game.Action(0, "up", "none")
  698. game.Tick(ticksPerCell)
  699. }
  700. })
  701. c.Test("Random items drop", func(c Context) {
  702. itemSettings := cloneSettings(gameSettings)
  703. itemSettings.PlayerBaseSpeed = 1000
  704. ticksPerCell := cellSize / itemSettings.PlayerBaseSpeed
  705. testItemDrop := func(settings GameSettings, dropProbability, seed int) {
  706. settings.ItemDropProbability = dropProbability
  707. game := newStartedGame(api, seed, []string{
  708. "%%%%%%%%%%%%%%%%%%%%..",
  709. "0...................1.",
  710. "......................",
  711. }, &settings)
  712. mt := engine.NewMersenneTwister(uint32(seed))
  713. totalWeight := 0
  714. for _, item := range game.Settings.Items {
  715. totalWeight += item.Weight
  716. }
  717. for i := 0; i < 10; i++ {
  718. pos := game.PlayerPos(0)
  719. x := pos.X / cellSize
  720. y := pos.Y/cellSize - 1
  721. placeBomb(game, 0, true)
  722. game.Tick(game.Settings.CellDecayDuration)
  723. if mt.Next()%100 < uint32(game.Settings.ItemDropProbability) && totalWeight > 0 {
  724. drop := mt.Next() % uint32(totalWeight)
  725. sum := 0
  726. for _, item := range engine.ItemTypes {
  727. sum += game.Settings.Items[item.Name].Weight
  728. if uint32(sum) > drop {
  729. got := rune(game.State.Field[y][x])
  730. assert(got == item.Code, "bonus at pos (%d, %d): %c expected, %c found", x, y, item.Code, got)
  731. break
  732. }
  733. }
  734. } else {
  735. assert(rune(game.State.Field[y][x]) == '.', "unexpected bonus at pos (%d, %d)", x, y)
  736. }
  737. game.Action(0, "up", "none")
  738. game.Tick(ticksPerCell)
  739. }
  740. }
  741. c.Test("with mostly default settings", func(c Context) {
  742. testItemDrop(itemSettings, 55, 45)
  743. })
  744. c.Test("with different seed", func(c Context) {
  745. for _, seed := range []int{154, 1, 13} {
  746. testItemDrop(itemSettings, 55, seed)
  747. }
  748. })
  749. c.Test("with different drop probability", func(c Context) {
  750. for _, drop := range []int{100, 0, 20, 9} {
  751. testItemDrop(itemSettings, drop, 45)
  752. }
  753. })
  754. c.Test("with zero weights", func(c Context) {
  755. for key, item := range itemSettings.Items {
  756. item.Weight = 0
  757. itemSettings.Items[key] = item
  758. }
  759. testItemDrop(itemSettings, 55, 45)
  760. })
  761. })
  762. c.Test("Game ticks come from one and only game", func(c Context) {
  763. user := createUserAndLogin(api, "first", "first")
  764. user2 := createUserAndLogin(api, "second", "second")
  765. user.WSAuth()
  766. user2.WSAuth()
  767. gameId := createGame(user, "")
  768. user2.Ok(JoinGame{GameId: gameId, Password: ""})
  769. user.Ok(StartGame{})
  770. user2.Ok(StartGame{})
  771. time.Sleep(100 * time.Millisecond)
  772. user.Ok(LeaveGame{})
  773. user2.Ok(LeaveGame{})
  774. user2.ExpectEvent(&WSPlayerLeftGame{})
  775. gameId2 := createGame(user2, "")
  776. user.Ok(JoinGame{GameId: gameId2, Password: ""})
  777. user.Ok(StartGame{})
  778. user2.Ok(StartGame{})
  779. user2.ExpectEvent(&WSGameStarted{})
  780. var gameState WSGameState
  781. var ticks []string
  782. tick := 0
  783. ok := true
  784. for i := 0; i < 30; i++ {
  785. user2.ExpectEvent(&gameState)
  786. ticks = append(ticks, fmt.Sprintf("%d", gameState.Tick))
  787. ok = ok && tick == gameState.Tick
  788. tick++
  789. }
  790. assert(ok, "expected 0,1,2,...n+1, got %s", strings.Join(ticks, ","))
  791. })
  792. }