added core Snake code

This commit is contained in:
Nathan DECHER 2020-03-23 20:11:39 +01:00
parent fe2902cdea
commit 7362b4dc5c
17 changed files with 477 additions and 6 deletions

5
.gitignore vendored
View file

@ -1 +1,6 @@
node_modules/ node_modules/
public/assets/*.png
public/assets/*.json
public/css/*.css
public/favicon.ico

View file

@ -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)

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

7
assets/snake.json Normal file
View file

@ -0,0 +1,7 @@
{
"color": "#fba49b",
"join": "round",
"cap": "round",
"headSize": 0.8,
"tailSize": 0.4
}

BIN
assets/wall.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

102
fullreset.less Normal file
View 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
View 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
View file

@ -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",

View file

@ -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"
} }
} }

View file

25
public/index.html Normal file
View 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
View 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());
}
}

View file

View file

81
src/less/snek.less Normal file
View 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;
}