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