puzzlescript
Version:
Play PuzzleScript games in your terminal!
946 lines (709 loc) • 18.2 kB
text/typescript
/* eslint-env jasmine */
import { LevelEngine } from './engine'
import Parser from './parser/parser'
import { clearRandomValuesForTesting, getRandomSeed, nextRandom, setRandomValuesForTesting } from './util'
function parseEngine(code: string) {
const { data } = Parser.parse(code)
const engine = new LevelEngine(data)
engine.setLevel(0)
return { engine, data }
}
describe('Directions', () => {
beforeEach(() => {
clearRandomValuesForTesting()
})
it('"randomly" generates integers', () => {
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(3)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(0)
expect(nextRandom(4)).toBe(2)
expect(nextRandom(4)).toBe(1)
expect(nextRandom(4)).toBe(1)
})
it('Marks a sprite when it wants to move', () => {
const { engine, data } = parseEngine(`
title foo
realtime_interval .01
========
OBJECTS
========
Background
green
Player
blue
=======
LEGEND
=======
. = Background
P = Player
================
COLLISIONLAYERS
================
Background
Player
===
RULES
===
RIGHT [ Player ] -> [ > Player ]
=======
LEVELS
=======
P.
`)
const player = data.getSpriteByName('player')
const { changedCells } = engine.tickUpdateCells()
expect(engine.toSnapshot()).toMatchSnapshot()
// Once these sprites actually move, we neet to separate engine.tick() into multiple steps:
// 1. Update all the cells with new sprites and the wantsToMove directions
// 2. Move all the sprites that want to move
// 3. Late: Update all the cells with new sprites ...
// 4. Late: Move all the sprites that want to move
// next tick for all the AGAIN rules
expect(engine.getCurrentLevel().getCells()[0][0].getWantsToMove(player)).toBe('RIGHT')
// Ensure only 1 cell was marked for update
expect(changedCells.size).toBe(1)
})
it('Moves the sprite', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
=======
LEGEND
=======
. = Background
P = Player
================
COLLISIONLAYERS
================
Background
Player
===
RULES
===
RIGHT [ Player ] -> [ > Player ]
=======
LEVELS
=======
P.
`)
const { changedCells } = engine.tick()
// expect(engine.toSnapshot()).toMatchSnapshot()
const player = data.getSpriteByName('player')
expect(engine.getCurrentLevel().getCells()[0][1].getSpritesAsSet().has(player)).toBe(true)
// Ensure both cells were marked for re-rendering
expect(changedCells.size).toBe(2)
expect(changedCells).toContain(engine.getCurrentLevel().getCells()[0][0])
expect(changedCells).toContain(engine.getCurrentLevel().getCells()[0][1])
})
it('Does not move the sprite if it collides with a sprite in another cell (same collisionlayer)', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
Wall
brown
=======
LEGEND
=======
. = Background
P = Player
W = Wall
================
COLLISIONLAYERS
================
Background
Player, Wall
===
RULES
===
RIGHT [ Player ] -> [ > Player ]
=======
LEVELS
=======
PW
`)
engine.tick()
// expect(engine.toSnapshot()).toMatchSnapshot()
const player = data.getSpriteByName('player')
expect(engine.getCurrentLevel().getCells()[0][1].getSpritesAsSet().has(player)).toBe(false)
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(player)).toBe(true)
// Make sure the wantsToMove flag is cleared
expect(engine.getCurrentLevel().getCells()[0][0].getWantsToMove(player)).toBe('STATIONARY')
// nothing actually changed visually
// expect(changedCells.size).toBe(0)
})
it('Does not move the sprite when ACTION is added to it', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
=======
LEGEND
=======
. = Background
P = Player
================
COLLISIONLAYERS
================
Background
Player
===
RULES
===
[ Player ] -> [ ACTION Player ]
=======
LEVELS
=======
P.
`)
engine.tick()
// expect(engine.toSnapshot()).toMatchSnapshot()
const player = data.getSpriteByName('player')
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(player)).toBe(true)
// Make sure the wantsToMove flag is cleared
expect(engine.getCurrentLevel().getCells()[0][0].getWantsToMove(player)).toBe('STATIONARY')
// nothing actually changed visually
// expect(changedCells.size).toBe(0)
})
it('Randomly decides whether to add the sprite using "RANDOM" in a bracket', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
one
white black
00000
00000
00100
00000
00000
two
white black
00000
01000
00000
00010
00000
three
white black
00000
01000
00100
00010
00000
=======
LEGEND
=======
. = Background
P = Player
DieSide = one OR two OR three
================
COLLISIONLAYERS
================
Background
Player
DieSide
===
RULES
===
(set direction so it is only evaluated once)
LEFT [ Background NO DieSide ] -> [ Background RANDOM DieSide ]
=======
LEVELS
=======
P.
`)
setRandomValuesForTesting([0, 1])
engine.tick()
expect(getRandomSeed()).toBe(2 + 1) // Add one because the background sprite was added to the level? (weird)
// expect(engine.toSnapshot()).toMatchSnapshot()
const two = data.getSpriteByName('two')
const three = data.getSpriteByName('three')
// Check that one popped up (because we set the "random" values above)
// Because the OR is stuck into a set, rather than a list, the 1st item is "three", not "one"
const threeCells = [...three.getCellsThatMatch()]
expect(threeCells.length).toBe(1)
expect(engine.getCurrentLevel().getCells()[0][1].getSpritesAsSet().has(two/*three*/)).toBe(true)
})
it('Moves the sprite in a "random" direction using "RANDOMDIR" in a bracket', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
=======
LEGEND
=======
. = Background
P = Player
================
COLLISIONLAYERS
================
Background
Player
===
RULES
===
RIGHT [ Player ] -> [ RANDOMDIR Player ]
=======
LEVELS
=======
.....
.....
..P..
.....
.....
`)
setRandomValuesForTesting([2, 1])
const { changedCells } = engine.tick()
// expect(engine.toSnapshot()).toMatchSnapshot()
const player = data.getSpriteByName('player')
expect(engine.getCurrentLevel().getCells()[2][2].getSpritesAsSet().has(player)).toBe(false)
// Check that the player is around thir previous location
let playerCells = [...player.getCellsThatMatch()]
let playerCell = playerCells[0]
expect(playerCells.length).toBe(1)
expect(engine.getCurrentLevel().getCells()[playerCell.rowIndex][playerCell.colIndex].getSpritesAsSet().has(player)).toBe(true)
// Ensure 2 cells were marked for re-rendering
expect(changedCells.size).toBe(2)
expect(changedCells).toContain(engine.getCurrentLevel().getCells()[2][2])
expect(changedCells).toContain(engine.getCurrentLevel().getCells()[playerCell.rowIndex][playerCell.colIndex])
engine.tick()
// Check that the player is no longer in the spot they were
expect(engine.getCurrentLevel().getCells()[playerCell.rowIndex][playerCell.colIndex].getSpritesAsSet().has(player)).toBe(false)
// Check that the player is around thir previous location
playerCells = [...player.getCellsThatMatch()]
playerCell = playerCells[0]
expect(playerCells.length).toBe(1)
expect(engine.getCurrentLevel().getCells()[playerCell.rowIndex][playerCell.colIndex].getSpritesAsSet().has(player)).toBe(true)
})
it('supports STATIONARY modifier (simple)', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
background
green
player
blue
incorrect
red
=======
LEGEND
=======
. = Background
P = player
================
COLLISIONLAYERS
================
Background
player
incorrect
===
RULES
===
RIGHT [ player ] -> [ > player ]
[ STATIONARY player ] -> [ incorrect ]
=======
LEVELS
=======
P.
`)
const player = data.getSpriteByName('player')
const incorrect = data.getSpriteByName('incorrect')
engine.tick()
const playerCells = [...player.getCellsThatMatch()]
const playerCell = playerCells[0]
const incorrectCells = [...incorrect.getCellsThatMatch()]
expect(incorrectCells.length).toBe(0)
expect(playerCells.length).toBe(1)
expect(playerCell.rowIndex).toBe(0)
expect(playerCell.colIndex).toBe(1)
})
it('supports STATIONARY modifier with other sprites that are added', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
player
blue
cooldown
transparent
=======
LEGEND
=======
. = Background
P = player
================
COLLISIONLAYERS
================
Background
player
cooldown
===
RULES
===
RIGHT [ STATIONARY player NO cooldown ] -> [ > player > cooldown ]
=======
LEVELS
=======
...
.P.
...
`)
const player = data.getSpriteByName('player')
const cooldown = data.getSpriteByName('cooldown')
engine.tick()
const playerCells = [...player.getCellsThatMatch()]
const playerCell = playerCells[0]
expect(player.getCellsThatMatch().size).toBe(1)
expect(playerCell.rowIndex).toBe(1)
expect(playerCell.colIndex).toBe(2)
// Check that there REALLY is only 1 Player sprite
expect(engine.getCurrentLevel().getCells()[0][1].getSpritesAsSet().has(player)).toBe(false)
expect(engine.getCurrentLevel().getCells()[1][0].getSpritesAsSet().has(player)).toBe(false)
expect(engine.getCurrentLevel().getCells()[2][1].getSpritesAsSet().has(player)).toBe(false)
// Check that the cooldown sprite was also added
expect(engine.getCurrentLevel().getCells()[1][2].getSpritesAsSet().has(cooldown)).toBe(true)
})
it('supports setting STATIONARY so sprites do not move', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
player
blue
=======
LEGEND
=======
. = Background
P = player
================
COLLISIONLAYERS
================
Background
player
===
RULES
===
RIGHT [ STATIONARY player ] -> [ > player ]
[ > player ] -> [ STATIONARY player ]
=======
LEVELS
=======
P.
`)
const player = data.getSpriteByName('player')
engine.tick()
const playerCells = [...player.getCellsThatMatch()]
const playerCell = playerCells[0]
expect(player.getCellsThatMatch().size).toBe(1)
expect(playerCell.rowIndex).toBe(0)
expect(playerCell.colIndex).toBe(0)
// Check that there REALLY is only 1 Player sprite
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(player)).toBe(true)
expect(engine.getCurrentLevel().getCells()[0][1].getSpritesAsSet().has(player)).toBe(false)
})
it('puts a top hat on the player (beam islands)', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
player
yellow
playertop
yellow
=======
LEGEND
=======
. = Background
P = player
================
COLLISIONLAYERS
================
Background
player
playertop
===
RULES
===
UP [ player | ] -> [ player | playertop ]
=======
LEVELS
=======
.
P
`)
const playerTop = data.getSpriteByName('playertop')
engine.tick()
const playerTopCells = [...playerTop.getCellsThatMatch()]
const playerTopCell = playerTopCells[0]
expect(playerTop.getCellsThatMatch().size).toBe(1)
expect(playerTopCell.rowIndex).toBe(0)
expect(playerTopCell.colIndex).toBe(0)
// Check that there REALLY is only 1 Player sprite
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(playerTop)).toBe(true)
})
it('Supports trivial tile negation', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
player
yellow
culprit
transparent
wrong
transparent
correct
transparent
=======
LEGEND
=======
. = Background
Z = culprit
================
COLLISIONLAYERS
================
Background
player
culprit
wrong
correct
===
RULES
===
[ NO culprit ] -> [ wrong ]
[ NO player ] -> [ correct ]
=======
LEVELS
=======
Z
`)
const wrong = data.getSpriteByName('wrong')
const correct = data.getSpriteByName('correct')
engine.tick()
expect(wrong.getCellsThatMatch().size).toBe(0)
expect(correct.getCellsThatMatch().size).toBe(1)
})
it('Supports tile negation (slightly more complicated)', () => {
const { engine, data } = parseEngine(`
title foo
=========
OBJECTS
=========
Background
blue
Player
#f7e26b #000000
01010
.000.
.0.0.
.....
.....
temp E
Transparent yellow
....1
....1
....1
....1
....1
wrong
Transparent red
1...1
.1.1.
..1..
.1.1.
1...1
========
LEGEND
========
@ = Player
. = background
========
SOUNDS
========
=================
COLLISIONLAYERS
=================
Background
temp
wrong
Player
=======
RULES
=======
( Preparation )
[ Player ] -> [ temp ]
(This rule is the culprit)
RIGHT [ NO temp ] -> [ wrong ]
===============
WINCONDITIONS
===============
========
LEVELS
========
@
`)
const wrong = data.getSpriteByName('wrong')
engine.tick()
expect(wrong.getCellsThatMatch().size).toBe(0)
})
it('Uses the absolute direction when moving a sprite, not the relative one in the rule (e.g. "[ < player ]" )', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
green
Player
blue
=======
LEGEND
=======
. = Background
P = Player
================
COLLISIONLAYERS
================
Background
Player
===
RULES
===
LEFT [ Player ] -> [ ^ Player ]
=======
LEVELS
=======
P
.
`)
const { changedCells } = engine.tickUpdateCells()
// expect(engine.toSnapshot()).toMatchSnapshot()
const player = data.getSpriteByName('player')
expect(engine.getCurrentLevel().getCells()[1][0].getSpritesAsSet().has(player)).toBe(false)
// Check that the player is around thir previous location
let playerCells = [...player.getCellsThatMatch()]
expect(playerCells.length).toBe(1)
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(player)).toBe(true)
expect(engine.getCurrentLevel().getCells()[0][0].getWantsToMove(player)).toBe('DOWN')
engine.tickMoveSprites(changedCells)
// Check that the player is no longer in the spot they were
playerCells = [...player.getCellsThatMatch()]
expect(playerCells.length).toBe(1)
expect(engine.getCurrentLevel().getCells()[1][0].getSpritesAsSet().has(player)).toBe(true)
})
it('removes the match when an OR tile loses its direction', () => {
const { engine, data } = parseEngine(`
title foo
========
OBJECTS
========
Background
gray
Player
yellow
Crate
brown
Wrong
red
=======
LEGEND
=======
. = Background
X = Player AND Crate
Movable = Player OR Crate
================
COLLISIONLAYERS
================
Background
Player
Crate
Wrong
===
RULES
===
RIGHT [ Player ] -> [ > Player ]
RIGHT [ > Player ] -> [ Player ] (remove the wantsToMove on the Player. The next rule should no longer match )
RIGHT [ > Movable ] -> [ Wrong ]
=======
LEVELS
=======
X.
`)
engine.tick()
// expect(engine.toSnapshot()).toMatchSnapshot()
const wrong = data.getSpriteByName('Wrong')
expect(engine.getCurrentLevel().getCells()[0][0].getSpritesAsSet().has(wrong)).toBe(false)
expect(wrong.getCellsThatMatch().size).toBe(0)
})
})