Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a2e41e05 | |||
| 17ab288b63 | |||
| f607034a33 | |||
| a934d5537b | |||
| efa4d9f80f | |||
| 5a569cbaf9 | |||
| b0106f9df0 | |||
| 7b083fda11 | |||
| 1ca8e46461 | |||
| db52ff95ae | |||
| 462668e5cd | |||
| 5775f6c96c | |||
| 363f85c06c | |||
| 5cd4ac2dfe | |||
| a5efd14fc8 | |||
| 03cb1b83b9 | |||
| 5bf5f49f03 | |||
| edd9d5c15a | |||
| ea79ba1dfa | |||
| a87b4679f4 | |||
| e34e7e07bd | |||
| 0923ad56dd | |||
| 65b33afa05 | |||
| 530260ac54 | |||
| bf06218815 | |||
| 2a9418c9c4 | |||
| 338934866b | |||
| 23535d17d8 | |||
| db655feedb | |||
| 6edd23f25f | |||
| 0578bfb6ad | |||
| d339dd0a06 | |||
| a7e2d1c201 | |||
| 3a571a9c30 | |||
| b12ac2fab9 | |||
| 2a67ab40ee | |||
| 7dd95f262c | |||
| 4746f34537 | |||
| 40ec4fa09a | |||
| fe22b85387 | |||
| 41cb894ef9 | |||
| 3997e94a68 | |||
| 76f1765be7 | |||
| 45ab40ebcb | |||
| e34c6c75bb | |||
| c5b94b59c5 | |||
| 02e72795cc | |||
| f050baafbc | |||
| 0e0cd3a7d1 | |||
| fc50821ff0 | |||
| 95751be63e | |||
| 93ba24a53a | |||
| 352c3aa16d | |||
| 86cd935f03 | |||
| 4bf204cae4 | |||
| bb932ff863 | |||
| 6b7ff7e86d | |||
| 1df62ff1fe | |||
| 10585fef98 | |||
| de390dff8a | |||
| 03e0c97280 | |||
| beb9598f69 | |||
| daef55781e | |||
| bd6d9f3399 | |||
| 7362b4dc5c | |||
| fe2902cdea | |||
| becfaf9eaf | |||
| 7de127b93e |
@@ -1,6 +1,9 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
public/assets/*.png
|
||||||
|
public/assets/*.json
|
||||||
public/css/*.css
|
public/css/*.css
|
||||||
public/js/*.js
|
public/js/*.js
|
||||||
|
public/favicon.ico
|
||||||
build/*.png
|
build/*.png
|
||||||
db.sqlite
|
db.sqlite
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM alpine:3.11.5
|
FROM alpine:3.11.5
|
||||||
RUN apk add --no-cache nodejs npm git python3 python2 alpine-sdk && npm i -g node-gyp && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i --unsafe-perm && apk del git python2 python3 alpine-sdk && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh
|
RUN apk add --no-cache nodejs npm git imagemagick make python3 python2 alpine-sdk && npm i -g node-gyp && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i --unsafe-perm && apk del git make imagemagick python2 python3 alpine-sdk && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh
|
||||||
CMD /snek/start.sh
|
CMD /snek/start.sh
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
.PHONY: all clean mrproper
|
.PHONY: all clean
|
||||||
|
|
||||||
SIZE = 32
|
|
||||||
TEMPSIZE = $(shell echo $(SIZE) '*4' | bc)
|
|
||||||
|
|
||||||
FIRE_ANIM = $(foreach angle, $(shell seq 0 6 359), build/fire$(angle).png)
|
FIRE_ANIM = $(foreach angle, $(shell seq 0 6 359), build/fire$(angle).png)
|
||||||
PEACH_DECAY_ANIM = $(foreach percent, $(shell seq 99 -1 0), build/peach-decay$(percent).png)
|
PEACH_DECAY_ANIM = $(foreach percent, $(shell seq 99 -1 0), build/peach-decay$(percent).png)
|
||||||
PEACH_RAINBOW_ANIM = $(foreach percent, $(shell seq 100 2 299), build/peach-rainbow$(percent).png)
|
PEACH_RAINBOW_ANIM = $(foreach percent, $(shell seq 100 2 299), build/peach-rainbow$(percent).png)
|
||||||
PORTAL_ANIM = $(foreach angle, $(shell seq 0 6 359), build/portal-a$(angle).png)
|
|
||||||
|
|
||||||
IMAGES = $(foreach name, apple wall oil key door, public/assets/$(name)$(SIZE).png)
|
IMAGES = $(foreach name, apple wall oil, public/assets/$(name)32.png)
|
||||||
TILESETS = $(foreach name, hole switch spikes, public/assets/$(name)-ts.png)
|
TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png)
|
||||||
ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow portal-a portal-b portal-c portal-d, public/assets/$(name)-anim.png)
|
ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow, public/assets/$(name)-anim.png)
|
||||||
JSON = $(foreach name, snake levelList config metaConfig, public/assets/$(name).json)
|
JSON = $(foreach name, snake levelList config metaConfig, public/assets/$(name).json)
|
||||||
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
|
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
|
||||||
CSS = public/css/snek.css
|
CSS = public/css/snek.css
|
||||||
@@ -35,54 +31,35 @@ public/assets/%32.png: assets/%.png
|
|||||||
convert $^ -resize 32x $@
|
convert $^ -resize 32x $@
|
||||||
public/assets/%256.png: assets/%.png
|
public/assets/%256.png: assets/%.png
|
||||||
convert $^ -resize 256x $@
|
convert $^ -resize 256x $@
|
||||||
public/assets/%$(SIZE).png: assets/%.png
|
|
||||||
convert $^ -resize $(SIZE)x $@
|
|
||||||
|
|
||||||
public/assets/%32.png: assets/%.jpg
|
public/assets/%32.png: assets/%.jpg
|
||||||
convert $^ -resize 32x $@
|
convert $^ -resize 32x $@
|
||||||
public/assets/%256.png: assets/%.jpg
|
public/assets/%256.png: assets/%.jpg
|
||||||
convert $^ -resize 256x $@
|
convert $^ -resize 256x $@
|
||||||
public/assets/%$(SIZE).png: assets/%.jpg
|
|
||||||
convert $^ -resize $(SIZE)x $@
|
|
||||||
|
|
||||||
build/%-smol.png: assets/%.png
|
|
||||||
convert $^ -resize $(TEMPSIZE)x\> $@
|
|
||||||
|
|
||||||
public/assets/%-ts.png: assets/%.png
|
public/assets/%-ts.png: assets/%.png
|
||||||
convert $^ -scale $(SIZE)x $@
|
convert $^ -scale 32x $@
|
||||||
|
|
||||||
public/assets/fire-anim.png: $(FIRE_ANIM)
|
public/assets/fire-anim.png: $(FIRE_ANIM)
|
||||||
convert $^ -append $@
|
convert $^ -append $@
|
||||||
|
|
||||||
build/fire%.png: build/fire-smol.png
|
build/fire%.png: assets/fire.png
|
||||||
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||||
|
|
||||||
public/assets/peach-decay-anim.png: $(PEACH_DECAY_ANIM)
|
public/assets/peach-decay-anim.png: $(PEACH_DECAY_ANIM)
|
||||||
convert $^ -append $@
|
convert $^ -append $@
|
||||||
|
|
||||||
build/peach-decay%.png: build/peach-smol.png
|
build/peach-decay%.png: assets/peach.png
|
||||||
convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||||
|
|
||||||
public/assets/peach-rainbow-anim.png: $(PEACH_RAINBOW_ANIM)
|
public/assets/peach-rainbow-anim.png: $(PEACH_RAINBOW_ANIM)
|
||||||
convert $^ -append $@
|
convert $^ -append $@
|
||||||
|
|
||||||
build/peach-rainbow%.png: build/peach-smol.png
|
build/peach-rainbow%.png: assets/peach.png
|
||||||
convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@
|
||||||
|
|
||||||
build/portal-a%.png: build/portal-smol.png
|
|
||||||
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
|
|
||||||
|
|
||||||
public/assets/portal-a-anim.png: $(PORTAL_ANIM)
|
|
||||||
convert $^ -append $@
|
|
||||||
public/assets/portal-b-anim.png: public/assets/portal-a-anim.png
|
|
||||||
convert $^ -modulate 100,100,200 $@
|
|
||||||
public/assets/portal-c-anim.png: public/assets/portal-a-anim.png
|
|
||||||
convert $^ -modulate 100,100,150 $@
|
|
||||||
public/assets/portal-d-anim.png: public/assets/portal-a-anim.png
|
|
||||||
convert $^ -modulate 100,100,50 $@
|
|
||||||
|
|
||||||
public/assets/%.json: assets/%.json
|
public/assets/%.json: assets/%.json
|
||||||
ln -s ../../$^ $@
|
cp $^ $@
|
||||||
|
|
||||||
public/css/snek.css: src/less/snek.less $(wildcard src/less/*.less)
|
public/css/snek.css: src/less/snek.less $(wildcard src/less/*.less)
|
||||||
node_modules/.bin/lessc $< $@
|
node_modules/.bin/lessc $< $@
|
||||||
@@ -92,5 +69,4 @@ public/js/snek.js: $(wildcard src/js/*.js)
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f build/*.*
|
rm -f build/*.*
|
||||||
mrproper: clean
|
|
||||||
rm -f $(OUTPUT)
|
rm -f $(OUTPUT)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ A "simple" Snake, done as my final JS class project
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- 60 FPS 2D animations
|
- 60 FPS 2D animations
|
||||||
- arcade, speedrun and puzzle game modes
|
- arcade and speedrun game modes
|
||||||
- touchscreen and controller support
|
- touchscreen and controller support
|
||||||
- playable at [snek.codinget.me](https://snek.codinget.me)
|
- playable at [snek.codinget.me](https://snek.codinget.me)
|
||||||
|
|
||||||
@@ -17,38 +17,21 @@ A "simple" Snake, done as my final JS class project
|
|||||||
- GNU Coreutils are known to work
|
- GNU Coreutils are known to work
|
||||||
- On Windows, WSL is known to work
|
- On Windows, WSL is known to work
|
||||||
- Imagemagick, with the `convert` tool in the PATH
|
- Imagemagick, with the `convert` tool in the PATH
|
||||||
- `bc`
|
|
||||||
- Make
|
- Make
|
||||||
- Node.js and npm, both in the PATH
|
- Node.js and npm, both in the PATH
|
||||||
- Node.js 10 and 12 are known to work
|
- Node.js 10 and 12 are known to work
|
||||||
- node-gyp and python are required for the database
|
- node-gyp and python are required for the database
|
||||||
- (if you have already used native modules, you have them)
|
|
||||||
|
|
||||||
## Prod dependencies (direct)
|
|
||||||
- Node.js and npm, both in the PATH
|
|
||||||
- Node.js 10 and 12 are known to work
|
|
||||||
- node-gyp and python are required for the database
|
|
||||||
- (if you have already used native modules, you have them)
|
|
||||||
|
|
||||||
## Prod dependencies (docker)
|
|
||||||
- Docker
|
|
||||||
|
|
||||||
## Running the game (dev)
|
## Running the game (dev)
|
||||||
- `git clone` this repo
|
- `git clone` this repo
|
||||||
- `npm install` the dependencies (this will also build the less and js and initialize the database)
|
- `npm install` the dependencies (this will also build the ressources and initialize the database)
|
||||||
- `npm start` the server
|
- `npm start` the server
|
||||||
- `make` every time you change something
|
|
||||||
|
|
||||||
## Running the game (prod, docker)
|
## Running the game (prod)
|
||||||
- Get the [Dockerfile](https://gitdab.com/Codinget/Snek/raw/branch/master/Dockerfile)
|
- Get the [Dockerfile](https://gitdab.com/Codinget/Snek/raw/branch/master/Dockerfile)
|
||||||
- `docker build` it
|
- `docker build` it
|
||||||
- `docker run -it -p80:3000` the container
|
- `docker run -it -p80:3000` the container
|
||||||
- ideally, put it behind a reverse proxy
|
- ideally, put it behind a reverse proxy
|
||||||
|
|
||||||
## Running the game (prod, direct)
|
|
||||||
- `git clone` this repo
|
|
||||||
- `npm install` the dependencies (this will also build the less and js and initialize the database)
|
|
||||||
- `npm start` the server
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://opensource.org/licenses/MIT)
|
[MIT](https://opensource.org/licenses/MIT)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ api.post('/leaderboards/:category/:id', (req, res) => {
|
|||||||
err: 'Invalid time'
|
err: 'Invalid time'
|
||||||
});
|
});
|
||||||
const speed=req.body.speed;
|
const speed=req.body.speed;
|
||||||
if((typeof speed)!='number' || speed%1 || speed<0) return res.status(400).json({
|
if((typeof speed)!='number' || speed%1 || speed<1) return res.status(400).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
err: 'Invalid speed'
|
err: 'Invalid speed'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"debug": false,
|
|
||||||
|
|
||||||
"player.name": "Player",
|
"player.name": "Player",
|
||||||
"player.leaderboards": false,
|
"player.leaderboards": false,
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@@ -41,25 +41,5 @@
|
|||||||
"Get a score as high as you can in 30 seconds",
|
"Get a score as high as you can in 30 seconds",
|
||||||
"Survive for as long as you can in an increasingly difficult game"
|
"Survive for as long as you can in an increasingly difficult game"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"puzzle": {
|
|
||||||
"desc": "Time doesn't flow in these puzzles. Try getting the fruits in as little moves as possible",
|
|
||||||
"rules": {
|
|
||||||
"fruitRegrow": false,
|
|
||||||
"timeFlow": false,
|
|
||||||
"speedIncrease": false,
|
|
||||||
"worldWrap": false,
|
|
||||||
"winCondition": "fruit",
|
|
||||||
"scoreSystem": "moves",
|
|
||||||
"moveCount": 50,
|
|
||||||
"uploadOnDeath": false,
|
|
||||||
"leaderboardsSort": "score"
|
|
||||||
},
|
|
||||||
"levelFilename": "puzzle<n>.json",
|
|
||||||
"levelDisplay": "Level <n>",
|
|
||||||
"levels": [
|
|
||||||
1, 2, 3
|
|
||||||
],
|
|
||||||
"nextLevel": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -1,19 +1,5 @@
|
|||||||
const DB=require('better-sqlite3');
|
const DB=require('better-sqlite3');
|
||||||
const fs=require('fs');
|
const fs=require('fs');
|
||||||
const child_process=require('child_process');
|
|
||||||
|
|
||||||
// prepare database
|
|
||||||
console.log('Preparing database');
|
|
||||||
if(fs.existsSync('db.sqlite')) fs.unlinkSync('db.sqlite');
|
|
||||||
const db=new DB('db.sqlite');
|
const db=new DB('db.sqlite');
|
||||||
db.exec(fs.readFileSync('init.sql', 'utf8'));
|
db.exec(fs.readFileSync('init.sql', 'utf8'));
|
||||||
|
|
||||||
// compile less
|
|
||||||
console.log('Compiling less');
|
|
||||||
child_process.execFileSync('node_modules/.bin/lessc', ['src/less/snek.less', 'public/css/snek.css']);
|
|
||||||
|
|
||||||
// merge js
|
|
||||||
console.log('Merging js');
|
|
||||||
const jsFiles=fs.readdirSync('src/js').map(f => 'src/js/'+f);
|
|
||||||
const merged=child_process.execFileSync('node', ['mergejs.js', ...jsFiles]);
|
|
||||||
fs.writeFileSync('public/js/snek.js', merged);
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"dimensions": [15, 10],
|
|
||||||
"walls": [
|
|
||||||
[5, 0], [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [5, 6], [5, 7], [5, 8], [5, 9],
|
|
||||||
[10, 0], [10, 1], [10, 2], [10, 3], [10, 4], [10, 5], [10, 6], [10, 7], [10, 8], [10, 9]
|
|
||||||
],
|
|
||||||
"food": [
|
|
||||||
[4, 5],
|
|
||||||
[8, 7],
|
|
||||||
[14, 9]
|
|
||||||
],
|
|
||||||
"snake": [
|
|
||||||
[0, 0]
|
|
||||||
],
|
|
||||||
"portals": {
|
|
||||||
"a": [4, 9],
|
|
||||||
"b": [6, 0],
|
|
||||||
"c": [9, 9],
|
|
||||||
"d": [11, 0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"world": [
|
|
||||||
"k wA wf",
|
|
||||||
" w wB",
|
|
||||||
" w ww",
|
|
||||||
" fw ",
|
|
||||||
" w ",
|
|
||||||
" K f"
|
|
||||||
],
|
|
||||||
"snake": [
|
|
||||||
[0, 5]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"world": [
|
|
||||||
"A i B",
|
|
||||||
" i s",
|
|
||||||
" Cik ",
|
|
||||||
"SSSSSSSSSSSiiiii",
|
|
||||||
" i f",
|
|
||||||
" i ",
|
|
||||||
" s itttt",
|
|
||||||
" t wKKKK",
|
|
||||||
" w ",
|
|
||||||
" wD "
|
|
||||||
],
|
|
||||||
"snake": [
|
|
||||||
[0, 9],
|
|
||||||
[1, 9],
|
|
||||||
[2, 9]
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"moveCount": 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"prepare": "node install.js"
|
"prepare": "rm -f db.sqlite && make clean all && node install.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -1 +0,0 @@
|
|||||||
../../assets/config.json
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
@@ -1 +0,0 @@
|
|||||||
../../assets/levelList.json
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../assets/metaConfig.json
|
|
||||||
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
@@ -1 +0,0 @@
|
|||||||
../../assets/snake.json
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -47,22 +47,6 @@
|
|||||||
A timed game only lasts for 30 seconds, and the goal is to get as high a score as possible.
|
A timed game only lasts for 30 seconds, and the goal is to get as high a score as possible.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
|
||||||
<h3>Survival</h3>
|
|
||||||
<p>
|
|
||||||
In survival mode, the playfield doesn't loop, fruits don't spawn, and you can't win.<br>
|
|
||||||
This modes get progressively harder, with speed increases, and snake growth.<br>
|
|
||||||
At first, this process is slow, but it gets faster with time.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h3>Puzzle mode</h3>
|
|
||||||
<p>
|
|
||||||
In puzzle mode, time doesn't flow until you move.<br>
|
|
||||||
Weird tiles make their debut, like portals, keys, etc.<br>
|
|
||||||
Your goal is to get the fruits in as little moves as you can.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h2>Tiles</h2>
|
<h2>Tiles</h2>
|
||||||
@@ -74,9 +58,6 @@
|
|||||||
<li><em>Oil</em> is flammable and will periodically catch on fire, which will kill you. It is otherwise perfectly safe</li>
|
<li><em>Oil</em> is flammable and will periodically catch on fire, which will kill you. It is otherwise perfectly safe</li>
|
||||||
<li><em>Super fruits</em> give you 10 points, and sometimes spawn in arcade mode</li>
|
<li><em>Super fruits</em> give you 10 points, and sometimes spawn in arcade mode</li>
|
||||||
<li><em>Decaying fruits</em> give you 5 points and sometimes spawn in arcade mode, but they also decay after 2 seconds and disappear</li>
|
<li><em>Decaying fruits</em> give you 5 points and sometimes spawn in arcade mode, but they also decay after 2 seconds and disappear</li>
|
||||||
<li><em>Portals</em> teleport you to the corresponding portal</li>
|
|
||||||
<li><em>Keys</em> make <em>Doors</em> disappear</li>
|
|
||||||
<li><em>Switches</em> enable or disable <em>Spikes</em></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -8,14 +8,6 @@ const assetSpecs=[
|
|||||||
{ name: 'flammable', filename: 'oil32.png', type: 'image' },
|
{ name: 'flammable', filename: 'oil32.png', type: 'image' },
|
||||||
{ name: 'hole', filename: 'hole-ts.png', type: 'image' },
|
{ name: 'hole', filename: 'hole-ts.png', type: 'image' },
|
||||||
{ name: 'fire', filename: 'fire-anim.png', type: 'image' },
|
{ name: 'fire', filename: 'fire-anim.png', type: 'image' },
|
||||||
{ name: 'portalA', filename: 'portal-a-anim.png', type: 'image' },
|
|
||||||
{ name: 'portalB', filename: 'portal-b-anim.png', type: 'image' },
|
|
||||||
{ name: 'portalC', filename: 'portal-c-anim.png', type: 'image' },
|
|
||||||
{ name: 'portalD', filename: 'portal-d-anim.png', type: 'image' },
|
|
||||||
{ name: 'key', filename: 'key32.png', type: 'image' },
|
|
||||||
{ name: 'door', filename: 'door32.png', type: 'image' },
|
|
||||||
{ name: 'switch', filename: 'switch-ts.png', type: 'image' },
|
|
||||||
{ name: 'spikes', filename: 'spikes-ts.png', type: 'image' },
|
|
||||||
{ name: 'snake', filename: 'snake.json', type: 'json' },
|
{ name: 'snake', filename: 'snake.json', type: 'json' },
|
||||||
{ name: 'levelList', filename: 'levelList.json', type: 'json' },
|
{ name: 'levelList', filename: 'levelList.json', type: 'json' },
|
||||||
{ name: 'config', filename: 'config.json', type: 'json' },
|
{ name: 'config', filename: 'config.json', type: 'json' },
|
||||||
@@ -23,16 +15,10 @@ const assetSpecs=[
|
|||||||
];
|
];
|
||||||
|
|
||||||
const tasks=[
|
const tasks=[
|
||||||
{ from: 'hole', type: 'tileset', steps: 1, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] },
|
{ from: 'hole', type: 'tileset', steps: 3, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] },
|
||||||
{ from: 'fire', type: 'animation', steps: 3 },
|
{ from: 'fire', type: 'animation', steps: 6 },
|
||||||
{ from: 'portalA', type: 'animation', steps: 3 },
|
|
||||||
{ from: 'portalB', type: 'animation', steps: 3 },
|
|
||||||
{ from: 'portalC', type: 'animation', steps: 3 },
|
|
||||||
{ from: 'portalD', type: 'animation', steps: 3 },
|
|
||||||
{ from: 'superFruit', type: 'animation', steps: 5 },
|
{ from: 'superFruit', type: 'animation', steps: 5 },
|
||||||
{ from: 'decayFruit', type: 'animation', steps: 5 },
|
{ from: 'decayFruit', type: 'animation', steps: 5 }
|
||||||
{ from: 'switch', type: 'tileset', steps: 1, tiles: ['on', 'off'] },
|
|
||||||
{ from: 'spikes', type: 'tileset', steps: 1, tiles: ['off', 'on'] }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const cvs=document.createElement('canvas');
|
const cvs=document.createElement('canvas');
|
||||||
@@ -42,8 +28,8 @@ cvs.classList.add('progressBar');
|
|||||||
cvs.classList.add('hiddenBottom');
|
cvs.classList.add('hiddenBottom');
|
||||||
|
|
||||||
const bar=new ProgressBar(assetSpecs.length*2+tasks.reduce((a, t) => a+t.steps, 0));
|
const bar=new ProgressBar(assetSpecs.length*2+tasks.reduce((a, t) => a+t.steps, 0));
|
||||||
bar.addUpdateListener(() => bar.draw(cvs, '#fba49b', '#930a16'));
|
bar.addUpdateListener(() => bar.draw(cvs, '#930a16', '#23090d'));
|
||||||
bar.draw(cvs, '#fba49b', '#930a16');
|
bar.draw(cvs, '#930a16', '#23090d');
|
||||||
|
|
||||||
document.body.appendChild(cvs);
|
document.body.appendChild(cvs);
|
||||||
setTimeout(() => cvs.classList.remove('hiddenBottom'), 0);
|
setTimeout(() => cvs.classList.remove('hiddenBottom'), 0);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ConfigEditor extends Popup {
|
|||||||
let id='cfgInput-'+(lastCEId++)+'-'+key.replace(/\./g, '-');
|
let id='cfgInput-'+(lastCEId++)+'-'+key.replace(/\./g, '-');
|
||||||
let label=span.appendChild(document.createElement('label'));
|
let label=span.appendChild(document.createElement('label'));
|
||||||
label.innerText=data.name;
|
label.innerText=data.name;
|
||||||
if(config.getB('debug')) label.title=key;
|
label.title=key;
|
||||||
|
|
||||||
let input;
|
let input;
|
||||||
if(data.type=='boolean') {
|
if(data.type=='boolean') {
|
||||||
@@ -67,7 +67,7 @@ class ConfigEditor extends Popup {
|
|||||||
input.disabled=
|
input.disabled=
|
||||||
data.excludes
|
data.excludes
|
||||||
.some(key => config.getB(key));
|
.some(key => config.getB(key));
|
||||||
input.title=input.disabled?`Disable '${data.excludes.map(k => metaConfig[k].name).join('\', \'')}' to enable`:'';
|
input.title=input.disabled?`Disable ${data.excludes.join(',')} to enable`:'';
|
||||||
};
|
};
|
||||||
|
|
||||||
setEnabled();
|
setEnabled();
|
||||||
@@ -78,7 +78,7 @@ class ConfigEditor extends Popup {
|
|||||||
} else if(data.parent) {
|
} else if(data.parent) {
|
||||||
const setEnabled=() => {
|
const setEnabled=() => {
|
||||||
input.disabled=!config.getB(data.parent);
|
input.disabled=!config.getB(data.parent);
|
||||||
input.title=input.disabled?`Enable '${metaConfig[data.parent].name}' to enable`:'';
|
input.title=input.disabled?`Enable ${data.parent} to enable`:'';
|
||||||
};
|
};
|
||||||
|
|
||||||
setEnabled();
|
setEnabled();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ProgressBar {
|
|||||||
ctx.fillRect(0, 0, canvas.width*this.completeCount/this.taskCount, canvas.height);
|
ctx.fillRect(0, 0, canvas.width*this.completeCount/this.taskCount, canvas.height);
|
||||||
ctx.fillStyle=textColor;
|
ctx.fillStyle=textColor;
|
||||||
ctx.textAlign='center';
|
ctx.textAlign='center';
|
||||||
ctx.textBaseline='middle';
|
ctx.textBaseline='center';
|
||||||
ctx.font=`${canvas.height/2}px 'Fira Code'`;
|
ctx.font=`${canvas.height/2}px 'Fira Code'`;
|
||||||
ctx.fillText(this.percent+'%', canvas.width/2, canvas.height/2);
|
ctx.fillText(this.percent+'%', canvas.width/2, canvas.height/2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
const assets=require('assets');
|
|
||||||
const config=require('config');
|
|
||||||
const {tiles: T}=require('tiles');
|
|
||||||
|
|
||||||
// declare our tiles
|
|
||||||
let snake,
|
|
||||||
wall, hole,
|
|
||||||
fire, flammable,
|
|
||||||
fruit, superFruit, decayFruit,
|
|
||||||
portalA, portalB, portalC, portalD,
|
|
||||||
key, door,
|
|
||||||
switchTile, spikes;
|
|
||||||
|
|
||||||
// load our tiles
|
|
||||||
assets.onReady(() => {
|
|
||||||
wall=assets.get('wall');
|
|
||||||
hole=assets.get('hole');
|
|
||||||
fire=assets.get('fire');
|
|
||||||
flammable=assets.get('flammable');
|
|
||||||
superFruit=assets.get('superFruit');
|
|
||||||
decayFruit=assets.get('decayFruit');
|
|
||||||
portalA=assets.get('portalA');
|
|
||||||
portalB=assets.get('portalB');
|
|
||||||
portalC=assets.get('portalC');
|
|
||||||
portalD=assets.get('portalD');
|
|
||||||
key=assets.get('key');
|
|
||||||
door=assets.get('door');
|
|
||||||
switchTile=assets.get('switch');
|
|
||||||
spikes=assets.get('spikes');
|
|
||||||
snake=assets.get('snake');
|
|
||||||
fruit=assets.get('fruit');
|
|
||||||
});
|
|
||||||
|
|
||||||
const draw=(game, canvas=game.canvas, ctx=canvas.getContext('2d')) => {
|
|
||||||
// clear the canvas, because it's easier than having to deal with everything
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
// get the cell size and offset
|
|
||||||
const cellSize=Math.min(
|
|
||||||
canvas.width/game.dimensions[0],
|
|
||||||
canvas.height/game.dimensions[1]
|
|
||||||
);
|
|
||||||
const offsetX=(canvas.width-cellSize*game.dimensions[0])/2;
|
|
||||||
const offsetY=(canvas.height-cellSize*game.dimensions[1])/2;
|
|
||||||
|
|
||||||
// tile draw functions
|
|
||||||
const putTile=(x, y, tile) => ctx.drawImage(
|
|
||||||
tile,
|
|
||||||
offsetX+cellSize*x,
|
|
||||||
offsetY+cellSize*y,
|
|
||||||
cellSize,
|
|
||||||
cellSize
|
|
||||||
);
|
|
||||||
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
|
||||||
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
|
||||||
]);
|
|
||||||
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
|
||||||
Math.min(Math.round(percent*tile.length), tile.length-1)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// adjascence check
|
|
||||||
const checkAdj=(x, y) => {
|
|
||||||
let adj={};
|
|
||||||
adj.u=game.world[x][y-1];
|
|
||||||
adj.d=game.world[x][y+1];
|
|
||||||
adj.l=(game.world[x-1] || [])[y];
|
|
||||||
adj.r=(game.world[x+1] || [])[y];
|
|
||||||
adj.ul=(game.world[x-1] || [])[y-1];
|
|
||||||
adj.ur=(game.world[x+1] || [])[y-1];
|
|
||||||
adj.dl=(game.world[x-1] || [])[y+1];
|
|
||||||
adj.dr=(game.world[x+1] || [])[y+1];
|
|
||||||
return adj;
|
|
||||||
};
|
|
||||||
|
|
||||||
// draw a grid/checkerboard if requested
|
|
||||||
if(config.getS('appearance.grid')=='grid') {
|
|
||||||
ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
|
||||||
ctx.lineCap='square';
|
|
||||||
ctx.lineWidth=1;
|
|
||||||
ctx.beginPath();
|
|
||||||
for(let x=1; x<game.dimensions[0]; x++) {
|
|
||||||
ctx.moveTo(offsetX+x*cellSize, offsetY);
|
|
||||||
ctx.lineTo(offsetX+x*cellSize, canvas.height-offsetY);
|
|
||||||
}
|
|
||||||
for(let y=1; y<game.dimensions[1]; y++) {
|
|
||||||
ctx.moveTo(offsetX, offsetY+y*cellSize);
|
|
||||||
ctx.lineTo(canvas.width-offsetX, offsetY+y*cellSize);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
} else if(config.getS('appearance.grid')=='checkerboard') {
|
|
||||||
ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
|
||||||
for(let x=0; x<game.dimensions[0]; x++) {
|
|
||||||
for(let y=(x+1)%2; y<game.dimensions[1]; y+=2) {
|
|
||||||
ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw our tiles
|
|
||||||
for(let x=0; x<game.dimensions[0]; x++) {
|
|
||||||
for(let y=0; y<game.dimensions[1]; y++) {
|
|
||||||
switch(game.world[x][y]) {
|
|
||||||
case T.WALL:
|
|
||||||
putTile(x, y, wall);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.FIRE:
|
|
||||||
putTileAnim(x, y, fire);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.HOLE:
|
|
||||||
case T.HOLE_S: {
|
|
||||||
putTile(x, y, hole.base);
|
|
||||||
let adj=checkAdj(x, y);
|
|
||||||
Object
|
|
||||||
.keys(adj)
|
|
||||||
.filter(k => adj[k]==T.HOLE || adj[k]==T.HOLE_S)
|
|
||||||
.forEach(k => putTile(x, y, hole[k]));
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case T.FLAMMABLE:
|
|
||||||
case T.FLAMMABLE_S:
|
|
||||||
putTile(x, y, flammable);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.SUPER_FOOD:
|
|
||||||
putTileAnim(x, y, superFruit);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.PORTAL_A:
|
|
||||||
case T.PORTAL_A_S:
|
|
||||||
putTileAnim(x, y, portalA);
|
|
||||||
break;
|
|
||||||
case T.PORTAL_B:
|
|
||||||
case T.PORTAL_B_S:
|
|
||||||
putTileAnim(x, y, portalB);
|
|
||||||
break;
|
|
||||||
case T.PORTAL_C:
|
|
||||||
case T.PORTAL_C_S:
|
|
||||||
putTileAnim(x, y, portalC);
|
|
||||||
break;
|
|
||||||
case T.PORTAL_D:
|
|
||||||
case T.PORTAL_D_S:
|
|
||||||
putTileAnim(x, y, portalD);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.KEY:
|
|
||||||
putTile(x, y, key);
|
|
||||||
break;
|
|
||||||
case T.DOOR:
|
|
||||||
putTile(x, y, door);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.SWITCH_ON:
|
|
||||||
case T.SWITCH_ON_S:
|
|
||||||
putTile(x, y, switchTile.on);
|
|
||||||
break;
|
|
||||||
case T.SWITCH_OFF:
|
|
||||||
case T.SWITCH_OFF_S:
|
|
||||||
putTile(x, y, switchTile.off);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case T.SPIKES_ON:
|
|
||||||
putTile(x, y, spikes.on);
|
|
||||||
break;
|
|
||||||
case T.SPIKES_OFF:
|
|
||||||
case T.SPIKES_OFF_S:
|
|
||||||
putTile(x, y, spikes.off);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
|
||||||
game.decayFood.forEach(([x, y, birth]) =>
|
|
||||||
putTileAnimPercent(x, y, decayFruit, (game.playTime-birth)/2000)
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw the lines between portals
|
|
||||||
if(Object.keys(game.portals).length) {
|
|
||||||
ctx.strokeStyle='rgba(128, 128, 128, 20%)';
|
|
||||||
ctx.lineCap='round';
|
|
||||||
ctx.lineWidth=cellSize*.15;
|
|
||||||
const drawTunnel=([xa, ya], [xb, yb]) => {
|
|
||||||
const angle=(Math.floor(Date.now()/10)%360)*(Math.PI/180);
|
|
||||||
for(let i=0; i<=1; i++) {
|
|
||||||
const dx=cellSize/3*Math.cos(angle+i*Math.PI);
|
|
||||||
const dy=cellSize/3*Math.sin(angle+i*Math.PI);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(
|
|
||||||
offsetX+cellSize*(xa+1/2)+dx,
|
|
||||||
offsetY+cellSize*(ya+1/2)+dy
|
|
||||||
);
|
|
||||||
ctx.lineTo(
|
|
||||||
offsetX+cellSize*(xb+1/2)+dx,
|
|
||||||
offsetY+cellSize*(yb+1/2)+dy
|
|
||||||
);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if(game.portals.a && game.portals.b) drawTunnel(game.portals.a, game.portals.b);
|
|
||||||
if(game.portals.c && game.portals.d) drawTunnel(game.portals.c, game.portals.d);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw our snake (it gets drawn completely differently, so here it goes)
|
|
||||||
{
|
|
||||||
ctx.fillStyle=snake.color;
|
|
||||||
ctx.strokeStyle=snake.color;
|
|
||||||
ctx.lineCap=snake.cap;
|
|
||||||
ctx.lineJoin=snake.join;
|
|
||||||
ctx.lineWidth=cellSize*snake.tailSize;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.ellipse(
|
|
||||||
offsetX+cellSize*(game.snake[0][0]+1/2),
|
|
||||||
offsetY+cellSize*(game.snake[0][1]+1/2),
|
|
||||||
cellSize/2*snake.headSize,
|
|
||||||
cellSize/2*snake.headSize,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Math.PI*2
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
game.snake.forEach(([x, y], i, a) => {
|
|
||||||
ctx.lineTo(
|
|
||||||
offsetX+cellSize*(x+1/2),
|
|
||||||
offsetY+cellSize*(y+1/2)
|
|
||||||
);
|
|
||||||
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
|
||||||
ctx.lineWidth=cellSize*snake.tailWrapSize;
|
|
||||||
} else {
|
|
||||||
ctx.lineWidth=cellSize*snake.tailSize;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(
|
|
||||||
offsetX+cellSize*(x+1/2),
|
|
||||||
offsetY+cellSize*(y+1/2)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// our fruit has a nice animation to it between .8 and 1.2 scale
|
|
||||||
{
|
|
||||||
const ms=Date.now();
|
|
||||||
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
|
||||||
game.fruits.forEach(([x, y]) => {
|
|
||||||
ctx.drawImage(
|
|
||||||
fruit,
|
|
||||||
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
|
||||||
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
|
||||||
cellSize*fruitScale,
|
|
||||||
cellSize*fruitScale
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// show the timer
|
|
||||||
if(game.rules.winCondition=='time') {
|
|
||||||
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
|
||||||
let remaining=(game.rules.gameDuration-game.playTime)/game.rules.gameDuration;
|
|
||||||
const w=game.dimensions[0]*cellSize;
|
|
||||||
const h=game.dimensions[1]*cellSize;
|
|
||||||
const p=w*2+h*2;
|
|
||||||
|
|
||||||
const wp=w/p;
|
|
||||||
const hp=h/p;
|
|
||||||
|
|
||||||
const pdst=(st, ed, frac) =>
|
|
||||||
(ed-st)*frac+st;
|
|
||||||
|
|
||||||
ctx.strokeStyle='#930a16';
|
|
||||||
ctx.lineJoin='miter';
|
|
||||||
ctx.lineCap='round';
|
|
||||||
ctx.lineWidth=5;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(canvas.width/2, offsetY+2);
|
|
||||||
|
|
||||||
let sp=Math.min(wp/2, remaining);
|
|
||||||
remaining-=sp;
|
|
||||||
ctx.lineTo(pdst(canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
|
||||||
if(remaining) {
|
|
||||||
sp=Math.min(hp, remaining);
|
|
||||||
remaining-=sp;
|
|
||||||
ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
|
||||||
}
|
|
||||||
if(remaining) {
|
|
||||||
sp=Math.min(wp, remaining);
|
|
||||||
remaining-=sp;
|
|
||||||
ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
|
||||||
}
|
|
||||||
if(remaining) {
|
|
||||||
sp=Math.min(hp, remaining);
|
|
||||||
remaining-=sp;
|
|
||||||
ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
|
||||||
}
|
|
||||||
if(remaining) {
|
|
||||||
ctx.lineTo(pdst(offsetX+2, canvas.width/2, remaining/wp*2), offsetY+2);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
|
||||||
let remaining=''+Math.ceil((game.rules.gameDuration-game.playTime)/1000);
|
|
||||||
while(remaining.length<(''+game.rules.gameDuration/1000).length) remaining='0'+remaining;
|
|
||||||
|
|
||||||
ctx.fillStyle='#930a16';
|
|
||||||
ctx.textAlign='center';
|
|
||||||
ctx.textBaseline='middle';
|
|
||||||
ctx.font='4rem "Fira Code"';
|
|
||||||
ctx.fillText(remaining, canvas.width/2, canvas.height/2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw the border around our game area
|
|
||||||
{
|
|
||||||
ctx.fillStyle='black';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, offsetY);
|
|
||||||
ctx.fillRect(0, 0, offsetX, canvas.height);
|
|
||||||
ctx.fillRect(offsetX+cellSize*game.dimensions[0], 0, offsetX, canvas.height);
|
|
||||||
ctx.fillRect(0, offsetY+cellSize*game.dimensions[1], canvas.width, offsetY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return module.exports={
|
|
||||||
draw
|
|
||||||
};
|
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
const {
|
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys();
|
||||||
tiles: T,
|
|
||||||
forChar,
|
|
||||||
getType,
|
|
||||||
isPortal,
|
|
||||||
snakeVersion, nonSnakeVersion
|
|
||||||
}=require('tiles');
|
|
||||||
|
|
||||||
class SnekGame {
|
class SnekGame {
|
||||||
constructor(settings, canvas, rules) {
|
constructor(settings, canvas, rules) {
|
||||||
// setup the delay
|
// setup the delay
|
||||||
this.delay=settings.delay || Infinity;
|
this.delay=settings.delay;
|
||||||
|
|
||||||
// score starts at 0
|
|
||||||
this.score=0;
|
|
||||||
|
|
||||||
// world is given in the level
|
// world is given in the level
|
||||||
if(settings.world) { // explicitly
|
if(settings.world) { // explicitly
|
||||||
@@ -22,7 +13,18 @@ class SnekGame {
|
|||||||
for(let x=0; x<this.world.length; x++) {
|
for(let x=0; x<this.world.length; x++) {
|
||||||
this.world[x]=Array(settings.world.length);
|
this.world[x]=Array(settings.world.length);
|
||||||
for(let y=0; y<this.world[x].length; y++) {
|
for(let y=0; y<this.world[x].length; y++) {
|
||||||
this.world[x][y]=forChar(settings.world[y][x]);
|
this.world[x][y]=(() => {
|
||||||
|
switch(settings.world[y][x]) {
|
||||||
|
case ' ': return EMPTY;
|
||||||
|
case 'f': return FOOD;
|
||||||
|
case 'F': return SUPER_FOOD;
|
||||||
|
case 'd': return DECAY_FOOD;
|
||||||
|
case 'w': return WALL;
|
||||||
|
case 'o': return HOLE;
|
||||||
|
case 'i': return FIRE;
|
||||||
|
case 'I': return FLAMMABLE;
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,21 +32,22 @@ class SnekGame {
|
|||||||
this.dimensions=[this.world.length, this.world[0].length];
|
this.dimensions=[this.world.length, this.world[0].length];
|
||||||
|
|
||||||
// extract the fruits
|
// extract the fruits
|
||||||
this.fruits=this.getTilesOfType(T.FOOD);
|
this.fruits=[];
|
||||||
|
this.world
|
||||||
|
.forEach((l, x) => l.forEach(
|
||||||
|
(c, y) => {
|
||||||
|
if(c==FOOD) this.fruits.push([x, y]);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
// extract the decaying fruits
|
// extract the decaying fruits
|
||||||
this.decayFood=this.getTilesOfType(T.DECAY_FOOD);
|
this.decayFood=[];
|
||||||
|
this.world
|
||||||
// extract the portals
|
.forEach((l, x) => l.forEach(
|
||||||
this.portals={};
|
(c, y) => {
|
||||||
this.world.forEach((l, x) =>
|
if(c==DECAY_FOOD) this.decaying.push([x, y, 0]);
|
||||||
l.forEach((c, y) => {
|
}
|
||||||
if(c==T.PORTAL_A) this.portals.a=[x, y];
|
));
|
||||||
if(c==T.PORTAL_B) this.portals.b=[x, y];
|
|
||||||
if(c==T.PORTAL_C) this.portals.c=[x, y];
|
|
||||||
if(c==T.PORTAL_D) this.portals.d=[x, y];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else { // dimension and objects
|
} else { // dimension and objects
|
||||||
|
|
||||||
// get the dimensions
|
// get the dimensions
|
||||||
@@ -54,61 +57,37 @@ class SnekGame {
|
|||||||
this.world=Array(settings.dimensions[0]);
|
this.world=Array(settings.dimensions[0]);
|
||||||
for(let i=0; i<settings.dimensions[0]; i++) {
|
for(let i=0; i<settings.dimensions[0]; i++) {
|
||||||
this.world[i]=Array(settings.dimensions[1]);
|
this.world[i]=Array(settings.dimensions[1]);
|
||||||
this.world[i].fill(T.EMPTY);
|
this.world[i].fill(EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the walls
|
// add the walls
|
||||||
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=T.WALL);
|
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL);
|
||||||
|
|
||||||
// add the holes
|
// add the holes
|
||||||
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=T.HOLE);
|
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE);
|
||||||
|
|
||||||
// add the fires and flammable tiles
|
// add the fires and flammable tiles
|
||||||
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=T.FIRE);
|
if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||||
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=T.FLAMMABLE);
|
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=FLAMMABLE);
|
||||||
|
|
||||||
// add the food
|
// add the food
|
||||||
settings.food.forEach(([x, y]) => this.world[x][y]=T.FOOD);
|
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
|
||||||
this.fruits=[...settings.food];
|
this.fruits=[...settings.food];
|
||||||
|
|
||||||
// add the super food
|
// add the super food
|
||||||
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=T.SUPER_FOOD);
|
if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=SUPER_FOOD);
|
||||||
|
|
||||||
// add the decaying food
|
// add the decaying food
|
||||||
if(settings.decayFood) {
|
if(settings.decayFood) {
|
||||||
settings.decayFood.forEach(([x, y]) => this.world[x][y]=T.DECAY_FOOD);
|
settings.decayFood.forEach(([x, y]) => this.world[x][y]=DECAY_FOOD);
|
||||||
this.decayFood=settings.decayFood.map(([x, y]) => [x, y, 0]);
|
this.decayFood=settings.decayFood.map(([x, y]) => [x, y, 0]);
|
||||||
} else {
|
} else {
|
||||||
this.decayFood=[];
|
this.decayFood=[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the portals
|
|
||||||
if(settings.portals) {
|
|
||||||
if(settings.portals.a) this.world[settings.portals.a[0]][settings.portals.a[1]]=T.PORTAL_A;
|
|
||||||
if(settings.portals.b) this.world[settings.portals.b[0]][settings.portals.b[1]]=T.PORTAL_B;
|
|
||||||
if(settings.portals.c) this.world[settings.portals.c[0]][settings.portals.c[1]]=T.PORTAL_C;
|
|
||||||
if(settings.portals.d) this.world[settings.portals.d[0]][settings.portals.d[1]]=T.PORTAL_D;
|
|
||||||
this.portals={...settings.portals};
|
|
||||||
} else {
|
|
||||||
this.portals={};
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the keys
|
|
||||||
if(settings.keys) settings.keys.forEach(([x, y]) => this.world[x][y]=T.KEY);
|
|
||||||
|
|
||||||
// add the doors
|
|
||||||
if(settings.doors) settings.doors.forEach(([x, y]) => this.world[x][y]=T.DOOR);
|
|
||||||
|
|
||||||
// add the switches
|
|
||||||
if(settings.switches) settings.switches.forEach(([x, y]) => this.world[x][y]=T.SWITCH_OFF);
|
|
||||||
|
|
||||||
// add the spikes
|
|
||||||
if(settings.spikesOn) settings.spikesOn.forEach(([x, y]) => this.world[x][y]=T.SPIKES_ON);
|
|
||||||
if(settings.spikesOff) settings.spikesOff.forEach(([x, y]) => this.world[x][y]=T.SPIKES_OFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the snake to the world
|
// add the snake to the world
|
||||||
settings.snake.forEach(([x, y]) => this.world[x][y]=T.SNAKE);
|
settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE);
|
||||||
|
|
||||||
|
|
||||||
// get the head and initial direction
|
// get the head and initial direction
|
||||||
@@ -142,20 +121,8 @@ class SnekGame {
|
|||||||
scoreSystem: 'fruit',
|
scoreSystem: 'fruit',
|
||||||
fireTickSpeed: 10,
|
fireTickSpeed: 10,
|
||||||
autoSizeGrow: false,
|
autoSizeGrow: false,
|
||||||
autoSpeedIncrease: false,
|
autoSpeedIncrease: false
|
||||||
timeFlow: true
|
|
||||||
}, rules, settings.rules || {});
|
}, rules, settings.rules || {});
|
||||||
|
|
||||||
// reset direction if time doesn't flow
|
|
||||||
if(!this.rules.timeFlow) {
|
|
||||||
this.lastDirection=[0, 0];
|
|
||||||
this.direction=[0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// set score if move-based
|
|
||||||
if(this.rules.scoreSystem=='moves') {
|
|
||||||
this.score=this.rules.moveCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get playTime() {
|
get playTime() {
|
||||||
@@ -166,19 +133,6 @@ class SnekGame {
|
|||||||
return Math.round(1000/this.delay);
|
return Math.round(1000/this.delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTile(x, y) {
|
|
||||||
return (this.world[x]||[])[y];
|
|
||||||
}
|
|
||||||
getTileA([x, y]) {
|
|
||||||
return (this.world[x]||[])[y];
|
|
||||||
}
|
|
||||||
putTile(x, y, t) {
|
|
||||||
this.world[x][y]=t;
|
|
||||||
}
|
|
||||||
putTileA([x, y], t) {
|
|
||||||
this.world[x][y]=t;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTilesOfType(type) {
|
getTilesOfType(type) {
|
||||||
return this
|
return this
|
||||||
.world
|
.world
|
||||||
@@ -191,16 +145,235 @@ class SnekGame {
|
|||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
}
|
}
|
||||||
replaceTilesOfType(type, newType) {
|
|
||||||
this.world.forEach(l =>
|
|
||||||
l.forEach((t, i) => {
|
|
||||||
if(t==type) l[i]=newType;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
require('renderer').draw(this, this.canvas, this.ctx);
|
const assets=require('assets');
|
||||||
|
const config=require('config');
|
||||||
|
|
||||||
|
// clear the canvas, because it's easier than having to deal with everything
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// get the cell size and offset
|
||||||
|
const cellSize=Math.min(
|
||||||
|
this.canvas.width/this.dimensions[0],
|
||||||
|
this.canvas.height/this.dimensions[1]
|
||||||
|
);
|
||||||
|
const offsetX=(this.canvas.width-cellSize*this.dimensions[0])/2;
|
||||||
|
const offsetY=(this.canvas.height-cellSize*this.dimensions[1])/2;
|
||||||
|
|
||||||
|
// draw a grid/checkerboard if requested
|
||||||
|
if(config.getS('appearance.grid')=='grid') {
|
||||||
|
this.ctx.strokeStyle='rgba(0, 0, 0, 50%)';
|
||||||
|
this.ctx.lineCap='square';
|
||||||
|
this.ctx.lineWidth=1;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
for(let x=1; x<this.dimensions[0]; x++) {
|
||||||
|
this.ctx.moveTo(offsetX+x*cellSize, offsetY);
|
||||||
|
this.ctx.lineTo(offsetX+x*cellSize, this.canvas.height-offsetY);
|
||||||
|
}
|
||||||
|
for(let y=1; y<this.dimensions[1]; y++) {
|
||||||
|
this.ctx.moveTo(offsetX, offsetY+y*cellSize);
|
||||||
|
this.ctx.lineTo(this.canvas.width-offsetX, offsetY+y*cellSize);
|
||||||
|
}
|
||||||
|
this.ctx.stroke();
|
||||||
|
} else if(config.getS('appearance.grid')=='checkerboard') {
|
||||||
|
this.ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
||||||
|
for(let x=0; x<this.dimensions[0]; x++) {
|
||||||
|
for(let y=(x+1)%2; y<this.dimensions[1]; y+=2) {
|
||||||
|
this.ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw our tiles
|
||||||
|
const wall=assets.get('wall');
|
||||||
|
const hole=assets.get('hole');
|
||||||
|
const fire=assets.get('fire');
|
||||||
|
const flammable=assets.get('flammable');
|
||||||
|
const superFruit=assets.get('superFruit');
|
||||||
|
const decayFruit=assets.get('decayFruit');
|
||||||
|
const putTile=(x, y, tile) => this.ctx.drawImage(
|
||||||
|
tile,
|
||||||
|
offsetX+cellSize*x,
|
||||||
|
offsetY+cellSize*y,
|
||||||
|
cellSize,
|
||||||
|
cellSize
|
||||||
|
);
|
||||||
|
const putTileAnim=(x, y, tile) => putTile(x, y, tile[
|
||||||
|
Math.floor(Date.now()/1000*60+x+y)%tile.length
|
||||||
|
]);
|
||||||
|
const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[
|
||||||
|
Math.min(Math.round(percent*tile.length), tile.length-1)
|
||||||
|
]);
|
||||||
|
const checkAdj=(x, y) => {
|
||||||
|
let adj={};
|
||||||
|
adj.u=this.world[x][y-1];
|
||||||
|
adj.d=this.world[x][y+1];
|
||||||
|
adj.l=(this.world[x-1] || [])[y];
|
||||||
|
adj.r=(this.world[x+1] || [])[y];
|
||||||
|
adj.ul=(this.world[x-1] || [])[y-1];
|
||||||
|
adj.ur=(this.world[x+1] || [])[y-1];
|
||||||
|
adj.dl=(this.world[x-1] || [])[y+1];
|
||||||
|
adj.dr=(this.world[x+1] || [])[y+1];
|
||||||
|
return adj;
|
||||||
|
};
|
||||||
|
for(let x=0; x<this.dimensions[0]; x++) {
|
||||||
|
for(let y=0; y<this.dimensions[1]; y++) {
|
||||||
|
switch(this.world[x][y]) {
|
||||||
|
case WALL:
|
||||||
|
putTile(x, y, wall);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FIRE:
|
||||||
|
putTileAnim(x, y, fire);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HOLE:
|
||||||
|
case HOLE_S: {
|
||||||
|
putTile(x, y, hole.base);
|
||||||
|
let adj=checkAdj(x, y);
|
||||||
|
Object
|
||||||
|
.keys(adj)
|
||||||
|
.filter(k => adj[k]==HOLE || adj[k]==HOLE_S)
|
||||||
|
.forEach(k => putTile(x, y, hole[k]));
|
||||||
|
break;
|
||||||
|
// technically, this works for all shapes
|
||||||
|
// however, the tileset only handles convex shapes
|
||||||
|
}
|
||||||
|
|
||||||
|
case FLAMMABLE:
|
||||||
|
case FLAMMABLE_S:
|
||||||
|
putTile(x, y, flammable);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SUPER_FOOD:
|
||||||
|
putTileAnim(x, y, superFruit);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw our decaying fruits (they have more information than just XY, so they need to be drawn here
|
||||||
|
this.decayFood.forEach(([x, y, birth]) =>
|
||||||
|
putTileAnimPercent(x, y, decayFruit, (this.playTime-birth)/2000)
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw our snake (it gets drawn completely differently, so here it goes)
|
||||||
|
const snake=assets.get('snake');
|
||||||
|
this.ctx.fillStyle=snake.color;
|
||||||
|
this.ctx.strokeStyle=snake.color;
|
||||||
|
this.ctx.lineCap=snake.cap;
|
||||||
|
this.ctx.lineJoin=snake.join;
|
||||||
|
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.ellipse(
|
||||||
|
offsetX+cellSize*(this.snake[0][0]+1/2),
|
||||||
|
offsetY+cellSize*(this.snake[0][1]+1/2),
|
||||||
|
cellSize/2*snake.headSize,
|
||||||
|
cellSize/2*snake.headSize,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.PI*2
|
||||||
|
);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.snake.forEach(([x, y], i, a) => {
|
||||||
|
this.ctx.lineTo(
|
||||||
|
offsetX+cellSize*(x+1/2),
|
||||||
|
offsetY+cellSize*(y+1/2)
|
||||||
|
);
|
||||||
|
if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) {
|
||||||
|
this.ctx.lineWidth=cellSize*snake.tailWrapSize;
|
||||||
|
} else {
|
||||||
|
this.ctx.lineWidth=cellSize*snake.tailSize;
|
||||||
|
}
|
||||||
|
this.ctx.stroke();
|
||||||
|
this.ctx.beginPath()
|
||||||
|
this.ctx.moveTo(
|
||||||
|
offsetX+cellSize*(x+1/2),
|
||||||
|
offsetY+cellSize*(y+1/2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// our fruit has a nice animation to it between .8 and 1.2 scale
|
||||||
|
const ms=Date.now();
|
||||||
|
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
|
||||||
|
const fruit=assets.get('fruit');
|
||||||
|
this.fruits.forEach(([x, y]) => {
|
||||||
|
this.ctx.drawImage(
|
||||||
|
fruit,
|
||||||
|
offsetX+cellSize*x+(1-fruitScale)*cellSize/2,
|
||||||
|
offsetY+cellSize*y+(1-fruitScale)*cellSize/2,
|
||||||
|
cellSize*fruitScale,
|
||||||
|
cellSize*fruitScale
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// show the timer
|
||||||
|
if(this.rules.winCondition=='time') {
|
||||||
|
if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') {
|
||||||
|
let remaining=(this.rules.gameDuration-this.playTime)/this.rules.gameDuration;
|
||||||
|
const w=this.dimensions[0]*cellSize;
|
||||||
|
const h=this.dimensions[1]*cellSize;
|
||||||
|
const p=w*2+h*2;
|
||||||
|
|
||||||
|
const wp=w/p;
|
||||||
|
const hp=h/p;
|
||||||
|
|
||||||
|
const pdst=(st, ed, frac) =>
|
||||||
|
(ed-st)*frac+st;
|
||||||
|
|
||||||
|
this.ctx.strokeStyle='#930a16';
|
||||||
|
this.ctx.lineJoin='miter';
|
||||||
|
this.ctx.lineCap='round';
|
||||||
|
this.ctx.lineWidth=5;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(this.canvas.width/2, offsetY+2);
|
||||||
|
|
||||||
|
let sp=Math.min(wp/2, remaining);
|
||||||
|
remaining-=sp;
|
||||||
|
this.ctx.lineTo(pdst(this.canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2);
|
||||||
|
if(remaining) {
|
||||||
|
sp=Math.min(hp, remaining);
|
||||||
|
remaining-=sp;
|
||||||
|
this.ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp));
|
||||||
|
}
|
||||||
|
if(remaining) {
|
||||||
|
sp=Math.min(wp, remaining);
|
||||||
|
remaining-=sp;
|
||||||
|
this.ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2);
|
||||||
|
}
|
||||||
|
if(remaining) {
|
||||||
|
sp=Math.min(hp, remaining);
|
||||||
|
remaining-=sp;
|
||||||
|
this.ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp));
|
||||||
|
}
|
||||||
|
if(remaining) {
|
||||||
|
this.ctx.lineTo(pdst(offsetX+2, this.canvas.width/2, remaining/wp*2), offsetY+2);
|
||||||
|
}
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') {
|
||||||
|
let remaining=''+Math.ceil((this.rules.gameDuration-this.playTime)/1000);
|
||||||
|
while(remaining.length<(''+this.rules.gameDuration/1000).length) remaining='0'+remaining;
|
||||||
|
|
||||||
|
this.ctx.fillStyle='#930a16';
|
||||||
|
this.ctx.textAlign='center';
|
||||||
|
this.ctx.textBaseline='middle';
|
||||||
|
this.ctx.font='4rem "Fira Code"';
|
||||||
|
this.ctx.fillText(remaining, this.canvas.width/2, this.canvas.height/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the border around our game area
|
||||||
|
this.ctx.fillStyle='black';
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, offsetY);
|
||||||
|
this.ctx.fillRect(0, 0, offsetX, this.canvas.height);
|
||||||
|
this.ctx.fillRect(offsetX+cellSize*this.dimensions[0], 0, offsetX, this.canvas.height);
|
||||||
|
this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
step() {
|
||||||
@@ -208,25 +381,23 @@ class SnekGame {
|
|||||||
this.lastDirection=this.direction;
|
this.lastDirection=this.direction;
|
||||||
|
|
||||||
// compute our new head
|
// compute our new head
|
||||||
let head;
|
const head=[
|
||||||
if(!this.portaled && isPortal(this.getTileA(this.snake[0]))) {
|
this.snake[0][0]+this.direction[0],
|
||||||
const tile=this.getTileA(this.snake[0]);
|
this.snake[0][1]+this.direction[1]
|
||||||
if(tile==T.PORTAL_A_S) head=this.portals.b;
|
];
|
||||||
if(tile==T.PORTAL_B_S) head=this.portals.a;
|
|
||||||
if(tile==T.PORTAL_C_S) head=this.portals.d;
|
|
||||||
if(tile==T.PORTAL_D_S) head=this.portals.c;
|
|
||||||
this.portaled=true;
|
|
||||||
} else {
|
|
||||||
head=[
|
|
||||||
this.snake[0][0]+this.direction[0],
|
|
||||||
this.snake[0][1]+this.direction[1]
|
|
||||||
];
|
|
||||||
this.portaled=false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get our tail out of the way
|
// get our tail out of the way
|
||||||
const tail=this.snake.pop();
|
const tail=this.snake.pop();
|
||||||
this.putTileA(tail, nonSnakeVersion(this.getTileA(tail)));
|
switch(this.world[tail[0]][tail[1]]) {
|
||||||
|
case HOLE_S:
|
||||||
|
this.world[tail[0]][tail[1]]=HOLE;
|
||||||
|
break;
|
||||||
|
case FLAMMABLE_S:
|
||||||
|
this.world[tail[0]][tail[1]]=FLAMMABLE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.world[tail[0]][tail[1]]=EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
// check for out of world conditions
|
// check for out of world conditions
|
||||||
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
|
if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) {
|
||||||
@@ -238,72 +409,48 @@ class SnekGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tile=this.getTileA(head);
|
switch(this.world[head[0]][head[1]]) {
|
||||||
switch(getType(tile)) {
|
|
||||||
// you hit, you die
|
// you hit, you die
|
||||||
case 'wall':
|
case WALL: return this.die("thought walls were edible", "hit a wall");
|
||||||
switch(tile) {
|
case FIRE: return this.die("burned to a crisp", "hit fire");
|
||||||
case T.WALL: return this.die("thought walls were edible", "hit a wall");
|
case SNAKE:
|
||||||
case T.FIRE: return this.die("burned to a crisp", "hit fire");
|
case HOLE_S:
|
||||||
case T.DOOR: return this.die("forgot to OPEN the door", "hit a door");
|
case FLAMMABLE_S:
|
||||||
case T.SPIKES_ON: return this.die("thought they were a girl's drink in a nightclub", "hit spikes");
|
|
||||||
}
|
|
||||||
|
|
||||||
// congratilations, you played yourself!
|
|
||||||
case 'snake':
|
|
||||||
return this.die("achieved every dog's dream", "ate their own tail");
|
return this.die("achieved every dog's dream", "ate their own tail");
|
||||||
|
|
||||||
// if either 3 consecutive segments or the whole snake is on a hole, you die
|
// if either 3 consecutive segments or the whole snake is on a hole, you die
|
||||||
case 'hole':
|
case HOLE:
|
||||||
if(
|
if(
|
||||||
this.snake.length==0 ||
|
this.snake.length==0 ||
|
||||||
this.snake.length==1 &&
|
this.snake.length==1 &&
|
||||||
this.getTileA(this.snake[0])==T.HOLE_S ||
|
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S ||
|
||||||
this.snake.length>=2 &&
|
this.snake.length>=2 &&
|
||||||
this.getTileA(this.snake[0])==T.HOLE_S &&
|
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S &&
|
||||||
this.getTileA(this.snake[1])==T.HOLE_S
|
this.world[this.snake[1][0]][this.snake[1][1]]==HOLE_S
|
||||||
) return this.die("fell harder than their grades", "fell in a hole");
|
) return this.die("fell harder than their grades", "fell in a hole");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// you eat, you get a massive score boost
|
// you eat, you get a massive score boost
|
||||||
case 'bonus':
|
case SUPER_FOOD:
|
||||||
this.putTileA(head, T.EMPTY);
|
this.score+=10;
|
||||||
switch(tile) {
|
|
||||||
case T.SUPER_FOOD:
|
|
||||||
this.score+=10;
|
|
||||||
|
|
||||||
// you eat, you get a small score boost
|
|
||||||
case T.DECAY_FOOD:
|
|
||||||
this.score+=5;
|
|
||||||
this.decayFood=this.decayFood.filter(
|
|
||||||
([x, y, _]) => !(x==head[0] && y==head[1])
|
|
||||||
);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// you eat, you destroy all doors
|
|
||||||
case 'key':
|
|
||||||
this.putTileA(head, T.EMPTY);
|
|
||||||
this.replaceTilesOfType(T.DOOR, T.EMPTY);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// you step on, you trigger
|
// you eat, you get a small score boost
|
||||||
case 'switch': {
|
case DECAY_FOOD:
|
||||||
this.putTileA(head, tile==T.SWITCH_ON?T.SWITCH_OFF:T.SWITCH_ON);
|
this.score+=5;
|
||||||
if(this.getTilesOfType(T.SPIKES_OFF_S).length) return this.die("spiked themselves", "activated spikes");
|
this.decayFood=this.decayFood.filter(
|
||||||
const oldSpikes=this.getTilesOfType(T.SPIKES_ON);
|
([x, y, _]) => !(x==head[0] && y==head[1])
|
||||||
this.replaceTilesOfType(T.SPIKES_OFF, T.SPIKES_ON);
|
);
|
||||||
oldSpikes.forEach(pos => this.putTileA(pos, T.SPIKES_OFF));
|
break;
|
||||||
} break;
|
|
||||||
|
|
||||||
// you eat, you grow
|
// you eat, you grow
|
||||||
case 'food':
|
case FOOD:
|
||||||
// re-grow the snake
|
// re-grow the snake partially (can't hit the tail, but it's there for all other intents and purposes
|
||||||
this.snake.push(tail);
|
this.snake.push(tail);
|
||||||
this.putTileA(tail, snakeVersion(this.getTileA(tail)));
|
|
||||||
this.length++;
|
this.length++;
|
||||||
|
|
||||||
// remove the fruit from existence
|
// remove the fruit from existence
|
||||||
this.putTileA(head, T.EMPTY);
|
this.world[head[0]][head[1]]=SNAKE;
|
||||||
this.fruits=this.fruits.filter(
|
this.fruits=this.fruits.filter(
|
||||||
([x, y]) => !(x==head[0] && y==head[1])
|
([x, y]) => !(x==head[0] && y==head[1])
|
||||||
);
|
);
|
||||||
@@ -313,26 +460,26 @@ class SnekGame {
|
|||||||
|
|
||||||
// custom rules
|
// custom rules
|
||||||
if(this.rules.fruitRegrow) {
|
if(this.rules.fruitRegrow) {
|
||||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
const emptyCells=this.getTilesOfType(EMPTY);
|
||||||
|
|
||||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||||
this.fruits.push(cell);
|
this.fruits.push(cell);
|
||||||
this.putTileA(cell, T.FOOD);
|
this.world[cell[0]][cell[1]]=FOOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.rules.superFruitGrow) {
|
if(this.rules.superFruitGrow) {
|
||||||
if(Math.random()<.1) { // 10% chance
|
if(Math.random()<.1) { // 10% chance
|
||||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
const emptyCells=this.getTilesOfType(EMPTY);
|
||||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||||
this.putTileA(cell, T.SUPER_FOOD);
|
this.world[cell[0]][cell[1]]=SUPER_FOOD;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.rules.decayingFruitGrow) {
|
if(this.rules.decayingFruitGrow) {
|
||||||
if(Math.random()<.2) { // 20% chance
|
if(Math.random()<.2) { // 20% chance
|
||||||
const emptyCells=this.getTilesOfType(T.EMPTY);
|
const emptyCells=this.getTilesOfType(EMPTY);
|
||||||
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||||
this.putTileA(cell, T.DECAY_FOOD);
|
this.world[cell[0]][cell[1]]=DECAY_FOOD;
|
||||||
this.decayFood.push([cell[0], cell[1], this.playTime]);
|
this.decayFood.push([cell[0], cell[1], this.playTime]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,14 +491,22 @@ class SnekGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// move our head forward
|
// move our head forward
|
||||||
tile=this.getTileA(head);
|
switch(this.world[head[0]][head[1]]) {
|
||||||
this.putTileA(head, snakeVersion(tile));
|
case HOLE:
|
||||||
|
this.world[head[0]][head[1]]=HOLE_S;
|
||||||
|
break;
|
||||||
|
case FLAMMABLE:
|
||||||
|
this.world[head[0]][head[1]]=FLAMMABLE_S;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.world[head[0]][head[1]]=SNAKE;
|
||||||
|
}
|
||||||
this.snake.unshift(head);
|
this.snake.unshift(head);
|
||||||
|
|
||||||
// decay decaying food
|
// decay decaying food
|
||||||
this.decayFood.forEach(
|
this.decayFood.forEach(
|
||||||
([x, y, birth]) => {
|
([x, y, birth]) => {
|
||||||
if(this.playTime>=birth+2000) this.putTile(x, y, T.EMPTY);
|
if(this.playTime>=birth+2000) this.world[x][y]=EMPTY;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.decayFood=this.decayFood.filter(
|
this.decayFood=this.decayFood.filter(
|
||||||
@@ -372,21 +527,15 @@ class SnekGame {
|
|||||||
if(this.tickId%this.rules.fireTickSpeed==0) {
|
if(this.tickId%this.rules.fireTickSpeed==0) {
|
||||||
const touchingFire=([x, y]) => {
|
const touchingFire=([x, y]) => {
|
||||||
const surrounding=[
|
const surrounding=[
|
||||||
this.getTile(x, y-1),
|
this.world[x][y-1],
|
||||||
this.getTile(x, y+1),
|
this.world[x][y+1],
|
||||||
this.getTile(x-1, y),
|
(this.world[x-1]||[])[y],
|
||||||
this.getTile(x-1, y)
|
(this.world[x+1]||[])[y]
|
||||||
];
|
];
|
||||||
return surrounding.some(tile => tile==T.FIRE);
|
return surrounding.some(tile => tile==FIRE);
|
||||||
};
|
};
|
||||||
if(this.getTilesOfType(T.FLAMMABLE_S).some(touchingFire)) return this.die("didn't know oil was flammable", "stood on oil when it caught on fire");
|
if(this.getTilesOfType(FLAMMABLE_S).some(touchingFire)) return this.die("didn't know oil was flammable", "stood on oil when it caught on fire");
|
||||||
this.getTilesOfType(T.FLAMMABLE).filter(touchingFire).forEach(pos => this.putTileA(pos, T.FIRE));
|
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE);
|
||||||
}
|
|
||||||
|
|
||||||
// THE WORLD!
|
|
||||||
if(!this.rules.timeFlow) {
|
|
||||||
this.lastDirection=[0, 0];
|
|
||||||
this.direction=[0, 0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// victory condition
|
// victory condition
|
||||||
@@ -399,9 +548,6 @@ class SnekGame {
|
|||||||
if(this.rules.winCondition=='score') {
|
if(this.rules.winCondition=='score') {
|
||||||
if(this.score>=this.rules.scoreObjective) return this.win();
|
if(this.score>=this.rules.scoreObjective) return this.win();
|
||||||
}
|
}
|
||||||
if(this.rules.scoreSystem=='moves') {
|
|
||||||
if(this.score) this.score--;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
@@ -409,13 +555,10 @@ class SnekGame {
|
|||||||
if(!this.lastStep) this.lastStep=this.firstStep;
|
if(!this.lastStep) this.lastStep=this.firstStep;
|
||||||
this.draw();
|
this.draw();
|
||||||
if(this.callback) this.callback('tick');
|
if(this.callback) this.callback('tick');
|
||||||
if(this.rules.timeFlow && this.lastStep+this.delay<Date.now()) {
|
if(this.lastStep+this.delay<Date.now()) {
|
||||||
this.lastStep+=this.delay;
|
this.lastStep+=this.delay;
|
||||||
this.step();
|
this.step();
|
||||||
}
|
}
|
||||||
if(!this.rules.timeFlow && (this.direction[0]!=0 || this.direction[1]!=0)) {
|
|
||||||
this.step();
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => this.tick());
|
requestAnimationFrame(() => this.tick());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +615,7 @@ class SnekGame {
|
|||||||
this.firstStep=Date.now();
|
this.firstStep=Date.now();
|
||||||
this.tickId=0;
|
this.tickId=0;
|
||||||
this.playing=true;
|
this.playing=true;
|
||||||
|
this.score=0;
|
||||||
requestAnimationFrame(() => this.tick());
|
requestAnimationFrame(() => this.tick());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
const [
|
|
||||||
EMPTY, SNAKE,
|
|
||||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
|
||||||
WALL,
|
|
||||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
|
||||||
HOLE, HOLE_S,
|
|
||||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
|
||||||
KEY, DOOR,
|
|
||||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
|
||||||
]=Array(255).keys();
|
|
||||||
|
|
||||||
const tiles={
|
|
||||||
EMPTY, SNAKE,
|
|
||||||
FOOD, SUPER_FOOD, DECAY_FOOD,
|
|
||||||
WALL,
|
|
||||||
FIRE, FLAMMABLE, FLAMMABLE_S,
|
|
||||||
HOLE, HOLE_S,
|
|
||||||
PORTAL_A, PORTAL_A_S, PORTAL_B, PORTAL_B_S, PORTAL_C, PORTAL_C_S, PORTAL_D, PORTAL_D_S,
|
|
||||||
KEY, DOOR,
|
|
||||||
SWITCH_ON, SWITCH_ON_S, SWITCH_OFF, SWITCH_OFF_S, SPIKES_OFF, SPIKES_OFF_S, SPIKES_ON
|
|
||||||
};
|
|
||||||
|
|
||||||
const tileNames=(() => {
|
|
||||||
let tileNames=[];
|
|
||||||
Object.keys(tiles).forEach(key => tileNames[tiles[key]]=key);
|
|
||||||
return tileNames;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const getName=t =>
|
|
||||||
tileNames[t] || `Unknown tile ${t}`;
|
|
||||||
|
|
||||||
const forChar=c => {
|
|
||||||
switch(c) {
|
|
||||||
case ' ': return EMPTY;
|
|
||||||
case 'f': return FOOD;
|
|
||||||
case 'F': return SUPER_FOOD;
|
|
||||||
case 'd': return DECAY_FOOD;
|
|
||||||
case 'w': return WALL;
|
|
||||||
case 'o': return HOLE;
|
|
||||||
case 'i': return FIRE;
|
|
||||||
case 'I': return FLAMMABLE;
|
|
||||||
case 'A': return PORTAL_A;
|
|
||||||
case 'B': return PORTAL_B;
|
|
||||||
case 'C': return PORTAL_C;
|
|
||||||
case 'D': return PORTAL_D;
|
|
||||||
case 'k': return KEY;
|
|
||||||
case 'K': return DOOR;
|
|
||||||
case 's': return SWITCH_OFF;
|
|
||||||
case 'S': return SPIKES_ON;
|
|
||||||
case 't': return SPIKES_OFF;
|
|
||||||
}
|
|
||||||
throw TypeError(`'${c}' doesn't correspond to any tile`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const charFor=t => {
|
|
||||||
switch(t) {
|
|
||||||
case EMPTY: return ' ';
|
|
||||||
case FOOD: return 'f';
|
|
||||||
case SUPER_FOOD: return 'F';
|
|
||||||
case DECAY_FOOD: return 'd';
|
|
||||||
case WALL: return 'w';
|
|
||||||
case HOLE: return 'o';
|
|
||||||
case FIRE: return 'i';
|
|
||||||
case FLAMMABLE: return 'I';
|
|
||||||
case PORTAL_A: return 'A';
|
|
||||||
case PORTAL_B: return 'B';
|
|
||||||
case PORTAL_C: return 'C';
|
|
||||||
case PORTAL_D: return 'D';
|
|
||||||
case KEY: return 'k';
|
|
||||||
case DOOR: return 'K';
|
|
||||||
case SWITCH_OFF: return 's';
|
|
||||||
case SPIKES_ON: return 'S';
|
|
||||||
case SPIKES_OFF: return 't';
|
|
||||||
}
|
|
||||||
throw TypeError(`'${getName(t)}' doesn't have a corresponding character'`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const snakeVersion=t => {
|
|
||||||
switch(t) {
|
|
||||||
case EMPTY: return SNAKE;
|
|
||||||
case HOLE: return HOLE_S;
|
|
||||||
case FLAMMABLE: return FLAMMABLE_S;
|
|
||||||
case PORTAL_A: return PORTAL_A_S;
|
|
||||||
case PORTAL_B: return PORTAL_B_S;
|
|
||||||
case PORTAL_C: return PORTAL_C_S;
|
|
||||||
case PORTAL_D: return PORTAL_D_S;
|
|
||||||
case SWITCH_OFF: return SWITCH_OFF_S;
|
|
||||||
case SWITCH_ON: return SWITCH_ON_S;
|
|
||||||
case SPIKES_OFF: return SPIKES_OFF_S;
|
|
||||||
}
|
|
||||||
throw TypeError(`'${getName(t)}' doesn't have a snake version'`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nonSnakeVersion=t => {
|
|
||||||
switch(t) {
|
|
||||||
case SNAKE: return EMPTY;
|
|
||||||
case HOLE_S: return HOLE;
|
|
||||||
case FLAMMABLE_S: return FLAMMABLE;
|
|
||||||
case PORTAL_A_S: return PORTAL_A;
|
|
||||||
case PORTAL_B_S: return PORTAL_B;
|
|
||||||
case PORTAL_C_S: return PORTAL_C;
|
|
||||||
case PORTAL_D_S: return PORTAL_D;
|
|
||||||
case SWITCH_OFF_S: return SWITCH_OFF;
|
|
||||||
case SWITCH_ON_S: return SWITCH_ON;
|
|
||||||
case SPIKES_OFF_S: return SPIKES_OFF;
|
|
||||||
}
|
|
||||||
throw TypeError(`'${getName(t)}' doesn't have a non-snake version'`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSnakeVersion=t => {
|
|
||||||
switch(t) {
|
|
||||||
case SNAKE:
|
|
||||||
case HOLE_S:
|
|
||||||
case FLAMMABLE_S:
|
|
||||||
case PORTAL_A_S: case PORTAL_B_S: case PORTAL_C_S: case PORTAL_D_S:
|
|
||||||
case SWITCH_OFF_S: case SWITCH_ON_S:
|
|
||||||
case SPIKES_OFF_S:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNonSnakeVersion=t => {
|
|
||||||
switch(t) {
|
|
||||||
case EMPTY:
|
|
||||||
case HOLE:
|
|
||||||
case FLAMMABLE:
|
|
||||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
|
||||||
case SWITCH_OFF: case SWITCH_ON:
|
|
||||||
case SPIKES_OFF:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSafe=t => {
|
|
||||||
switch(t) {
|
|
||||||
case EMPTY:
|
|
||||||
case HOLE:
|
|
||||||
case FLAMMABLE:
|
|
||||||
case PORTAL_A: case PORTAL_B: case PORTAL_C: case PORTAL_D:
|
|
||||||
case SWITCH_OFF: case SWITCH_ON:
|
|
||||||
case SPIKES_OFF:
|
|
||||||
case FOOD: case SUPER_FOOD: case DECAY_FOOD:
|
|
||||||
case KEY:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWall=t => {
|
|
||||||
switch(t) {
|
|
||||||
case WALL:
|
|
||||||
case FIRE:
|
|
||||||
case DOOR:
|
|
||||||
case SPIKES_ON:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getType=t => {
|
|
||||||
if(isSnakeVersion(t)) return 'snake';
|
|
||||||
if(isWall(t)) return 'wall';
|
|
||||||
|
|
||||||
switch(t) {
|
|
||||||
case EMPTY:
|
|
||||||
case FLAMMABLE:
|
|
||||||
case SPIKES_OFF:
|
|
||||||
return 'empty';
|
|
||||||
|
|
||||||
case FOOD:
|
|
||||||
return 'food';
|
|
||||||
|
|
||||||
case SUPER_FOOD:
|
|
||||||
case DECAY_FOOD:
|
|
||||||
return 'bonus';
|
|
||||||
|
|
||||||
case HOLE:
|
|
||||||
return 'hole';
|
|
||||||
|
|
||||||
case PORTAL_A:
|
|
||||||
case PORTAL_B:
|
|
||||||
case PORTAL_C:
|
|
||||||
case PORTAL_D:
|
|
||||||
return 'portal';
|
|
||||||
|
|
||||||
case KEY:
|
|
||||||
return 'key';
|
|
||||||
|
|
||||||
case SWITCH_ON:
|
|
||||||
case SWITCH_OFF:
|
|
||||||
return 'switch';
|
|
||||||
}
|
|
||||||
|
|
||||||
throw TypeError(`'${getName(t)}' isn't a valid tile`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPortal=t => {
|
|
||||||
switch(t) {
|
|
||||||
case PORTAL_A:
|
|
||||||
case PORTAL_B:
|
|
||||||
case PORTAL_C:
|
|
||||||
case PORTAL_D:
|
|
||||||
case PORTAL_A_S:
|
|
||||||
case PORTAL_B_S:
|
|
||||||
case PORTAL_C_S:
|
|
||||||
case PORTAL_D_S:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return module.exports={
|
|
||||||
tiles,
|
|
||||||
tileNames, getName,
|
|
||||||
forChar, charFor,
|
|
||||||
snakeVersion, nonSnakeVersion, isSnakeVersion, isNonSnakeVersion,
|
|
||||||
isSafe, isWall, isPortal,
|
|
||||||
getType
|
|
||||||
};
|
|
||||||