PK NV snake/README.mdUT c# Snake
A Pen created on CodePen.io. Original URL: [https://codepen.io/ste-vg/pen/EEZoYd](https://codepen.io/ste-vg/pen/EEZoYd).
A retro game of Snake with keyboard and touch controls.
Built with typescript, rx.js and jsfx.PK+. PK NV snake/LICENSE.txtUT cThe MIT License (MIT)
Copyright (c) 2023 Steve Gardner (https://codepen.io/ste-vg/pen/EEZoYd)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.PKqR^ ^ PK NV snake/src/index.htmlUT c
Use keyboard arrow keys to control
Swipe up, down, left or right to control.
PKNJ PK NV snake/src/style.scssUT c$color-background: #7ca256;
$color-background-dark: darken(#7ca256, 1%);
$color-snake: #212121;
html, body
{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #d2e0d2;
overflow: hidden;
}
@import url('https://fonts.googleapis.com/css?family=VT323');
$tilt-amount: 10deg;
$shine-move: 40px;
$snake-lift: 30px;
h1
{
margin: 0;
}
#container
{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
text-transform: uppercase;
font-family: 'VT323', monospace;
perspective: 1000px;
#board-container
{
border-radius: 8px;
margin: 20px;
padding: 20px 20px 10px 20px;
//box-shadow: 9px 7px 30px -6px rgba(0,0,0,0.25);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
background-color: white;
background-image: radial-gradient(farthest-corner at 10px 10px, $color-background 0%, $color-background-dark 100%);
transform: rotateX(0deg);
transform-style: preserve-3d;
transition: transform 0.3s ease, box-shadow 0.5s ease;
//overflow: hidden;
#board
{
border-radius: 5px;
border: solid 1px $color-snake;
transform: translateZ($snake-lift);
}
.shine-container
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 8px;
overflow: hidden;
.shine
{
position: absolute;
top: -$shine-move;
left: -$shine-move;
right: -$shine-move;
bottom: -$shine-move;
background: linear-gradient(45deg, rgba(255,255,255,.75) 0%,rgba(255,255,255,0) 60%);
transition: transform 0.3s ease;
}
}
.info-container
{
height: 100%;
width: 100%;
min-height: 50px;
margin-top: 5px;
display: flex;
flex-direction: row;
//align-items: center;
align-content: center;
justify-content: space-between;
transform: translateZ(20px);
.label
{
margin: 5px 5px 0 0;
}
#score
{
font-size: 1.5em;
font-weight: 300;
padding: 10px 20px;
}
.flex
{
flex: 1;
}
}
}
&.PLAYING
{
#board-container
{
box-shadow: 0 45px 100px rgba(0, 0, 0, 0.3);
transform: translateZ(40px);
&.up
{
transform: rotateX($tilt-amount);
.shine-container .shine { transform: rotateX($tilt-amount) translateX(-$shine-move) translateZ(1px) }
}
&.down
{
transform: rotateX(-$tilt-amount);
.shine-container .shine { transform: rotateX(-$tilt-amount) translateX($shine-move) translateZ(1px) }
}
&.left
{
transform: rotateY(-$tilt-amount);
.shine-container .shine { transform: rotateY(-$tilt-amount) translateY(-$shine-move) translateZ(1px) }
}
&.right
{
transform: rotateY($tilt-amount);
.shine-container .shine { transform: rotateY($tilt-amount) translateY($shine-move) translateZ(1px) }
}
}
}
#start-button
{
font-family: inherit;
text-transform: uppercase;
font-size: 1.5em;
background-color: transparent;
color: $color-snake;
padding: 10px 20px;
//border: 2px solid $color-snake;
border: 0;
border-radius: 2px;
//margin: 10px 10px 40px 10px;
cursor: pointer;
outline: none;
&:hover
{
color: white;
// background-color: $color-background;
border-color: $color-background;
}
}
.state-driven
{
display: none;
}
&.READY
{
#start-button
{
display: block;
}
}
&.ENDED
{
#start-button
{
display: block;
}
.re
{
display: inline;
}
}
.controls
{
.keyboard{ display: inline; }
.touch{ display: none; }
@media (any-hover: none) and (any-pointer: coarse)
{
.keyboard{ display: none; }
.touch{ display: inline; }
}
}
}
@keyframes flash
{
0% { opacity: 1 }
50% { opacity: 0 }
}
#board
{
--grid-columns: 0;
--grid-rows: 0;
--grid-size: 0;
width: calc(var(--grid-size) * var(--grid-columns) * 1px);
height: calc(var(--grid-size) * var(--grid-rows) * 1px);
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-template-rows: repeat(var(--grid-rows), 1fr);
grid-gap: 1px;
>div
{
background-color: transparent;
&.food, &.snake
{
box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
&.food
{
background-color: $color-snake;
border-radius: 50%;
margin: 1px;
}
&.snake
{
background-color: $color-snake;
&.head
{
&.up{ border-top-left-radius: 50%; border-top-right-radius: 50%;}
&.down{ border-bottom-left-radius: 50%; border-bottom-right-radius: 50%;}
&.left{ border-top-left-radius: 50%; border-bottom-left-radius: 50%;}
&.right{ border-top-right-radius: 50%; border-bottom-right-radius: 50%;}
}
&.tail
{
&.up{ border-bottom-left-radius: 50% 100%; border-bottom-right-radius: 50% 100%;}
&.down{ border-top-left-radius: 50% 100%; border-top-right-radius: 50% 100%;}
&.right{ border-bottom-left-radius: 100% 50%; border-top-left-radius: 100% 50%;}
&.left{ border-bottom-right-radius: 100% 50%; border-top-right-radius: 100% 50%;}
}
&.turn-left
{
&.up{ border-top-right-radius: 50%;}
&.down{ border-bottom-right-radius: 50%;}
}
&.turn-right
{
&.up{ border-top-left-radius: 50%;}
&.down{ border-bottom-left-radius: 50%;}
}
&.turn-up
{
&.left{ border-bottom-left-radius: 50%;}
&.right{ border-bottom-right-radius: 50%;}
}
&.turn-down
{
&.left{ border-top-left-radius: 50%;}
&.right{ border-top-right-radius: 50%;}
}
&.dead
{
animation: flash 0.3s steps(1) infinite;
}
}
}
}
$snake-shadow: 5px;
.up #board >div
{
&.food, &.snake { box-shadow: 0px $snake-shadow 0px 0px rgba(0,0,0,0.1);}
}
.down #board >div
{
&.food, &.snake { box-shadow: 0px (-$snake-shadow) 0px 0px rgba(0,0,0,0.1);}
}
.left #board >div
{
&.food, &.snake { box-shadow: $snake-shadow 0px 0px 0px rgba(0,0,0,0.1);}
}
.right #board >div
{
&.food, &.snake { box-shadow: -$snake-shadow 0px 0px 0px rgba(0,0,0,0.1);}
}
PK!8" PK NV snake/src/script.tsUT c
// Also on Github with Webpack and better Typescript support
// https://github.com/ste-vg/snake
console.clear();
enum GAME_STATES
{
ready = 'READY',
playing = 'PLAYING',
ended = 'ENDED',
paused = 'PAUSED'
}
enum SOUND
{
move = 'move',
dead = 'dead',
collect = 'collect',
start = 'start'
}
interface Direction
{
name: string;
x: number;
y: number;
}
interface Position
{
x: number;
y: number;
}
interface SnakePart
{
position: Position;
direction: Direction;
}
interface States
{
direction: Direction;
nextDirection: Direction[];
speed: number;
game: string;
timeStamp: number;
snakeLength: number;
score: number;
}
class App
{
private game:Snake;
private score:HTMLElement;
private container:HTMLElement;
private boardContainer:HTMLElement;
private gameState:string;
constructor()
{
this.setupUI();
this.setupGame();
}
setupUI()
{
this.score = document.getElementById('score');
this.container = document.getElementById('container');
this.boardContainer = document.getElementById('board-container');
let startButton = Rx.Observable.fromEvent(document.getElementById('start-button'), 'click');
startButton.subscribe((e:MouseEvent) => { console.log('click'); this.startGame(); })
}
setupGame()
{
let board = document.getElementById('board');
this.game = new Snake(board);
this.game.score.subscribe((score:number) => this.score.innerHTML = String(score));
this.game.state.subscribe((state:string) =>
{
this.gameState = state;
this.container.setAttribute('class', state)
})
this.game.direction.subscribe((direction:string) => this.boardContainer.setAttribute('class', direction))
this.game.reset();
}
startGame()
{
if(this.gameState == GAME_STATES.ready || this.gameState == GAME_STATES.ended)
{
this.game.start();
}
}
}
class Snake
{
private SETTINGS = {
grid: {size: 10, rows: 20, columns: 28},
game: {scoreIncrement: 10},
snake: {startLength: 3, startSpeed: 300, speedIncrement: 10, minSpeed: 100, growBy: 2}
}
private DIRECTION = {
up: {name: 'up', x: 0, y: -1},
down: {name: 'down', x: 0, y: 1},
left: {name: 'left', x: -1, y: 0},
right: {name: 'right', x: 1, y: 0},
}
private states:States = {
direction: this.DIRECTION.up,
nextDirection: [this.DIRECTION.up],
speed: 0,
game: GAME_STATES.ready,
timeStamp: 0,
snakeLength: 0,
score: 0
}
//http://loov.io/jsfx
private sfxLibrary:any = {
"start":{
"Frequency":{"Start":463.2977575242697,"Slide":0.4268311992714056,"RepeatSpeed":0.6870767779635416},
"Generator":{"A":0.015696072909390766},
"Volume":{"Sustain":0.11353385475559997,"Decay":0.15242709930669884}
},
"collect1":{
"Frequency":{"Start":1183.9224793246758,"ChangeSpeed":0.12793431035602038,"ChangeAmount":4.8612434857196085},
"Volume":{"Sustain":0.011448880380128946,"Decay":0.3895997546965799,"Punch":0.4554389528366015}
},
"collect2":{
"Frequency":{"Start":1070.9337014976563,"ChangeSpeed":0.1375978771153015,"ChangeAmount":5.9409661118536246},
"Volume":{"Sustain":0.04890791064198004,"Decay":0.3415421194668815,"Punch":0.46291381941601983}
},
"dead":{
"Frequency":{"Start":194.70758491034655,"Slide":-0.011628522004559189,"ChangeSpeed":0.6591296059731018,"ChangeAmount":2.6287197798189297},
"Generator":{"Func":"noise"},
"Volume":{"Sustain":0.17655222296084297,"Decay":0.24077933399701645,"Punch":0.6485369099751499}
},
"move1":{
"Frequency":{"Start":452,"Slide":-0.04,"Min":30,"DeltaSlide":-0.05},
"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
"Filter":{"HP":0.10068425608105215},
"Volume":{"Sustain":0,"Decay":0.041,"Attack":0.011,"Punch":0.04,"Master":0.18}
},
"move2":{
"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
"Phaser":{"Offset":-0.03,"Sweep":-0.02},
"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.06}
},
"move3":{
"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
"Phaser":{"Offset":-0.03,"Sweep":-0.02},
"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.16}
},
"move4":{
"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
"Phaser":{"Offset":-0.03,"Sweep":-0.02},
"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.27}
}
}
private player:any = jsfx.Sounds(this.sfxLibrary);
private sounds:any = {
collect: ['collect1', 'collect2'],
dead: ['dead'],
start: ['start'],
move: ['move1', 'move2', 'move3', 'move4']
}
private board:HTMLElement;
private grid:HTMLElement[] = [];
private snake:SnakePart[] = [];
private food:Position;
private touchStartPosition:Position;
// subjects
public state:Subject = new Rx.Subject();
public score:Subject = new Rx.Subject();
public direction:Subject = new Rx.Subject();
// observables
private keyPress:Observable;
private input:Input;
// subscriptions
private keyPressSubscription:Subscription;
private touchStartSubscription:Subscription;
private touchEndSubscription:Subscription;
private keyRestartSubscription:Subscription;
constructor(boardElement: HTMLElement)
{
this.board = boardElement;
// setup the game board grid
this.board.style.setProperty("--grid-size", String(this.SETTINGS.grid.size));
this.board.style.setProperty("--grid-columns", String(this.SETTINGS.grid.columns));
this.board.style.setProperty("--grid-rows", String(this.SETTINGS.grid.rows));
let count = this.SETTINGS.grid.columns * this.SETTINGS.grid.rows;
for(let i = 0; i < count; i++)
{
let sq = document.createElement("div");
this.grid.push(sq);
this.board.appendChild(sq);
}
// setup observables
this.input = new Input(document.body);
this.keyPress = Rx.Observable.fromEvent(document, "keydown")
.filter((e:KeyboardEvent) => ['arrowright', 'arrowleft', 'arrowup', 'arrowdown'].indexOf(e.key.toLowerCase()) >= 0)
.map((e:KeyboardEvent) =>
{
e.preventDefault();
return e.key.toLowerCase().replace('arrow','')
})
let onEnter = Rx.Observable.fromEvent(document, "keydown")
.filter((e:KeyboardEvent) => ['enter'].indexOf(e.key.toLowerCase()) >= 0)
this.touchStartSubscription = this.input.starts.subscribe((position:Position) => {
this.touchStartPosition = position;
})
this.touchEndSubscription = this.input.ends.subscribe((position:Position) =>
{
let hDiff = this.touchStartPosition.x - position.x;
let hDiffAbs = Math.abs(hDiff);
let vDiff = this.touchStartPosition.y - position.y;
let vDiffAbs = Math.abs(vDiff);
if(hDiffAbs > 10 || vDiffAbs > 10)
{
if(hDiffAbs > vDiffAbs)
{
if(hDiff < 0) this.setDirection(this.DIRECTION['right']);
else this.setDirection(this.DIRECTION['left']);
}
else
{
if(vDiff < 0) this.setDirection(this.DIRECTION['down']);
else this.setDirection(this.DIRECTION['up']);
}
}
})
this.keyPressSubscription = this.keyPress.subscribe((key: string) =>
{
if(this.states.game == GAME_STATES.playing)
{
this.setDirection(this.DIRECTION[key])
}
})
this.keyRestartSubscription = onEnter.subscribe(e => this.start())
}
private playSound(type:SOUND)
{
let options = this.sounds[type];
let selected = options[Math.floor(Math.random() * options.length)];
this.player[selected]();
}
private checkDirection(setDirection:Direction, newDirection:Direction):boolean
{
return setDirection.x != newDirection.x && setDirection.y != newDirection.y;
}
private setDirection(direction:Direction)
{
let queueable:boolean = false;
if(this.states.direction.name != this.states.nextDirection[0].name)
{
//if a valid move we could queue this move
if(this.states.nextDirection.length == 1 && this.checkDirection(this.states.nextDirection[0], direction))
{
queueable = true;
}
}
if(queueable && this.checkDirection(this.states.nextDirection[0], direction))
{
this.states.nextDirection.push(direction);
this.playSound(SOUND.move);
}
else if(this.checkDirection(this.states.direction, direction))
{
this.states.nextDirection = [direction];
this.playSound(SOUND.move);
}
}
public reset()
{
this.updateGameState(GAME_STATES.ready);
this.snake = []
this.states.direction = this.DIRECTION.up;
this.states.nextDirection = [this.DIRECTION.up];
this.states.snakeLength = this.SETTINGS.snake.startLength;
this.updateScore(0);
let center:Position = {x: Math.round(this.SETTINGS.grid.columns / 2), y: Math.round(this.SETTINGS.grid.rows / 2)};
for(let i = 0; i < this.states.snakeLength; i++)
{
let snakePart:SnakePart = {
position: {x: center.x, y: center.y + (i * 1)},
direction: this.DIRECTION.up
}
this.snake.unshift(snakePart);
}
this.placeFood();
this.draw();
}
private draw()
{
// reset all sqaures
for(let i = 0; i < this.grid.length; i++) this.grid[i].className = '';
// set snake squares
for(let i = 0; i < this.snake.length; i++)
{
let classes = ['snake'];
if(this.states.game == GAME_STATES.ended) classes.push('dead');
if(i == 0) classes.push('tail');
if(i == this.snake.length - 1) classes.push('head');
let snakePart = this.snake[i];
let nextSnakePart = this.snake[i + 1] ? this.snake[i + 1] : null;
if(nextSnakePart && snakePart.direction.name != nextSnakePart.direction.name)
{
classes.push('turn-' + nextSnakePart.direction.name)
}
if(i == 0 && nextSnakePart)
{
classes.push(nextSnakePart.direction.name);
}
else
{
classes.push(snakePart.direction.name);
}
let gridIndex = this.getIndexFromPosition(snakePart.position);
this.grid[gridIndex].className = classes.join(' ');
}
// set food sqaure
let foodSquare = this.grid[this.getIndexFromPosition(this.food)];
foodSquare.className = 'food';
}
private getIndexFromPosition(position:Position):number
{
return position.x + (position.y * this.SETTINGS.grid.columns);
}
private getPositionFromIndex(index:number):Position
{
let y = Math.floor(index / this.SETTINGS.grid.columns);
let x = Math.floor(index % this.SETTINGS.grid.columns);
return {x: x, y: y};
}
private eatFood()
{
this.addScore();
this.playSound(SOUND.collect);
this.states.snakeLength += this.SETTINGS.snake.growBy;
this.states.speed -= this.SETTINGS.snake.speedIncrement;
if(this.states.speed < this.SETTINGS.snake.minSpeed) this.states.speed = this.SETTINGS.snake.minSpeed;
this.placeFood();
}
private updateGameState(newState:string)
{
this.states.game = newState;
this.state.next(this.states.game);
}
private addScore()
{
this.updateScore(this.states.score + this.SETTINGS.game.scoreIncrement);
}
private updateScore(newScore:number)
{
this.states.score = newScore;
this.score.next(this.states.score);
}
private placeFood()
{
let takenSpaces: number[] = [];
for(let i = 0; i < this.snake.length; i++)
{
let index = this.getIndexFromPosition(this.snake[i].position);
takenSpaces.push(index);
}
let availableSpaces: number[] = [];
for(let i = 0; i < this.grid.length; i++)
{
if(takenSpaces.indexOf(i) < 0) availableSpaces.push(i);
}
let i = Math.floor(Math.random() * availableSpaces.length);
this.food = this.getPositionFromIndex(availableSpaces[i]);
}
private tick(timeStamp:number)
{
if(this.states.game == GAME_STATES.playing)
{
if(!this.states.timeStamp || (timeStamp - this.states.timeStamp) > this.states.speed)
{
this.states.timeStamp = timeStamp;
if(this.states.nextDirection.length > 1)
{
this.states.direction = this.states.nextDirection.shift();
}
else
{
this.states.direction = this.states.nextDirection[0];
}
this.direction.next(this.states.nextDirection[this.states.nextDirection.length - 1].name);
let snakeHead = this.snake[this.snake.length - 1];
let newPosition:Position = {
x: snakeHead.position.x + this.states.direction.x,
y: snakeHead.position.y + this.states.direction.y
}
// end the game if the new postion is out of bounds
if( newPosition.x < 0 ||
newPosition.x > this.SETTINGS.grid.columns - 1 ||
newPosition.y < 0 ||
newPosition.y > this.SETTINGS.grid.rows - 1)
{
return this.end();
}
// end the game if the new position is already taken by snake
for(let i = 0; i < this.snake.length; i++)
{
if(this.snake[i].position.x == newPosition.x && this.snake[i].position.y == newPosition.y)
{
return this.end();
}
}
// all good to proceed with new snake head
let newSnakeHead:SnakePart = {
position: newPosition,
direction: this.DIRECTION[this.states.direction.name]
}
this.snake.push(newSnakeHead);
while(this.snake.length > this.states.snakeLength)
{
this.snake.shift();
}
// check if head is on food
if(newSnakeHead.position.x == this.food.x && newSnakeHead.position.y == this.food.y)
{
this.eatFood();
}
this.draw();
}
window.requestAnimationFrame(time => this.tick(time));
}
}
public start()
{
this.reset();
this.playSound(SOUND.start);
this.states.speed = this.SETTINGS.snake.startSpeed;
this.updateGameState(GAME_STATES.playing);
this.tick(0);
window.focus();
}
private end()
{
console.warn('GAME OVER')
this.playSound(SOUND.dead);
this.updateGameState(GAME_STATES.ended);
this.direction.next('');
this.draw();
}
}
// touch & mouse input code form https://codepen.io/HunorMarton/post/handling-complex-mouse-and-touch-events-with-rxjs
class Input
{
private mouseDowns:Observable;
private mouseMoves:Observable;
private mouseUps:Observable;
private touchStarts:Observable;
private touchMoves:Observable;
private touchEnds:Observable;
public starts:Observable;
public moves:Observable;
public ends:Observable;
constructor(element:HTMLElement)
{
this.mouseDowns = Rx.Observable.fromEvent(element, "mousedown").map(this.mouseEventToCoordinate);
this.mouseMoves = Rx.Observable.fromEvent(window, "mousemove").map(this.mouseEventToCoordinate);
this.mouseUps = Rx.Observable.fromEvent(window, "mouseup").map(this.mouseEventToCoordinate);
this.touchStarts = Rx.Observable.fromEvent(element, "touchstart").map(this.touchEventToCoordinate);
this.touchMoves = Rx.Observable.fromEvent(element, "touchmove").map(this.touchEventToCoordinate);
this.touchEnds = Rx.Observable.fromEvent(window, "touchend").map(this.touchEventToCoordinate);
this.starts = this.mouseDowns.merge(this.touchStarts);
this.moves = this.mouseMoves.merge(this.touchMoves);
this.ends = this.mouseUps.merge(this.touchEnds);
}
private mouseEventToCoordinate = (mouseEvent:MouseEvent) =>
{
mouseEvent.preventDefault();
return {
x: mouseEvent.clientX,
y: mouseEvent.clientY
};
};
private touchEventToCoordinate = (touchEvent:TouchEvent) =>
{
//touchEvent.preventDefault();
return {
x: touchEvent.changedTouches[0].clientX,
y: touchEvent.changedTouches[0].clientY
};
};
}
let app = new App();PKh!bd? ? PK NV snake/dist/index.htmlUT c
CodePen - Snake
Use keyboard arrow keys to control
Swipe up, down, left or right to control.
PKAR}
}
PK NV snake/dist/style.cssUT c@import url("https://fonts.googleapis.com/css?family=VT323");
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #d2e0d2;
overflow: hidden;
}
h1 {
margin: 0;
}
#container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
text-transform: uppercase;
font-family: "VT323", monospace;
perspective: 1000px;
}
#container #board-container {
border-radius: 8px;
margin: 20px;
padding: 20px 20px 10px 20px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
background-color: white;
background-image: radial-gradient(farthest-corner at 10px 10px, #7ca256 0%, #799f54 100%);
transform: rotateX(0deg);
transform-style: preserve-3d;
transition: transform 0.3s ease, box-shadow 0.5s ease;
}
#container #board-container #board {
border-radius: 5px;
border: solid 1px #212121;
transform: translateZ(30px);
}
#container #board-container .shine-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 8px;
overflow: hidden;
}
#container #board-container .shine-container .shine {
position: absolute;
top: -40px;
left: -40px;
right: -40px;
bottom: -40px;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0) 60%);
transition: transform 0.3s ease;
}
#container #board-container .info-container {
height: 100%;
width: 100%;
min-height: 50px;
margin-top: 5px;
display: flex;
flex-direction: row;
align-content: center;
justify-content: space-between;
transform: translateZ(20px);
}
#container #board-container .info-container .label {
margin: 5px 5px 0 0;
}
#container #board-container .info-container #score {
font-size: 1.5em;
font-weight: 300;
padding: 10px 20px;
}
#container #board-container .info-container .flex {
flex: 1;
}
#container.PLAYING #board-container {
box-shadow: 0 45px 100px rgba(0, 0, 0, 0.3);
transform: translateZ(40px);
}
#container.PLAYING #board-container.up {
transform: rotateX(10deg);
}
#container.PLAYING #board-container.up .shine-container .shine {
transform: rotateX(10deg) translateX(-40px) translateZ(1px);
}
#container.PLAYING #board-container.down {
transform: rotateX(-10deg);
}
#container.PLAYING #board-container.down .shine-container .shine {
transform: rotateX(-10deg) translateX(40px) translateZ(1px);
}
#container.PLAYING #board-container.left {
transform: rotateY(-10deg);
}
#container.PLAYING #board-container.left .shine-container .shine {
transform: rotateY(-10deg) translateY(-40px) translateZ(1px);
}
#container.PLAYING #board-container.right {
transform: rotateY(10deg);
}
#container.PLAYING #board-container.right .shine-container .shine {
transform: rotateY(10deg) translateY(40px) translateZ(1px);
}
#container #start-button {
font-family: inherit;
text-transform: uppercase;
font-size: 1.5em;
background-color: transparent;
color: #212121;
padding: 10px 20px;
border: 0;
border-radius: 2px;
cursor: pointer;
outline: none;
}
#container #start-button:hover {
color: white;
border-color: #7ca256;
}
#container .state-driven {
display: none;
}
#container.READY #start-button {
display: block;
}
#container.ENDED #start-button {
display: block;
}
#container.ENDED .re {
display: inline;
}
#container .controls .keyboard {
display: inline;
}
#container .controls .touch {
display: none;
}
@media (any-hover: none) and (any-pointer: coarse) {
#container .controls .keyboard {
display: none;
}
#container .controls .touch {
display: inline;
}
}
@-webkit-keyframes flash {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
}
@keyframes flash {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
}
#board {
--grid-columns: 0;
--grid-rows: 0;
--grid-size: 0;
width: calc(var(--grid-size) * var(--grid-columns) * 1px);
height: calc(var(--grid-size) * var(--grid-rows) * 1px);
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-template-rows: repeat(var(--grid-rows), 1fr);
grid-gap: 1px;
}
#board > div {
background-color: transparent;
}
#board > div.food, #board > div.snake {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
#board > div.food {
background-color: #212121;
border-radius: 50%;
margin: 1px;
}
#board > div.snake {
background-color: #212121;
}
#board > div.snake.head.up {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
}
#board > div.snake.head.down {
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
}
#board > div.snake.head.left {
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
}
#board > div.snake.head.right {
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
}
#board > div.snake.tail.up {
border-bottom-left-radius: 50% 100%;
border-bottom-right-radius: 50% 100%;
}
#board > div.snake.tail.down {
border-top-left-radius: 50% 100%;
border-top-right-radius: 50% 100%;
}
#board > div.snake.tail.right {
border-bottom-left-radius: 100% 50%;
border-top-left-radius: 100% 50%;
}
#board > div.snake.tail.left {
border-bottom-right-radius: 100% 50%;
border-top-right-radius: 100% 50%;
}
#board > div.snake.turn-left.up {
border-top-right-radius: 50%;
}
#board > div.snake.turn-left.down {
border-bottom-right-radius: 50%;
}
#board > div.snake.turn-right.up {
border-top-left-radius: 50%;
}
#board > div.snake.turn-right.down {
border-bottom-left-radius: 50%;
}
#board > div.snake.turn-up.left {
border-bottom-left-radius: 50%;
}
#board > div.snake.turn-up.right {
border-bottom-right-radius: 50%;
}
#board > div.snake.turn-down.left {
border-top-left-radius: 50%;
}
#board > div.snake.turn-down.right {
border-top-right-radius: 50%;
}
#board > div.snake.dead {
-webkit-animation: flash 0.3s steps(1) infinite;
animation: flash 0.3s steps(1) infinite;
}
.up #board > div.food, .up #board > div.snake {
box-shadow: 0px 5px 0px 0px rgba(0, 0, 0, 0.1);
}
.down #board > div.food, .down #board > div.snake {
box-shadow: 0px -5px 0px 0px rgba(0, 0, 0, 0.1);
}
.left #board > div.food, .left #board > div.snake {
box-shadow: 5px 0px 0px 0px rgba(0, 0, 0, 0.1);
}
.right #board > div.food, .right #board > div.snake {
box-shadow: -5px 0px 0px 0px rgba(0, 0, 0, 0.1);
}PK PK NV snake/dist/script.jsUT c"use strict";
// Also on Github with Webpack and better Typescript support
// https://github.com/ste-vg/snake
console.clear();
var GAME_STATES;
(function (GAME_STATES) {
GAME_STATES["ready"] = "READY";
GAME_STATES["playing"] = "PLAYING";
GAME_STATES["ended"] = "ENDED";
GAME_STATES["paused"] = "PAUSED";
})(GAME_STATES || (GAME_STATES = {}));
var SOUND;
(function (SOUND) {
SOUND["move"] = "move";
SOUND["dead"] = "dead";
SOUND["collect"] = "collect";
SOUND["start"] = "start";
})(SOUND || (SOUND = {}));
class App {
constructor() {
this.setupUI();
this.setupGame();
}
setupUI() {
this.score = document.getElementById('score');
this.container = document.getElementById('container');
this.boardContainer = document.getElementById('board-container');
let startButton = Rx.Observable.fromEvent(document.getElementById('start-button'), 'click');
startButton.subscribe((e) => { console.log('click'); this.startGame(); });
}
setupGame() {
let board = document.getElementById('board');
this.game = new Snake(board);
this.game.score.subscribe((score) => this.score.innerHTML = String(score));
this.game.state.subscribe((state) => {
this.gameState = state;
this.container.setAttribute('class', state);
});
this.game.direction.subscribe((direction) => this.boardContainer.setAttribute('class', direction));
this.game.reset();
}
startGame() {
if (this.gameState == GAME_STATES.ready || this.gameState == GAME_STATES.ended) {
this.game.start();
}
}
}
class Snake {
constructor(boardElement) {
this.SETTINGS = {
grid: { size: 10, rows: 20, columns: 28 },
game: { scoreIncrement: 10 },
snake: { startLength: 3, startSpeed: 300, speedIncrement: 10, minSpeed: 100, growBy: 2 }
};
this.DIRECTION = {
up: { name: 'up', x: 0, y: -1 },
down: { name: 'down', x: 0, y: 1 },
left: { name: 'left', x: -1, y: 0 },
right: { name: 'right', x: 1, y: 0 },
};
this.states = {
direction: this.DIRECTION.up,
nextDirection: [this.DIRECTION.up],
speed: 0,
game: GAME_STATES.ready,
timeStamp: 0,
snakeLength: 0,
score: 0
};
//http://loov.io/jsfx
this.sfxLibrary = {
"start": {
"Frequency": { "Start": 463.2977575242697, "Slide": 0.4268311992714056, "RepeatSpeed": 0.6870767779635416 },
"Generator": { "A": 0.015696072909390766 },
"Volume": { "Sustain": 0.11353385475559997, "Decay": 0.15242709930669884 }
},
"collect1": {
"Frequency": { "Start": 1183.9224793246758, "ChangeSpeed": 0.12793431035602038, "ChangeAmount": 4.8612434857196085 },
"Volume": { "Sustain": 0.011448880380128946, "Decay": 0.3895997546965799, "Punch": 0.4554389528366015 }
},
"collect2": {
"Frequency": { "Start": 1070.9337014976563, "ChangeSpeed": 0.1375978771153015, "ChangeAmount": 5.9409661118536246 },
"Volume": { "Sustain": 0.04890791064198004, "Decay": 0.3415421194668815, "Punch": 0.46291381941601983 }
},
"dead": {
"Frequency": { "Start": 194.70758491034655, "Slide": -0.011628522004559189, "ChangeSpeed": 0.6591296059731018, "ChangeAmount": 2.6287197798189297 },
"Generator": { "Func": "noise" },
"Volume": { "Sustain": 0.17655222296084297, "Decay": 0.24077933399701645, "Punch": 0.6485369099751499 }
},
"move1": {
"Frequency": { "Start": 452, "Slide": -0.04, "Min": 30, "DeltaSlide": -0.05 },
"Generator": { "Func": "sine", "A": 0.08999657142884616, "ASlide": 0.3390436675524937 },
"Filter": { "HP": 0.10068425608105215 },
"Volume": { "Sustain": 0, "Decay": 0.041, "Attack": 0.011, "Punch": 0.04, "Master": 0.18 }
},
"move2": {
"Frequency": { "Start": 452, "Slide": -0.01, "Min": 30, "DeltaSlide": -0.05 },
"Generator": { "Func": "sine", "A": 0.08999657142884616, "ASlide": 0.3390436675524937 },
"Filter": { "HP": 0.26, "LPResonance": 0, "HPSlide": 0.35, "LPSlide": 0.51, "LP": 1 },
"Volume": { "Sustain": 0.02, "Decay": 0.001, "Attack": 0.021, "Punch": 0.05, "Master": 0.18 },
"Phaser": { "Offset": -0.03, "Sweep": -0.02 },
"Vibrato": { "FrequencySlide": 0.04, "Frequency": 14.01, "Depth": 0.06 }
},
"move3": {
"Frequency": { "Start": 452, "Slide": -0.01, "Min": 30, "DeltaSlide": -0.05 },
"Generator": { "Func": "sine", "A": 0.08999657142884616, "ASlide": 0.3390436675524937 },
"Filter": { "HP": 0.26, "LPResonance": 0, "HPSlide": 0.35, "LPSlide": 0.51, "LP": 1 },
"Volume": { "Sustain": 0.02, "Decay": 0.001, "Attack": 0.021, "Punch": 0.05, "Master": 0.18 },
"Phaser": { "Offset": -0.03, "Sweep": -0.02 },
"Vibrato": { "FrequencySlide": 0.04, "Frequency": 14.01, "Depth": 0.16 }
},
"move4": {
"Frequency": { "Start": 452, "Slide": -0.01, "Min": 30, "DeltaSlide": -0.05 },
"Generator": { "Func": "sine", "A": 0.08999657142884616, "ASlide": 0.3390436675524937 },
"Filter": { "HP": 0.26, "LPResonance": 0, "HPSlide": 0.35, "LPSlide": 0.51, "LP": 1 },
"Volume": { "Sustain": 0.02, "Decay": 0.001, "Attack": 0.021, "Punch": 0.05, "Master": 0.18 },
"Phaser": { "Offset": -0.03, "Sweep": -0.02 },
"Vibrato": { "FrequencySlide": 0.04, "Frequency": 14.01, "Depth": 0.27 }
}
};
this.player = jsfx.Sounds(this.sfxLibrary);
this.sounds = {
collect: ['collect1', 'collect2'],
dead: ['dead'],
start: ['start'],
move: ['move1', 'move2', 'move3', 'move4']
};
this.grid = [];
this.snake = [];
// subjects
this.state = new Rx.Subject();
this.score = new Rx.Subject();
this.direction = new Rx.Subject();
this.board = boardElement;
// setup the game board grid
this.board.style.setProperty("--grid-size", String(this.SETTINGS.grid.size));
this.board.style.setProperty("--grid-columns", String(this.SETTINGS.grid.columns));
this.board.style.setProperty("--grid-rows", String(this.SETTINGS.grid.rows));
let count = this.SETTINGS.grid.columns * this.SETTINGS.grid.rows;
for (let i = 0; i < count; i++) {
let sq = document.createElement("div");
this.grid.push(sq);
this.board.appendChild(sq);
}
// setup observables
this.input = new Input(document.body);
this.keyPress = Rx.Observable.fromEvent(document, "keydown")
.filter((e) => ['arrowright', 'arrowleft', 'arrowup', 'arrowdown'].indexOf(e.key.toLowerCase()) >= 0)
.map((e) => {
e.preventDefault();
return e.key.toLowerCase().replace('arrow', '');
});
let onEnter = Rx.Observable.fromEvent(document, "keydown")
.filter((e) => ['enter'].indexOf(e.key.toLowerCase()) >= 0);
this.touchStartSubscription = this.input.starts.subscribe((position) => {
this.touchStartPosition = position;
});
this.touchEndSubscription = this.input.ends.subscribe((position) => {
let hDiff = this.touchStartPosition.x - position.x;
let hDiffAbs = Math.abs(hDiff);
let vDiff = this.touchStartPosition.y - position.y;
let vDiffAbs = Math.abs(vDiff);
if (hDiffAbs > 10 || vDiffAbs > 10) {
if (hDiffAbs > vDiffAbs) {
if (hDiff < 0)
this.setDirection(this.DIRECTION['right']);
else
this.setDirection(this.DIRECTION['left']);
}
else {
if (vDiff < 0)
this.setDirection(this.DIRECTION['down']);
else
this.setDirection(this.DIRECTION['up']);
}
}
});
this.keyPressSubscription = this.keyPress.subscribe((key) => {
if (this.states.game == GAME_STATES.playing) {
this.setDirection(this.DIRECTION[key]);
}
});
this.keyRestartSubscription = onEnter.subscribe(e => this.start());
}
playSound(type) {
let options = this.sounds[type];
let selected = options[Math.floor(Math.random() * options.length)];
this.player[selected]();
}
checkDirection(setDirection, newDirection) {
return setDirection.x != newDirection.x && setDirection.y != newDirection.y;
}
setDirection(direction) {
let queueable = false;
if (this.states.direction.name != this.states.nextDirection[0].name) {
//if a valid move we could queue this move
if (this.states.nextDirection.length == 1 && this.checkDirection(this.states.nextDirection[0], direction)) {
queueable = true;
}
}
if (queueable && this.checkDirection(this.states.nextDirection[0], direction)) {
this.states.nextDirection.push(direction);
this.playSound(SOUND.move);
}
else if (this.checkDirection(this.states.direction, direction)) {
this.states.nextDirection = [direction];
this.playSound(SOUND.move);
}
}
reset() {
this.updateGameState(GAME_STATES.ready);
this.snake = [];
this.states.direction = this.DIRECTION.up;
this.states.nextDirection = [this.DIRECTION.up];
this.states.snakeLength = this.SETTINGS.snake.startLength;
this.updateScore(0);
let center = { x: Math.round(this.SETTINGS.grid.columns / 2), y: Math.round(this.SETTINGS.grid.rows / 2) };
for (let i = 0; i < this.states.snakeLength; i++) {
let snakePart = {
position: { x: center.x, y: center.y + (i * 1) },
direction: this.DIRECTION.up
};
this.snake.unshift(snakePart);
}
this.placeFood();
this.draw();
}
draw() {
// reset all sqaures
for (let i = 0; i < this.grid.length; i++)
this.grid[i].className = '';
// set snake squares
for (let i = 0; i < this.snake.length; i++) {
let classes = ['snake'];
if (this.states.game == GAME_STATES.ended)
classes.push('dead');
if (i == 0)
classes.push('tail');
if (i == this.snake.length - 1)
classes.push('head');
let snakePart = this.snake[i];
let nextSnakePart = this.snake[i + 1] ? this.snake[i + 1] : null;
if (nextSnakePart && snakePart.direction.name != nextSnakePart.direction.name) {
classes.push('turn-' + nextSnakePart.direction.name);
}
if (i == 0 && nextSnakePart) {
classes.push(nextSnakePart.direction.name);
}
else {
classes.push(snakePart.direction.name);
}
let gridIndex = this.getIndexFromPosition(snakePart.position);
this.grid[gridIndex].className = classes.join(' ');
}
// set food sqaure
let foodSquare = this.grid[this.getIndexFromPosition(this.food)];
foodSquare.className = 'food';
}
getIndexFromPosition(position) {
return position.x + (position.y * this.SETTINGS.grid.columns);
}
getPositionFromIndex(index) {
let y = Math.floor(index / this.SETTINGS.grid.columns);
let x = Math.floor(index % this.SETTINGS.grid.columns);
return { x: x, y: y };
}
eatFood() {
this.addScore();
this.playSound(SOUND.collect);
this.states.snakeLength += this.SETTINGS.snake.growBy;
this.states.speed -= this.SETTINGS.snake.speedIncrement;
if (this.states.speed < this.SETTINGS.snake.minSpeed)
this.states.speed = this.SETTINGS.snake.minSpeed;
this.placeFood();
}
updateGameState(newState) {
this.states.game = newState;
this.state.next(this.states.game);
}
addScore() {
this.updateScore(this.states.score + this.SETTINGS.game.scoreIncrement);
}
updateScore(newScore) {
this.states.score = newScore;
this.score.next(this.states.score);
}
placeFood() {
let takenSpaces = [];
for (let i = 0; i < this.snake.length; i++) {
let index = this.getIndexFromPosition(this.snake[i].position);
takenSpaces.push(index);
}
let availableSpaces = [];
for (let i = 0; i < this.grid.length; i++) {
if (takenSpaces.indexOf(i) < 0)
availableSpaces.push(i);
}
let i = Math.floor(Math.random() * availableSpaces.length);
this.food = this.getPositionFromIndex(availableSpaces[i]);
}
tick(timeStamp) {
if (this.states.game == GAME_STATES.playing) {
if (!this.states.timeStamp || (timeStamp - this.states.timeStamp) > this.states.speed) {
this.states.timeStamp = timeStamp;
if (this.states.nextDirection.length > 1) {
this.states.direction = this.states.nextDirection.shift();
}
else {
this.states.direction = this.states.nextDirection[0];
}
this.direction.next(this.states.nextDirection[this.states.nextDirection.length - 1].name);
let snakeHead = this.snake[this.snake.length - 1];
let newPosition = {
x: snakeHead.position.x + this.states.direction.x,
y: snakeHead.position.y + this.states.direction.y
};
// end the game if the new postion is out of bounds
if (newPosition.x < 0 ||
newPosition.x > this.SETTINGS.grid.columns - 1 ||
newPosition.y < 0 ||
newPosition.y > this.SETTINGS.grid.rows - 1) {
return this.end();
}
// end the game if the new position is already taken by snake
for (let i = 0; i < this.snake.length; i++) {
if (this.snake[i].position.x == newPosition.x && this.snake[i].position.y == newPosition.y) {
return this.end();
}
}
// all good to proceed with new snake head
let newSnakeHead = {
position: newPosition,
direction: this.DIRECTION[this.states.direction.name]
};
this.snake.push(newSnakeHead);
while (this.snake.length > this.states.snakeLength) {
this.snake.shift();
}
// check if head is on food
if (newSnakeHead.position.x == this.food.x && newSnakeHead.position.y == this.food.y) {
this.eatFood();
}
this.draw();
}
window.requestAnimationFrame(time => this.tick(time));
}
}
start() {
this.reset();
this.playSound(SOUND.start);
this.states.speed = this.SETTINGS.snake.startSpeed;
this.updateGameState(GAME_STATES.playing);
this.tick(0);
window.focus();
}
end() {
console.warn('GAME OVER');
this.playSound(SOUND.dead);
this.updateGameState(GAME_STATES.ended);
this.direction.next('');
this.draw();
}
}
// touch & mouse input code form https://codepen.io/HunorMarton/post/handling-complex-mouse-and-touch-events-with-rxjs
class Input {
constructor(element) {
this.mouseEventToCoordinate = (mouseEvent) => {
mouseEvent.preventDefault();
return {
x: mouseEvent.clientX,
y: mouseEvent.clientY
};
};
this.touchEventToCoordinate = (touchEvent) => {
//touchEvent.preventDefault();
return {
x: touchEvent.changedTouches[0].clientX,
y: touchEvent.changedTouches[0].clientY
};
};
this.mouseDowns = Rx.Observable.fromEvent(element, "mousedown").map(this.mouseEventToCoordinate);
this.mouseMoves = Rx.Observable.fromEvent(window, "mousemove").map(this.mouseEventToCoordinate);
this.mouseUps = Rx.Observable.fromEvent(window, "mouseup").map(this.mouseEventToCoordinate);
this.touchStarts = Rx.Observable.fromEvent(element, "touchstart").map(this.touchEventToCoordinate);
this.touchMoves = Rx.Observable.fromEvent(element, "touchmove").map(this.touchEventToCoordinate);
this.touchEnds = Rx.Observable.fromEvent(window, "touchend").map(this.touchEventToCoordinate);
this.starts = this.mouseDowns.merge(this.touchStarts);
this.moves = this.mouseMoves.merge(this.touchMoves);
this.ends = this.mouseUps.merge(this.touchEnds);
}
}
let app = new App();PKo_QE E PK NV+. snake/README.mdUT cPK NVqR^ ^ ) snake/LICENSE.txtUT cPK NVNJ snake/src/index.htmlUT cPK NV!8" snake/src/style.scssUT cPK NVh!bd? ? ( snake/src/script.tsUT cPK NVAR}
}
h snake/dist/index.htmlUT cPK NV s snake/dist/style.cssUT cPK NVo_QE E ƌ snake/dist/script.jsUT cPK P )