diff --git a/.gitignore b/.gitignore index c2658d7..92a832d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ node_modules/ +public/assets/*.png +public/assets/*.json +public/css/*.css +public/favicon.ico + diff --git a/Makefile b/Makefile index e69de29..ecf1e02 100644 --- a/Makefile +++ b/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) diff --git a/README.md b/README.md index 2a37e92..fbbe9c6 100644 --- a/README.md +++ b/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` diff --git a/assets/apple.png b/assets/apple.png new file mode 100755 index 0000000..5e520ba Binary files /dev/null and b/assets/apple.png differ diff --git a/assets/snake.json b/assets/snake.json new file mode 100644 index 0000000..77ced44 --- /dev/null +++ b/assets/snake.json @@ -0,0 +1,7 @@ +{ + "color": "#fba49b", + "join": "round", + "cap": "round", + "headSize": 0.8, + "tailSize": 0.4 +} diff --git a/assets/wall.png b/assets/wall.png new file mode 100755 index 0000000..1d83b52 Binary files /dev/null and b/assets/wall.png differ diff --git a/fullreset.less b/fullreset.less new file mode 100644 index 0000000..784b3d9 --- /dev/null +++ b/fullreset.less @@ -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; +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..03d31f6 --- /dev/null +++ b/index.js @@ -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}`); +}); + diff --git a/package-lock.json b/package-lock.json index d47093c..da8f631 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8a005ca..911bcb9 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "author": "Codinget", "license": "MIT", "dependencies": { - "coffeescript": "^2.5.1", "express": "^4.17.1" } } diff --git a/public/.gitkeep b/public/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/assets/.gitkeep b/public/css/.gitkeep similarity index 100% rename from assets/.gitkeep rename to public/css/.gitkeep diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b6dde28 --- /dev/null +++ b/public/index.html @@ -0,0 +1,25 @@ + + + + Snek + + + + + + +
+ +

Snek

+

A simple Snake

+
+
+ +
+ + + diff --git a/public/js/snek.js b/public/js/snek.js new file mode 100644 index 0000000..511ff33 --- /dev/null +++ b/public/js/snek.js @@ -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.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 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()); + } +} diff --git a/public/levels/.gitkeep b/public/levels/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/less/snek.less b/src/less/snek.less new file mode 100644 index 0000000..d24f1c5 --- /dev/null +++ b/src/less/snek.less @@ -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; +}