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/
|
||||
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)
|
||||
|
||||
## 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",
|
||||
"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": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"author": "Codinget",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"coffeescript": "^2.5.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