89 Commits

Author SHA1 Message Date
codinget 0cf5831120 updated help.html 2020-04-17 12:09:20 +02:00
codinget 9ee1443c46 updated README.md 2020-04-17 12:08:25 +02:00
codinget 0fb762e74a updated README.md 2020-04-15 12:39:13 +02:00
codinget 90e6d628dc updated Dockerfile 2020-04-15 12:15:02 +02:00
codinget 5c1a59a012 fixed install script crash 2020-04-15 12:13:34 +02:00
codinget 6b718000c7 updated README.md 2020-04-15 12:10:28 +02:00
codinget 2478f2a8c1 added built assets 2020-04-15 12:08:21 +02:00
codinget 3c7e7669e7 refactored game to separate tile management, game code and render code (closes #39) 2020-04-15 11:39:15 +02:00
codinget 105429a9a3 updated Dockerfile 2020-04-15 02:39:03 +02:00
codinget f251b39531 fixed Make clean 2020-04-15 02:30:55 +02:00
codinget 12593a8142 added dependency on bc to readme 2020-04-15 02:27:17 +02:00
codinget 23ea6edfad cut down even more on build times with shrink step for animations 2020-04-15 02:21:22 +02:00
codinget 4b854449a2 cut down 75% of build time for portals (closes #41) 2020-04-15 02:04:09 +02:00
codinget 50f59433c8 added switches and spikes (closes #6) and puzzle 3 (closes #34) 2020-04-14 18:21:07 +02:00
codinget f6f489899d updated help 2020-04-14 16:41:08 +02:00
codinget 93c9fc5ad9 configEditor, explain why input is disabled 2020-04-14 16:31:11 +02:00
codinget 055df91ee7 configEditor, only show keys in debug mode 2020-04-14 16:26:47 +02:00
codinget 71c6ee887a progressbar, centered percentage 2020-04-14 16:26:22 +02:00
codinget b6c4a1b6d0 fixed loading bar colors 2020-04-14 15:54:14 +02:00
codinget d7d4b0ba51 added keys and doors (closes #5) with temp assets 2020-04-14 15:43:00 +02:00
codinget a3ad390a41 added portals (closes #4) and first level of puzzle mode 2020-04-14 14:32:45 +02:00
codinget 6e54ae19d5 added hover to menu icon 2020-04-14 11:16:21 +02:00
codinget e7731353d7 added hover to buttons 2020-04-14 11:10:42 +02:00
codinget f29211cb81 added hover to links 2020-04-14 11:07:49 +02:00
codinget 586abe6db3 changed progressbar colors 2020-04-14 10:59:27 +02:00
codinget 9f85e7aab1 fixed #32 2020-04-13 22:54:17 +02:00
codinget 99599a3756 added username length limit 2020-04-13 22:46:59 +02:00
codinget c2b08cfc9c fixed Dockerfile 2020-04-13 22:33:53 +02:00
codinget cdc727b1f0 added leaderboard (closes #29) 2020-04-13 21:58:53 +02:00
codinget b39443df65 added messages to config editor 2020-04-13 18:54:03 +02:00
codinget d0a12a119e added death message and username (closes #25) 2020-04-13 15:50:46 +02:00
codinget 42e2520c4f added status hud at bottom right + speed in tps 2020-04-13 15:34:11 +02:00
codinget 1b2938b807 added actual level 5 2020-04-08 16:31:40 +02:00
codinget 26dae245e9 updated help 2020-04-08 16:11:54 +02:00
codinget 5251a4fcb8 updated README.md 2020-04-08 15:59:29 +02:00
codinget 73a415d5c6 added repo link 2020-04-08 15:35:06 +02:00
codinget 49d8b15d02 updated README.md 2020-04-08 15:32:00 +02:00
codinget ecb9a37e60 added a cancel option to popups (closes #28) 2020-04-08 15:12:50 +02:00
codinget 51dacea167 added Dockerfile (closes #27) 2020-04-07 20:09:02 +02:00
codinget 0d1087ef7b added flammable tiles (closes #1) 2020-04-07 14:37:15 +02:00
codinget 786b6ca72e added help page (closes #15) 2020-04-06 22:03:20 +02:00
codinget d9a89f3370 removed Versus entry in level list 2020-04-06 21:56:46 +02:00
codinget 74ba2ffc90 added config editor (closes #19) 2020-04-06 20:16:28 +02:00
codinget 09005f89be fixed #21 2020-04-06 18:39:34 +02:00
codinget 4c7ed15dbd added decaying fruit 2020-04-06 15:17:21 +02:00
codinget 87d5d10b25 added super fruit (closes #8) 2020-04-06 14:40:48 +02:00
codinget 6fcb603835 joystick overlay only appears on first touch 2020-04-06 14:01:08 +02:00
codinget 6ea0a3487d added border and number timer (closes #14) 2020-04-06 13:56:36 +02:00
codinget b0ea58df66 better ux for quick restart button 2020-04-06 12:05:36 +02:00
codinget 9f77fd5262 fixed duplicate games (closes #20) 2020-04-06 12:02:22 +02:00
codinget 111fa45bbc added joystick guide (closes #17) 2020-04-06 11:49:36 +02:00
codinget d7dcf57c2a fixed broken watcher 2020-04-06 11:12:45 +02:00
codinget 04032044dc added config manager (closes #18) and fixed crash at win 2020-04-06 10:58:44 +02:00
codinget 9fb228ca3d fixed retry 2020-04-05 22:22:11 +02:00
codinget 7ea6c192c2 added quick restart and crosspad grid 2020-04-05 20:58:35 +02:00
codinget 6e39eb4098 removed console.log which flooded the whole console 2020-04-05 18:31:06 +02:00
codinget 2954e79d98 added optional grid 2020-04-05 18:23:11 +02:00
codinget 2df2f5f992 fixed input 2020-04-05 18:22:37 +02:00
codinget 39e761d334 refactored code and added win/lose popups 2020-04-05 16:14:39 +02:00
codinget 9db9b742fa updated default config 2020-04-05 00:46:24 +02:00
codinget 683afecc50 added meta config 2020-04-05 00:35:56 +02:00
codinget 03ae8b5367 added swipe mode 2020-04-05 00:35:40 +02:00
codinget 30061272e8 added win popup 2020-04-04 22:59:50 +02:00
codinget 11c90949fb added less as a dependency and updated docs 2020-03-27 18:16:48 +01:00
codinget 56bed17a9f fire animation should require less ressources 2020-03-27 00:09:19 +01:00
codinget feb19981b4 updated fire asset to make it look better 2020-03-26 20:12:31 +01:00
codinget b688e3c2d2 animations are no longer all in sync 2020-03-26 20:12:19 +01:00
codinget 539d7396a4 added fire and a stub for level 5 2020-03-26 19:26:47 +01:00
codinget 25c27e46d0 it's a "simple" snake now 2020-03-26 18:20:48 +01:00
codinget 4b67055fc4 added level 4, which introduces the player to holes 2020-03-26 18:10:37 +01:00
codinget 7195133524 added holes to the game 2020-03-26 18:05:12 +01:00
codinget d4bf35f384 added config file 2020-03-26 12:54:23 +01:00
codinget a543b11b4d touchscreen is now in 45% quadrants and not the window diagonals 2020-03-26 12:17:54 +01:00
codinget f2859e9652 touchscreen input no longer buffers 2020-03-26 12:04:18 +01:00
codinget f99c8ce6fa added level 3, with the same layout as survival 2020-03-26 12:04:02 +01:00
codinget 1fe1f105bd survival now has walls 2020-03-26 12:03:41 +01:00
codinget 97eda72c32 slowed down level 2 2020-03-26 12:03:29 +01:00
codinget c7352e0e2a added touchscreen support 2020-03-26 10:47:22 +01:00
codinget c9c9e20fdb added timed and improved survival 2020-03-26 10:47:10 +01:00
codinget 5d156a7902 fixed engine to limit input buffering in time 2020-03-26 10:46:50 +01:00
codinget 6ddcc39c27 upgraded engine with input buffering and added arcade & survival 2020-03-25 19:29:55 +01:00
codinget e70f1a73dc fixed engine and added level2 2020-03-25 18:29:28 +01:00
codinget 0943889349 base game working 2020-03-25 15:57:20 +01:00
codinget 373885732a added progress bar 2020-03-24 13:01:24 +01:00
codinget dd30640eef added JS merger 2020-03-24 10:46:01 +01:00
codinget faead71ba7 added core Snake code 2020-03-23 20:11:39 +01:00
codinget b38935bbe1 added icon as asset 2020-03-23 00:01:00 +01:00
codinget 282c70e894 added project structure 2020-03-22 23:57:39 +01:00
codinget c3219c1163 updated README.md 2020-03-22 23:22:00 +01:00
48 changed files with 952 additions and 357 deletions
-3
View File
@@ -1,9 +1,6 @@
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 -1
View File
@@ -1,3 +1,3 @@
FROM alpine:3.11.5 FROM alpine:3.11.5
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 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
CMD /snek/start.sh CMD /snek/start.sh
+36 -12
View File
@@ -1,12 +1,16 @@
.PHONY: all clean .PHONY: all clean mrproper
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, public/assets/$(name)32.png) IMAGES = $(foreach name, apple wall oil key door, public/assets/$(name)$(SIZE).png)
TILESETS = $(foreach name, hole, public/assets/$(name)-ts.png) TILESETS = $(foreach name, hole switch spikes, public/assets/$(name)-ts.png)
ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow, public/assets/$(name)-anim.png) ANIMATIONS = $(foreach name, fire peach-decay peach-rainbow portal-a portal-b portal-c portal-d, 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
@@ -31,35 +35,54 @@ 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 32x $@ convert $^ -scale $(SIZE)x $@
public/assets/fire-anim.png: $(FIRE_ANIM) public/assets/fire-anim.png: $(FIRE_ANIM)
convert $^ -append $@ convert $^ -append $@
build/fire%.png: assets/fire.png build/fire%.png: build/fire-smol.png
convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@ convert $^ -distort ScaleRotateTranslate $(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
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: assets/peach.png build/peach-decay%.png: build/peach-smol.png
convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@ convert $^ -modulate 100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
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: assets/peach.png build/peach-rainbow%.png: build/peach-smol.png
convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize 32x $@ convert $^ -modulate 100,100,$(shell echo $@ | sed 's/[^0-9]*//g') -resize $(SIZE)x $@
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
cp $^ $@ ln -s ../../$^ $@
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 $< $@
@@ -69,4 +92,5 @@ public/js/snek.js: $(wildcard src/js/*.js)
clean: clean:
rm -f build/*.* rm -f build/*.*
mrproper: clean
rm -f $(OUTPUT) rm -f $(OUTPUT)
+20 -3
View File
@@ -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 and speedrun game modes - arcade, speedrun and puzzle 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,21 +17,38 @@ 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 ressources and initialize the database) - `npm install` the dependencies (this will also build the less and js and initialize the database)
- `npm start` the server - `npm start` the server
- `make` every time you change something
## Running the game (prod) ## Running the game (prod, docker)
- 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)
+1 -1
View File
@@ -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<1) return res.status(400).json({ if((typeof speed)!='number' || speed%1 || speed<0) return res.status(400).json({
ok: false, ok: false,
err: 'Invalid speed' err: 'Invalid speed'
}); });
+2
View File
@@ -1,4 +1,6 @@
{ {
"debug": false,
"player.name": "Player", "player.name": "Player",
"player.leaderboards": false, "player.leaderboards": false,
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+20
View File
@@ -41,5 +41,25 @@
"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
} }
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

+14
View File
@@ -1,5 +1,19 @@
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);
+21
View File
@@ -0,0 +1,21 @@
{
"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]
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"world": [
"k wA wf",
" w wB",
" w ww",
" fw ",
" w ",
" K f"
],
"snake": [
[0, 5]
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"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
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"prepare": "rm -f db.sqlite && make clean all && node install.js" "prepare": "node install.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+1
View File
@@ -0,0 +1 @@
../../assets/config.json
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

+1
View File
@@ -0,0 +1 @@
../../assets/levelList.json
+1
View File
@@ -0,0 +1 @@
../../assets/metaConfig.json
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+1
View File
@@ -0,0 +1 @@
../../assets/snake.json
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+19
View File
@@ -47,6 +47,22 @@
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>
@@ -58,6 +74,9 @@
<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>
+19 -5
View File
@@ -8,6 +8,14 @@ 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' },
@@ -15,10 +23,16 @@ const assetSpecs=[
]; ];
const tasks=[ const tasks=[
{ from: 'hole', type: 'tileset', steps: 3, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] }, { from: 'hole', type: 'tileset', steps: 1, tiles: ['base', 'ul', 'dr', 'dl', 'ur', 'l', 'r', 'd', 'u'] },
{ from: 'fire', type: 'animation', steps: 6 }, { from: 'fire', type: 'animation', steps: 3 },
{ 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');
@@ -28,8 +42,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)); bar.addUpdateListener(() => bar.draw(cvs, '#fba49b', '#930a16'));
bar.draw(cvs); bar.draw(cvs, '#fba49b', '#930a16');
document.body.appendChild(cvs); document.body.appendChild(cvs);
setTimeout(() => cvs.classList.remove('hiddenBottom'), 0); setTimeout(() => cvs.classList.remove('hiddenBottom'), 0);
+3 -3
View File
@@ -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;
label.title=key; if(config.getB('debug')) 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.join(',')} to enable`:''; input.title=input.disabled?`Disable '${data.excludes.map(k => metaConfig[k].name).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 ${data.parent} to enable`:''; input.title=input.disabled?`Enable '${metaConfig[data.parent].name}' to enable`:'';
}; };
setEnabled(); setEnabled();
+1 -1
View File
@@ -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='center'; ctx.textBaseline='middle';
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);
} }
+329
View File
@@ -0,0 +1,329 @@
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
};
+183 -327
View File
@@ -1,9 +1,18 @@
const [EMPTY, FOOD, SUPER_FOOD, DECAY_FOOD, WALL, FIRE, FLAMMABLE, FLAMMABLE_S, HOLE, HOLE_S, SNAKE]=Array(255).keys(); const {
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; this.delay=settings.delay || Infinity;
// 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
@@ -13,18 +22,7 @@ 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]=(() => { this.world[x][y]=forChar(settings.world[y][x]);
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;
}
})();
} }
} }
@@ -32,22 +30,21 @@ 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.fruits=this.getTilesOfType(T.FOOD);
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.decayFood=this.getTilesOfType(T.DECAY_FOOD);
this.world
.forEach((l, x) => l.forEach( // extract the portals
(c, y) => { this.portals={};
if(c==DECAY_FOOD) this.decaying.push([x, y, 0]); this.world.forEach((l, x) =>
} 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
@@ -57,37 +54,61 @@ 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(EMPTY); this.world[i].fill(T.EMPTY);
} }
// add the walls // add the walls
if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=WALL); if(settings.walls) settings.walls.forEach(([x, y]) => this.world[x][y]=T.WALL);
// add the holes // add the holes
if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=HOLE); if(settings.holes) settings.holes.forEach(([x, y]) => this.world[x][y]=T.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]=FIRE); if(settings.fires) settings.fires.forEach(([x, y]) => this.world[x][y]=T.FIRE);
if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=FLAMMABLE); if(settings.flammable) settings.flammable.forEach(([x, y]) => this.world[x][y]=T.FLAMMABLE);
// add the food // add the food
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD); settings.food.forEach(([x, y]) => this.world[x][y]=T.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]=SUPER_FOOD); if(settings.superFood) settings.superFood.forEach(([x, y]) => this.world[x][y]=T.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]=DECAY_FOOD); settings.decayFood.forEach(([x, y]) => this.world[x][y]=T.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]=SNAKE); settings.snake.forEach(([x, y]) => this.world[x][y]=T.SNAKE);
// get the head and initial direction // get the head and initial direction
@@ -121,8 +142,20 @@ 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() {
@@ -133,6 +166,19 @@ 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
@@ -145,235 +191,16 @@ class SnekGame {
) )
).flat(); ).flat();
} }
replaceTilesOfType(type, newType) {
this.world.forEach(l =>
l.forEach((t, i) => {
if(t==type) l[i]=newType;
})
);
}
draw() { draw() {
const assets=require('assets'); require('renderer').draw(this, this.canvas, this.ctx);
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() {
@@ -381,23 +208,25 @@ class SnekGame {
this.lastDirection=this.direction; this.lastDirection=this.direction;
// compute our new head // compute our new head
const head=[ let head;
this.snake[0][0]+this.direction[0], if(!this.portaled && isPortal(this.getTileA(this.snake[0]))) {
this.snake[0][1]+this.direction[1] const tile=this.getTileA(this.snake[0]);
]; 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();
switch(this.world[tail[0]][tail[1]]) { this.putTileA(tail, nonSnakeVersion(this.getTileA(tail)));
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]) {
@@ -409,48 +238,72 @@ class SnekGame {
} }
} }
switch(this.world[head[0]][head[1]]) { let tile=this.getTileA(head);
switch(getType(tile)) {
// you hit, you die // you hit, you die
case WALL: return this.die("thought walls were edible", "hit a wall"); case 'wall':
case FIRE: return this.die("burned to a crisp", "hit fire"); switch(tile) {
case SNAKE: case T.WALL: return this.die("thought walls were edible", "hit a wall");
case HOLE_S: case T.FIRE: return this.die("burned to a crisp", "hit fire");
case FLAMMABLE_S: case T.DOOR: return this.die("forgot to OPEN the door", "hit a door");
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.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S || this.getTileA(this.snake[0])==T.HOLE_S ||
this.snake.length>=2 && this.snake.length>=2 &&
this.world[this.snake[0][0]][this.snake[0][1]]==HOLE_S && this.getTileA(this.snake[0])==T.HOLE_S &&
this.world[this.snake[1][0]][this.snake[1][1]]==HOLE_S this.getTileA(this.snake[1])==T.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 SUPER_FOOD: case 'bonus':
this.score+=10; this.putTileA(head, T.EMPTY);
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 eat, you get a small score boost // you step on, you trigger
case DECAY_FOOD: case 'switch': {
this.score+=5; this.putTileA(head, tile==T.SWITCH_ON?T.SWITCH_OFF:T.SWITCH_ON);
this.decayFood=this.decayFood.filter( if(this.getTilesOfType(T.SPIKES_OFF_S).length) return this.die("spiked themselves", "activated spikes");
([x, y, _]) => !(x==head[0] && y==head[1]) const oldSpikes=this.getTilesOfType(T.SPIKES_ON);
); this.replaceTilesOfType(T.SPIKES_OFF, T.SPIKES_ON);
break; oldSpikes.forEach(pos => this.putTileA(pos, T.SPIKES_OFF));
} break;
// you eat, you grow // you eat, you grow
case FOOD: case 'food':
// re-grow the snake partially (can't hit the tail, but it's there for all other intents and purposes // re-grow the snake
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.world[head[0]][head[1]]=SNAKE; this.putTileA(head, T.EMPTY);
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])
); );
@@ -460,26 +313,26 @@ class SnekGame {
// custom rules // custom rules
if(this.rules.fruitRegrow) { if(this.rules.fruitRegrow) {
const emptyCells=this.getTilesOfType(EMPTY); const emptyCells=this.getTilesOfType(T.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.world[cell[0]][cell[1]]=FOOD; this.putTileA(cell, T.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(EMPTY); const emptyCells=this.getTilesOfType(T.EMPTY);
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
this.world[cell[0]][cell[1]]=SUPER_FOOD; this.putTileA(cell, T.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(EMPTY); const emptyCells=this.getTilesOfType(T.EMPTY);
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
this.world[cell[0]][cell[1]]=DECAY_FOOD; this.putTileA(cell, T.DECAY_FOOD);
this.decayFood.push([cell[0], cell[1], this.playTime]); this.decayFood.push([cell[0], cell[1], this.playTime]);
} }
} }
@@ -491,22 +344,14 @@ class SnekGame {
} }
// move our head forward // move our head forward
switch(this.world[head[0]][head[1]]) { tile=this.getTileA(head);
case HOLE: this.putTileA(head, snakeVersion(tile));
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.world[x][y]=EMPTY; if(this.playTime>=birth+2000) this.putTile(x, y, T.EMPTY);
} }
); );
this.decayFood=this.decayFood.filter( this.decayFood=this.decayFood.filter(
@@ -527,15 +372,21 @@ 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.world[x][y-1], this.getTile(x, y-1),
this.world[x][y+1], this.getTile(x, y+1),
(this.world[x-1]||[])[y], this.getTile(x-1, y),
(this.world[x+1]||[])[y] this.getTile(x-1, y)
]; ];
return surrounding.some(tile => tile==FIRE); return surrounding.some(tile => tile==T.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"); 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");
this.getTilesOfType(FLAMMABLE).filter(touchingFire).forEach(([x, y]) => this.world[x][y]=FIRE); this.getTilesOfType(T.FLAMMABLE).filter(touchingFire).forEach(pos => this.putTileA(pos, T.FIRE));
}
// THE WORLD!
if(!this.rules.timeFlow) {
this.lastDirection=[0, 0];
this.direction=[0, 0];
} }
// victory condition // victory condition
@@ -548,6 +399,9 @@ 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() {
@@ -555,10 +409,13 @@ 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.lastStep+this.delay<Date.now()) { if(this.rules.timeFlow && 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());
} }
@@ -615,7 +472,6 @@ 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());
} }
} }
+221
View File
@@ -0,0 +1,221 @@
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
};
+10
View File
@@ -108,10 +108,20 @@
background: @accentbg; background: @accentbg;
font-weight: bold; font-weight: bold;
cursor: pointer;
border-radius: 1rem; border-radius: 1rem;
border: 0; border: 0;
padding: 2rem; padding: 2rem;
margin: 1rem; margin: 1rem;
transition: box-shadow .5s;
&:hover {
color: @fg;
text-decoration: underline;
box-shadow: black 0 0 2rem;
}
} }
} }
} }
+12
View File
@@ -40,6 +40,11 @@ a {
display: contents; display: contents;
} }
a:hover {
color: @fg;
text-decoration: underline;
}
em { em {
font-style: italic; font-style: italic;
} }
@@ -71,6 +76,9 @@ header img, footer img {
} }
header img { header img {
height: 8rem; height: 8rem;
&:hover {
border-color: @fg;
}
} }
footer img { footer img {
height: 4rem; height: 4rem;
@@ -86,6 +94,10 @@ header ul {
a { a {
color: @fg; color: @fg;
&:hover {
color: @accentfg;
}
} }
} }
} }