puzzlescript
Version:
Play PuzzleScript games in your terminal!
467 lines (379 loc) • 9.42 kB
text/typescript
/* eslint-env jasmine */
import { lookupColorPalette } from '../colors'
import { SimpleRuleGroup } from '../models/rule'
import Parser from './parser'
function checkGrammar(code: string) {
// check that it does not throw an Error
const ast = Parser.parseToAST(code)
expect(ast).toMatchSnapshot()
}
function checkParse(code: string) {
return Parser.parse(code)
}
function checkParseRule(code: string, varNames: string[]) {
// Now check if the semantics parsed
const legendItems = varNames.map((varName) => {
return `${varName} = testObject`
})
return checkParse(`
title checkParseRule
===
OBJECTS
===
testObject
transparent
===
LEGEND
===
${legendItems.join('\n')}
===
COLLISIONLAYERS
===
testObject
====
RULES
====
${code}
`)
}
function parseRule(code: string, varNames: string[]) {
// Add a header
checkGrammar(`
title checkGrammar
===
RULES
===
${code}
`)
return checkParseRule(code, varNames)
}
describe('rules', () => {
it('parses a simple rule', () => {
parseRule('[ z ] -> [ ]', ['z'])
})
it('parses a simple rule 2', () => {
parseRule('[ z ] -> [ z ]', ['z'])
})
it('parses a simple rule without whitespace', () => {
parseRule('[z]->[z]', ['z'])
})
it('parses a rule with multiple cells', () => {
parseRule('[ z | x ] -> [ | ]', ['z', 'x'])
})
it('parses a rule with multiple layers', () => {
parseRule('[ z x ] -> [ ]', ['z', 'x'])
})
it('parses a rule with ellpisis', () => {
parseRule('[ z | ... | x | z ] -> [ RANDOM z | ... | x | ]', ['z', 'x'])
})
it('parses a rule with a period for a variable name', () => {
parseRule('[.] -> []', ['.'])
})
it('parses a rule with a _ for a variable name', () => {
parseRule('[z_x] -> []', ['z_x'])
})
describe('Cell Modifiers', () => {
it('parses a rule with directions', () => {
parseRule('[ACTION z ] -> [ ]', ['z'])
parseRule('[^ z ] -> [ ]', ['z'])
parseRule('[v z ] -> [ ]', ['z']) // This needs to be the down arrow, not a variable
parseRule('[> z ] -> [ ]', ['z'])
parseRule('[< z ] -> [ ]', ['z'])
parseRule('[LEFT z ] -> [ ]', ['z'])
parseRule('[RIGHT z ] -> [ ]', ['z'])
parseRule('[UP z ] -> [ ]', ['z'])
parseRule('[DOWN z ] -> [ ]', ['z'])
parseRule('[STATIONARY z ] -> [ ]', ['z'])
parseRule('[MOVING z ] -> [ ]', ['z'])
parseRule('[VERTICAL z ] -> [ ]', ['z'])
parseRule('[HORIZONTAL z ] -> [ ]', ['z'])
parseRule('[PERPENDICULAR z ] -> [ ]', ['z'])
parseRule('[ORTHOGONAL z ] -> [ ]', ['z'])
parseRule('[RANDOMDIR z ] -> [ ]', ['z'])
})
it('parses a rule with modifiers', () => {
parseRule('[ NO z ] -> [ ]', ['z'])
})
it('parses a rule with a variable that begins with the name of a modifier', () => {
parseRule('[ stationaryz ] -> [ stationaryz ]', ['stationaryz'])
})
it('parses a rule with modifiers 2', () => {
parseRule('[ STATIONARY z ] -> [ z ]', ['z'])
})
})
describe('Commands', () => {
it('parses a rule with a command inside the brackets (these should be moved up to the Action Commands)', () => {
parseRule('[z]->[SFX0]', ['z'])
parseRule('[z]->[z SFX1]', ['z'])
parseRule('[z]->[z winter]', ['z', 'winter'])
parseRule('[z|] -> [|z AGAIN]', ['z'])
})
})
describe('General Formatting', () => {
it('parses an almost-empty file', () => {
Parser.parseToAST(`title foo`)
})
it('parses when there are empty blocks', () => {
Parser.parseToAST(`
title foo
===
objects
===
===
legend
===
===
rules
===
===
levels
===
`)
})
it('parses object shortcut characters', () => {
Parser.parseToAST(`
title foo
===
OBJECTS
===
player Z
transparent
===
RULES
===
[P]->[]
===
LEVELS
===
P`)
})
it('Correctly parses weird variable names', () => {
checkGrammar(`
title foo
===
LEGEND
===
Bush? = Player
. = Player
====
RULES
====
[Bush?] -> [Bush?]
[.] -> [.]
`)
})
it('Looks up color palettes using a string or an index', () => {
const { data: data1 } = checkParse(`
title foo
color_palette gameboycolour
===
OBJECTS
===
player
yellow
`)
expect(data1.objects[0].getPixels(1, 1)[0][0].toHex().toLowerCase()).toBe(lookupColorPalette('gameboycolour', 'yellow').toLowerCase())
const { data: data2 } = checkParse(`
title foo
color_palette 2
===
OBJECTS
===
player
yellow
`)
expect(data2.objects[0].getPixels(1, 1)[0][0].toHex().toLowerCase()).toBe(lookupColorPalette('gameboycolour', 'yellow').toLowerCase())
})
it('Supports characters that would be invalid in one scope but are valid in another scope', () => {
checkParse(`
title foo
===
OBJECTS
===
hello .
transparent
hello2 ]
transparent
hello3 ♡
transparent
=======
LEGEND
=======
[ = hello
, = hello2
д = hello3
sfx11 = hello3 (this is a valid variable name since there are only 10 SFX)
=======
COLLISIONLAYERS
=======
hello,hello2
hello3
=======
RULES
=======
[.]->[.]
[♡]->[♡]
`)
})
it('Supports objects named using only numbers (mirror-isles)', () => {
checkParse(`
title foo
===
OBJECTS
===
Table
Yellow Red White
(12121
21212
12121
21212
0...0)
.....
.121.
.212.
.121.
.0.0.
ChairRightLoose
Yellow Red
`)
checkParse(`
title foo
===
OBJECTS
===
player
yellow
00000
00000
00000
00000
00000
00
yellow
01
yellow
`)
})
})
it('Converts an invalid color to a Transparent one', () => {
const { data } = checkParse(`
title foo
===
OBJECTS
===
player
someInvalidColorName
===
COLLISIONLAYERS
===
player
`)
expect(data.objects[0].getPixels(5, 5)[0][0].isTransparent()).toBe(true)
// expect(message).toBe('Invalid color name. "someinvalidcolorname" is not a valid color. Using "transparent" instead')
// expect(gameNode.__getSourceLineAndColumn()).toBeTruthy()
})
it('Sets the collision layer for nested sprites', () => {
const { data } = checkParse(`
title foo
===
OBJECTS
===
player
TRANSPARENT
===
LEGEND
===
x = player
y = x
===
COLLISIONLAYERS
===
y
`)
// just make sure it doesn't throw an exception
const player = data._getSpriteByName('player')
expect(player && player.getCollisionLayer().id).toBeGreaterThan(0)
})
it('Denotes if a rule is late, rigid, or again', () => {
const { data } = checkParse(`
title foo
===
OBJECTS
===
player
someInvalidColorName
===
COLLISIONLAYERS
===
player
===
RULES
===
LATE [ Player ] -> []
RIGID [ Player ] -> []
[ Player ] -> [] AGAIN
LATE RIGID [ Player ] -> [] AGAIN
[ Player ] -> []
[ Player ] -> [ Player again] (from "Rose")
`)
function expector(rule: SimpleRuleGroup, late: boolean, rigid: boolean) {
expect(rule.getChildRules()[0].isLate()).toBe(late)
expect(rule.hasRigid()).toBe(rigid)
}
expector(data.rules[0], true, false)
expector(data.rules[1], false, true)
expector(data.rules[2], false, false)
expector(data.rules[3], true, true)
expector(data.rules[4], false, false)
expector(data.rules[5], false, false)
})
describe('RANDOM keyword propagation', () => {
it('marks a rule as being RANDOM', () => {
const { data } = parseRule('RANDOM [.] -> []', ['.'])
expect(data.rules[0].isRandom).toBe(true)
})
it('marks a rule Group as being RANDOM', () => {
const { data } = parseRule(`
RANDOM [.] -> []
+ [Player] -> []`, ['.', 'Player'])
expect(data.rules[0].isRandom).toBe(true)
})
it('does not mark a rule Loop as being RANDOM', () => {
const { data } = parseRule(`
STARTLOOP
RANDOM [.] -> []
+ [Player] -> []
ENDLOOP
`, ['.', 'Player'])
expect(data.rules[0].isRandom).toBe(false)
})
it('properly groups loops and groups', () => {
const { data } = parseRule(`
RIGHT [ ] -> [ ]
STARTLOOP
(1st rulegroup)
RIGHT [ ] -> [ ]
+ RIGHT [ ] -> [ ]
+ RIGHT [ ] -> [ ]
+ RIGHT [ ] -> [ ]
( 2nd rulegroup)
RIGHT [ ] -> [ ]
+ RIGHT [ ] -> [ ]
RIGHT [ ] -> [ ]
ENDLOOP
RIGHT [ ] -> [ ]
`, ['.', 'Player'])
expect(data.rules.length).toBe(3)
// startloop
expect(data.rules[1].getChildRules().length).toBe(3)
// 1st rulegroup
expect(data.rules[1].getChildRules()[0].getChildRules().length).toBe(4)
// 2nd rulegroup
expect(data.rules[1].getChildRules()[1].getChildRules().length).toBe(2)
})
})
describe('Expected Failures', () => {
// Something using collisionLayers. Like A and B are in the same layer and we try to run either: `[ A B ] -> []` or `[A] -> [A B]`
// Check that the magic objects `Background` and `Player` are set to something
})
})