parent
b38935bbe1
commit
faead71ba7
@ -1 +1,6 @@ |
||||
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