ngx-snake
Version:
Snake game as an angular component, its only the core of the game... you need to add everything around it (controls, score...) yourself :)
368 lines (358 loc) • 16 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, Output, NgModule } from '@angular/core';
import { ReplaySubject, Subject, BehaviorSubject, interval } from 'rxjs';
import { switchMap, filter, tap, take } from 'rxjs/operators';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
var GameState;
(function (GameState) {
GameState[GameState["Paused"] = 0] = "Paused";
GameState[GameState["Started"] = 1] = "Started";
GameState[GameState["Over"] = 2] = "Over";
})(GameState || (GameState = {}));
var TileState;
(function (TileState) {
TileState["Free"] = "free";
TileState["Head"] = "head";
TileState["Body"] = "body";
TileState["Tail"] = "tail";
TileState["Wall"] = "wall";
TileState["Food"] = "food";
})(TileState || (TileState = {}));
var MoveDirections;
(function (MoveDirections) {
MoveDirections[MoveDirections["UP"] = 0] = "UP";
MoveDirections[MoveDirections["RIGHT"] = 1] = "RIGHT";
MoveDirections[MoveDirections["DOWN"] = 2] = "DOWN";
MoveDirections[MoveDirections["LEFT"] = 3] = "LEFT";
})(MoveDirections || (MoveDirections = {}));
const INITIAL_SPEED = 700;
class GameManagerService {
constructor() {
this._grid = [];
this._grid$ = new ReplaySubject(1);
this.grid$ = this._grid$.asObservable();
this._gameOver$ = new Subject();
this.gameOver$ = this._gameOver$;
this._foodEaten$ = new Subject();
this.foodEaten$ = this._foodEaten$;
this._gridSize = {
h: 10,
w: 10
};
this._snake = [];
this._nextMoveDir = MoveDirections.RIGHT;
this._moveDir = MoveDirections.RIGHT;
this._food = null;
this._interval$ = new BehaviorSubject(INITIAL_SPEED);
this._paused = true;
this._playable = true;
this.signal$ = this._interval$
.asObservable()
.pipe(switchMap((period) => interval(period)), filter(() => !this._paused), tap(() => this._gameCycle()));
}
initialize(height, width) {
this._gridSize.w = width;
this._gridSize.h = height;
this._buildEmptyGrid();
this._initSnake();
this._drawSnake();
this._gridChanged();
this._signalSub = this.signal$.subscribe();
}
ngOnDestroy() {
if (this._signalSub) {
this._signalSub.unsubscribe();
}
}
start() {
if (this._playable) {
this._paused = false;
}
}
changeSpeed(period) {
this._interval$.next(period);
}
pause() {
this._paused = true;
}
reset() {
this.pause();
this._playable = true;
this._moveDir = MoveDirections.RIGHT;
this._nextMoveDir = this._moveDir;
this._interval$.next(INITIAL_SPEED);
this._buildEmptyGrid();
this._initSnake();
this._drawSnake();
this._gridChanged();
}
up() { this._moveDir !== MoveDirections.DOWN ? this._nextMoveDir = MoveDirections.UP : this._moveDir; }
right() { this._moveDir !== MoveDirections.LEFT ? this._nextMoveDir = MoveDirections.RIGHT : this._moveDir; }
down() { this._moveDir !== MoveDirections.UP ? this._nextMoveDir = MoveDirections.DOWN : this._moveDir; }
left() { this._moveDir !== MoveDirections.RIGHT ? this._nextMoveDir = MoveDirections.LEFT : this._moveDir; }
_endGame() {
this._playable = false;
this.pause();
this._gameOver$.next();
}
_buildEmptyGrid() {
const newGrid = [];
for (let y = 0; y <= this._gridSize.h; y++) {
const row = [];
for (let x = 0; x <= this._gridSize.w; x++) {
row.push(TileState.Free);
}
newGrid.push(row);
}
this._grid = newGrid;
}
_gridChanged() {
this._grid$.next(this._grid);
}
_initSnake() {
this._snake = [];
const xCenter = Math.floor(this._gridSize.w / 2);
const yCenter = Math.floor(this._gridSize.h / 2);
this._snake.push({ x: xCenter, y: yCenter });
this._snake.push({ x: xCenter - 1, y: yCenter });
this._snake.push({ x: xCenter - 2, y: yCenter });
}
_spawnFood() {
if (!this._food) {
const eligibleFields = [];
for (let y = 1; y <= this._gridSize.h - 1; y++) {
for (let x = 1; x <= this._gridSize.w - 1; x++) {
if (this._grid[y][x] === TileState.Free) {
eligibleFields.push({
x, y
});
}
}
}
const shuffled = eligibleFields.sort((a, b) => 0.5 - Math.random());
this._food = shuffled[0];
}
this._grid[this._food.y][this._food.x] = TileState.Food;
}
_drawSnake() {
const head = this._snake[0];
const tail = this._snake[this._snake.length - 1];
this._grid[head.y][head.x] = TileState.Head;
for (let i = 1; i < this._snake.length - 1; i++) {
const part = this._snake[i];
this._grid[part.y][part.x] = TileState.Body;
}
this._grid[tail.y][tail.x] = TileState.Tail;
}
_gameCycle() {
const head = this._snake[0];
let newHead;
this._moveDir = this._nextMoveDir;
if (this._moveDir === MoveDirections.UP) {
newHead = { x: head.x, y: head.y - 1 };
}
else if (this._moveDir === MoveDirections.RIGHT) {
newHead = { x: head.x + 1, y: head.y };
}
else if (this._moveDir === MoveDirections.DOWN) {
newHead = { x: head.x, y: head.y + 1 };
}
else {
// Moving left
newHead = { x: head.x - 1, y: head.y };
}
if (this._willCrash(newHead)) {
return this._endGame();
}
// position new head
this._snake.unshift(newHead);
if (this._willGrow(newHead)) {
this._increaseSpeed();
this._foodEaten$.next();
this._food = null;
}
else {
// drop old tail
this._snake.pop();
}
this._buildEmptyGrid();
this._drawSnake();
this._spawnFood();
this._gridChanged();
}
/**
* Checks if field is not currently occupied (is free to take)
* @param newHead
* @private
*/
_willCrash(newHead) {
// Gets out of the board
if (newHead.x < 0 || newHead.y < 0 || newHead.x > this._gridSize.w || newHead.y > this._gridSize.h) {
return true;
}
const CRASHABLE_FIELDS = [
TileState.Body,
TileState.Wall
];
if (CRASHABLE_FIELDS.includes(this._grid[newHead.y][newHead.x])) {
return true;
}
// If crashing with tail then check if tail will move...
if (this._grid[newHead.y][newHead.x] === TileState.Tail && this._willGrow(newHead)) {
return true;
}
return false;
}
_willGrow(newHead) {
if (this._food && this._food.y === newHead.y && this._food.x === newHead.x) {
return true;
}
return false;
}
_increaseSpeed() {
this._interval$
.pipe(take(1))
.subscribe((current) => {
if (current >= 600) {
this._interval$.next(current - 100);
}
else if (current >= 500) {
this._interval$.next(current - 30);
}
else if (current >= 400) {
this._interval$.next(current - 20);
}
else {
this._interval$.next(current - 10);
}
});
}
}
GameManagerService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: GameManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
GameManagerService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: GameManagerService });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: GameManagerService, decorators: [{
type: Injectable
}] });
class TileComponent {
constructor(el, _renderer) {
this.el = el;
this._renderer = _renderer;
}
ngOnInit() {
if (this.state) {
this._renderer.addClass(this.el.nativeElement, this.state);
}
}
}
TileComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: TileComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
TileComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.1", type: TileComponent, selector: "ngx-snake-tile", inputs: { state: "state" }, ngImport: i0, template: `<div></div>`, isInline: true, styles: ["ngx-snake-tile{display:block;background:#ccf1ed;border:1px solid #4e4645;width:25px;height:25px;float:left;margin:1px;box-sizing:border-box}ngx-snake-tile div{height:100%;width:100%;display:block;background:#ccf1ed}ngx-snake-tile.free div{background:#ccf1ed}ngx-snake-tile.head div{background:#4e6c31}ngx-snake-tile.body div{background:#4e6c31}ngx-snake-tile.tail div{background:#4e6c31}ngx-snake-tile.wall div{background:#C2C3C7}ngx-snake-tile.food{padding:3px}ngx-snake-tile.food div{background:#00E436}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: TileComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-snake-tile', template: `<div></div>`, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["ngx-snake-tile{display:block;background:#ccf1ed;border:1px solid #4e4645;width:25px;height:25px;float:left;margin:1px;box-sizing:border-box}ngx-snake-tile div{height:100%;width:100%;display:block;background:#ccf1ed}ngx-snake-tile.free div{background:#ccf1ed}ngx-snake-tile.head div{background:#4e6c31}ngx-snake-tile.body div{background:#4e6c31}ngx-snake-tile.tail div{background:#4e6c31}ngx-snake-tile.wall div{background:#C2C3C7}ngx-snake-tile.food{padding:3px}ngx-snake-tile.food div{background:#00E436}\n"] }]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }]; }, propDecorators: { state: [{
type: Input
}] } });
class BoardComponent {
constructor() {
}
ngOnInit() {
}
}
BoardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: BoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
BoardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.1", type: BoardComponent, selector: "ngx-snake-board", inputs: { data: "data" }, ngImport: i0, template: "<div class=\"ngx-snake-board-row\" *ngFor=\"let row of data\">\n <ngx-snake-tile *ngFor=\"let tileState of row\"\n [state]=\"tileState\"></ngx-snake-tile>\n</div>\n", styles: [":host .ngx-snake-board-row{display:block;clear:both}\n"], components: [{ type: TileComponent, selector: "ngx-snake-tile", inputs: ["state"] }], directives: [{ type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: BoardComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-snake-board', template: "<div class=\"ngx-snake-board-row\" *ngFor=\"let row of data\">\n <ngx-snake-tile *ngFor=\"let tileState of row\"\n [state]=\"tileState\"></ngx-snake-tile>\n</div>\n", styles: [":host .ngx-snake-board-row{display:block;clear:both}\n"] }]
}], ctorParameters: function () { return []; }, propDecorators: { data: [{
type: Input
}] } });
class NgxSnakeComponent {
constructor(_manager) {
this._manager = _manager;
this.boardHeight = 10;
this.boardWidth = 10;
this.foodEaten = this._manager.foodEaten$;
this.gameOver = this._manager.gameOver$;
this.grid$ = this._manager.grid$;
}
ngOnInit() {
this._manager.initialize(this.boardHeight, this.boardWidth);
}
actionUp() { this._manager.up(); }
actionRight() { this._manager.right(); }
actionDown() { this._manager.down(); }
actionLeft() { this._manager.left(); }
actionStart() {
this._manager.start();
}
actionStop() {
this._manager.pause();
}
actionReset() {
this._manager.reset();
}
}
NgxSnakeComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeComponent, deps: [{ token: GameManagerService }], target: i0.ɵɵFactoryTarget.Component });
NgxSnakeComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.1", type: NgxSnakeComponent, selector: "ngx-snake", inputs: { boardHeight: "boardHeight", boardWidth: "boardWidth" }, outputs: { foodEaten: "foodEaten", gameOver: "gameOver" }, providers: [GameManagerService], ngImport: i0, template: `
<ngx-snake-board
[data]="grid$ | async"></ngx-snake-board>
`, isInline: true, components: [{ type: BoardComponent, selector: "ngx-snake-board", inputs: ["data"] }], pipes: { "async": i2.AsyncPipe } });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeComponent, decorators: [{
type: Component,
args: [{
selector: 'ngx-snake',
template: `
<ngx-snake-board
[data]="grid$ | async"></ngx-snake-board>
`,
styles: [],
providers: [GameManagerService]
}]
}], ctorParameters: function () { return [{ type: GameManagerService }]; }, propDecorators: { boardHeight: [{
type: Input
}], boardWidth: [{
type: Input
}], foodEaten: [{
type: Output
}], gameOver: [{
type: Output
}] } });
class NgxSnakeModule {
static forRoot() {
return {
ngModule: NgxSnakeModule
};
}
}
NgxSnakeModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
NgxSnakeModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeModule, declarations: [NgxSnakeComponent,
BoardComponent,
TileComponent], imports: [CommonModule], exports: [NgxSnakeComponent] });
NgxSnakeModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeModule, imports: [[
CommonModule
]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.1", ngImport: i0, type: NgxSnakeModule, decorators: [{
type: NgModule,
args: [{
declarations: [
NgxSnakeComponent,
BoardComponent,
TileComponent
],
imports: [
CommonModule
],
exports: [
NgxSnakeComponent
]
}]
}] });
/*
* Public API Surface of ngx-snake
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxSnakeComponent, NgxSnakeModule };
//# sourceMappingURL=ngx-snake.mjs.map