parent
b38935bbe1
commit
faead71ba7
@ -1 +1,6 @@ |
|||||||
node_modules/ |
node_modules/ |
||||||
|
public/assets/*.png |
||||||
|
public/assets/*.json |
||||||
|
public/css/*.css |
||||||
|
public/favicon.ico |
||||||
|
|
||||||
|
@ -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)
|
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1,7 @@ |
|||||||
|
{ |
||||||
|
"color": "#fba49b", |
||||||
|
"join": "round", |
||||||
|
"cap": "round", |
||||||
|
"headSize": 0.8, |
||||||
|
"tailSize": 0.4 |
||||||
|
} |
After Width: | Height: | Size: 382 KiB |
@ -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; |
||||||
|
} |
@ -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}`); |
||||||
|
}); |
||||||
|
|
@ -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> |
@ -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()); |
||||||
|
} |
||||||
|
} |
@ -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 new issue