added core Snake code
This commit is contained in:
parent
fe2902cdea
commit
7362b4dc5c
17 changed files with 477 additions and 6 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1 +1,6 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
public/assets/*.png
|
||||||
|
public/assets/*.json
|
||||||
|
public/css/*.css
|
||||||
|
public/favicon.ico
|
||||||
|
|
||||||
|
|
42
Makefile
42
Makefile
|
@ -0,0 +1,42 @@
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
ICON = public/assets/icon32.png public/assets/icon256.png public/favicon.ico
|
||||||
|
APPLE = public/assets/apple32.png
|
||||||
|
WALL = public/assets/wall32.png
|
||||||
|
|
||||||
|
SNAKE = public/assets/snake.json
|
||||||
|
|
||||||
|
CSS = public/css/snek.css
|
||||||
|
|
||||||
|
OUTPUT = $(ICON) $(APPLE) $(WALL) $(SNAKE) $(CSS)
|
||||||
|
|
||||||
|
all: icon css apple wall snake
|
||||||
|
icon: $(ICON)
|
||||||
|
apple: $(APPLE)
|
||||||
|
wall: $(WALL)
|
||||||
|
|
||||||
|
snake: $(SNAKE)
|
||||||
|
|
||||||
|
css: $(CSS)
|
||||||
|
|
||||||
|
public/assets/icon32.png: assets/icon.jpg
|
||||||
|
convert $^ -resize 32x $@
|
||||||
|
public/assets/icon256.png: assets/icon.jpg
|
||||||
|
convert $^ -resize 256x $@
|
||||||
|
public/favicon.ico: assets/icon.jpg
|
||||||
|
convert $^ -resize 32x $@
|
||||||
|
|
||||||
|
public/assets/apple32.png: assets/apple.png
|
||||||
|
convert $^ -resize 32x $@
|
||||||
|
|
||||||
|
public/assets/wall32.png: assets/wall.png
|
||||||
|
convert $^ -resize 32x $@
|
||||||
|
|
||||||
|
public/assets/snake.json: assets/snake.json
|
||||||
|
cp $^ $@
|
||||||
|
|
||||||
|
public/css/snek.css: src/less/snek.less
|
||||||
|
lessc $^ $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(OUTPUT)
|
12
README.md
12
README.md
|
@ -5,3 +5,15 @@ A simple Snake, done as my final JS class project
|
||||||
|
|
||||||
[Original subject](https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html)
|
[Original subject](https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html)
|
||||||
|
|
||||||
|
## Dev dependencies
|
||||||
|
- Imagemagick, with the `convert` tool in the PATH
|
||||||
|
- Make
|
||||||
|
- A Less compiler, with `lessc` in the PATH
|
||||||
|
|
||||||
|
## Running
|
||||||
|
- Clone this repo
|
||||||
|
- `npm i`
|
||||||
|
- `npm start`
|
||||||
|
|
||||||
|
## Generating the assets
|
||||||
|
- `npm run-script build` or `make`
|
||||||
|
|
BIN
assets/apple.png
Executable file
BIN
assets/apple.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
7
assets/snake.json
Normal file
7
assets/snake.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"color": "#fba49b",
|
||||||
|
"join": "round",
|
||||||
|
"cap": "round",
|
||||||
|
"headSize": 0.8,
|
||||||
|
"tailSize": 0.4
|
||||||
|
}
|
BIN
assets/wall.png
Executable file
BIN
assets/wall.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 382 KiB |
102
fullreset.less
Normal file
102
fullreset.less
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
html5doctor.com Reset Stylesheet
|
||||||
|
v1.6.1
|
||||||
|
Last Updated: 2010-09-17
|
||||||
|
Author: Richard Clark - http://richclarkdesign.com
|
||||||
|
Twitter: @rich_clark
|
||||||
|
*/
|
||||||
|
|
||||||
|
html, body, div, span, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
abbr, address, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, samp,
|
||||||
|
small, strong, sub, sup, var,
|
||||||
|
b, i,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
border:0;
|
||||||
|
outline:0;
|
||||||
|
font-size:100%;
|
||||||
|
vertical-align:baseline;
|
||||||
|
background:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
article,aside,details,figcaption,figure,
|
||||||
|
footer,header,hgroup,menu,nav,section {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote, q {
|
||||||
|
quotes:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content:'';
|
||||||
|
content:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
font-size:100%;
|
||||||
|
vertical-align:baseline;
|
||||||
|
background:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* change colours to suit your needs */
|
||||||
|
ins {
|
||||||
|
background-color:#ff9;
|
||||||
|
color:#000;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* change colours to suit your needs */
|
||||||
|
mark {
|
||||||
|
background-color:#ff9;
|
||||||
|
color:#000;
|
||||||
|
font-style:italic;
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title], dfn[title] {
|
||||||
|
border-bottom:1px dotted;
|
||||||
|
cursor:help;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-spacing:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* change border colour to suit your needs */
|
||||||
|
hr {
|
||||||
|
display:block;
|
||||||
|
height:1px;
|
||||||
|
border:0;
|
||||||
|
border-top:1px solid #cccccc;
|
||||||
|
margin:1em 0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
vertical-align:middle;
|
||||||
|
}
|
10
index.js
Normal file
10
index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
const express=require('express');
|
||||||
|
|
||||||
|
const app=express();
|
||||||
|
const PORT=process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(express.static('public'));
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Listening on 0.0.0.0:${PORT}`);
|
||||||
|
});
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -40,11 +40,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||||
},
|
},
|
||||||
"coffeescript": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz",
|
|
||||||
"integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ=="
|
|
||||||
},
|
|
||||||
"content-disposition": {
|
"content-disposition": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
"author": "Codinget",
|
"author": "Codinget",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"coffeescript": "^2.5.1",
|
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
public/index.html
Normal file
25
public/index.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Snek</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="favicon" href="favicon.ico">
|
||||||
|
<link rel="stylesheet" href="css/snek.css">
|
||||||
|
<script src="js/snek.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<img src="assets/icon256.png">
|
||||||
|
<h1>Snek</h1>
|
||||||
|
<h2>A simple Snake</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<img src="assets/apple32.png">
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<img src="assets/icon32.png">
|
||||||
|
<p>Snek by <a href="https://codinget.me">Codinget</a>
|
||||||
|
<p>Original <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/enseignement/intro-js/s6.html">subject</a> by <a href="https://perso.liris.cnrs.fr/pierre-antoine.champin/">P.A. Champin</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
193
public/js/snek.js
Normal file
193
public/js/snek.js
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys();
|
||||||
|
|
||||||
|
const ifNaN=(v, r) => isNaN(v)?r:v;
|
||||||
|
|
||||||
|
class SnekGame {
|
||||||
|
constructor(settings, canvas) {
|
||||||
|
// build the world
|
||||||
|
this.dimensions=[...settings.dimensions];
|
||||||
|
this.world=Array(settings.dimensions[0])
|
||||||
|
.forEach((_, i, a) => a[i]=Array(settings.dimensions[1]).fill(EMPTY));
|
||||||
|
settings.walls.forEach(([x, y]) => this.world[x][y]=WALL);
|
||||||
|
settings.food.forEach(([x, y]) => this.world[x][y]=FOOD);
|
||||||
|
settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE);
|
||||||
|
|
||||||
|
// setup the delay
|
||||||
|
this.delay=settings.delay;
|
||||||
|
|
||||||
|
// get the head and initial direction
|
||||||
|
this.head=[...settings.snake[0]];
|
||||||
|
this.direction=[
|
||||||
|
ifNaN(settings.snake[1][0]-settings.snake[0][0], 1),
|
||||||
|
ifNaN(settings.snake[1][1]-settings.snake[0][1], 0)
|
||||||
|
];
|
||||||
|
|
||||||
|
// get the snake and the fruits themselves
|
||||||
|
this.snake=[...settings.snake];
|
||||||
|
this.fruits=[...settings.food];
|
||||||
|
|
||||||
|
// get our canvas, like, if we want to actually draw
|
||||||
|
this.canvas=canvas;
|
||||||
|
this.ctx=canvas.getContext('2d');
|
||||||
|
//TODO this.gl=canvas.getContext('webgl');
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
// clear the canvas, because it's easier than having to deal with everything
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.with, this.canvas.height);
|
||||||
|
|
||||||
|
// get the cell size and offset
|
||||||
|
const cellSize=Math.min(
|
||||||
|
this.canvas.with/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 our walls
|
||||||
|
const wall=Assets.get('wall');
|
||||||
|
for(let x=0; x<this.dimensions[0]; x++) {
|
||||||
|
for(let y=0; x<this.dimensions[1]; y++) {
|
||||||
|
switch(this.world[x][y]) {
|
||||||
|
case WALL:
|
||||||
|
this.ctx.drawImage(
|
||||||
|
wall,
|
||||||
|
offsetX+cellSize*x,
|
||||||
|
offsetY+cellSize*y,
|
||||||
|
cellSize,
|
||||||
|
cellSize
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw our snake
|
||||||
|
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) => {
|
||||||
|
this.ctx.lineTo(
|
||||||
|
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/1000*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*x+(1-fruitScale)*cellSize/2,
|
||||||
|
cellSize*fruitScale,
|
||||||
|
cellSize*fruitScale
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
step() {
|
||||||
|
// compute our new head
|
||||||
|
const head=[
|
||||||
|
this.snake[0][0]+this.direction[0],
|
||||||
|
this.snake[0][1]+this.direction[1]
|
||||||
|
];
|
||||||
|
|
||||||
|
// get our tail out of the way
|
||||||
|
const tail=this.snake.pop();
|
||||||
|
this.world[tail[0]][tail[1]]=EMPTY;
|
||||||
|
|
||||||
|
switch(this.world[head[0]][head[1]]) {
|
||||||
|
// you hit, you die
|
||||||
|
case WALL:
|
||||||
|
case SNAKE:
|
||||||
|
return this.die();
|
||||||
|
|
||||||
|
// you eat, you don't die
|
||||||
|
case FRUIT:
|
||||||
|
// re-grow the snake
|
||||||
|
this.snake.push(tail);
|
||||||
|
this.world[tail[0]][tail[1]]=SNAKE;
|
||||||
|
|
||||||
|
// remove the fruit from existence
|
||||||
|
this.world[head[0]][head[1]]=SNAKE;
|
||||||
|
this.fruits.splice(
|
||||||
|
this.fruits.find(
|
||||||
|
([x, y]) => x==head[0] && y==head[1]
|
||||||
|
),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// custom rules
|
||||||
|
if(this.rules.regrowFruits) {
|
||||||
|
const emptyCells=this.world
|
||||||
|
.map(
|
||||||
|
(l, x) => l
|
||||||
|
.map(
|
||||||
|
(r, y) => r==EMPTY?[x,y]:null
|
||||||
|
).filter(
|
||||||
|
a => a
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)];
|
||||||
|
this.fruits.push(cell);
|
||||||
|
this.world[cell[0]][cell[1]]=FRUIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move our head forward
|
||||||
|
this.world[head[0]][head[1]]=SNAKE;
|
||||||
|
this.snake.unshift(head);
|
||||||
|
|
||||||
|
// victory condition
|
||||||
|
if(!this.fruits.length) return this.win();
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
if(!this.lastStep) this.lastStep=this.firstStep;
|
||||||
|
if(this.lastStep+delay<Date.now()) {
|
||||||
|
this.lastStep+=delay;
|
||||||
|
this.step();
|
||||||
|
}
|
||||||
|
this.draw();
|
||||||
|
requestAnimationFrame(() => this.tick());
|
||||||
|
}
|
||||||
|
|
||||||
|
win() {
|
||||||
|
this.playing=false;
|
||||||
|
// you gud lol
|
||||||
|
console.log("You gud lol");
|
||||||
|
console.log(`Won in ${(Date.now()-this.firstStep)/1000} seconds`);
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
this.playing=false;
|
||||||
|
// you bad lol
|
||||||
|
console.log("You bad lol");
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.firstStep=Date.now();
|
||||||
|
requestAnimationFrame(() => this.tick());
|
||||||
|
}
|
||||||
|
}
|
81
src/less/snek.less
Normal file
81
src/less/snek.less
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// full CSS reset
|
||||||
|
@import 'fullreset.less';
|
||||||
|
|
||||||
|
// load the font
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Fira+Code:400,700&display=swap');
|
||||||
|
html {
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup REM units
|
||||||
|
html {
|
||||||
|
font-size: 62.5% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the colors and styles
|
||||||
|
@accentbg: #fba49b;
|
||||||
|
@bg: #ffefdf;
|
||||||
|
@accentfg: #930a16;
|
||||||
|
@fg: #23090d;
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: @fg;
|
||||||
|
background: @bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, strong, a {
|
||||||
|
color: @accentfg;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
header, footer {
|
||||||
|
background: @accentbg;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 4px solid @accentfg;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the layout
|
||||||
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img, footer img {
|
||||||
|
float: left;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
header img {
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
footer img {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header, footer, main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
Loading…
Reference in a new issue