Feature/snake (#1256)

* snake game

Co-authored-by: Drew Ansbacher <drew@start9labs.com>
This commit is contained in:
Drew Ansbacher
2022-02-24 16:20:52 -07:00
committed by GitHub
parent 63f0a3bca8
commit 8cb0186621
14 changed files with 487 additions and 2 deletions

View File

@@ -62,6 +62,17 @@
</ion-item>
</ion-menu-toggle>
</ion-item-group>
<img
(click)="openSnek()"
style="
cursor: pointer;
position: absolute;
bottom: 90px;
right: 20px;
width: 25px;
"
src="assets/img/icons/snek.png"
/>
<div
style="
text-align: center;

View File

@@ -32,6 +32,7 @@ import { LocalStorageService } from './services/local-storage.service'
import { EOSService } from './services/eos.service'
import { MarketplaceService } from './pages/marketplace-routes/marketplace.service'
import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
import { SnakePage } from './modals/snake/snake.page'
@Component({
selector: 'app-root',
@@ -39,6 +40,14 @@ import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
styleUrls: ['app.component.scss'],
})
export class AppComponent {
code = {
s: false,
n: false,
e: false,
k: false,
unlocked: false,
}
@HostListener('document:keydown.enter', ['$event'])
@debounce()
handleKeyboardEvent() {
@@ -48,6 +57,29 @@ export class AppComponent {
if (elem) elem.click()
}
@HostListener('document:keypress', ['$event'])
async keyPress(e: KeyboardEvent) {
if (e.repeat || this.code.unlocked) return
if (this.code[e.key] === false) {
this.code[e.key] = true
}
if (
Object.entries(this.code)
.filter(([key, value]) => key.length === 1)
.map(([key, value]) => value)
.reduce((a, b) => a && b)
) {
await this.openSnek()
}
}
@HostListener('document:keyup', ['$event'])
keyUp(e: KeyboardEvent) {
if (this.code[e.key]) {
this.code[e.key] = false
}
}
ServerStatus = ServerStatus
showMenu = false
selectedIndex = 0
@@ -238,6 +270,42 @@ export class AppComponent {
}
}
async openSnek() {
this.code.unlocked = true
const modal = await this.modalCtrl.create({
component: SnakePage,
cssClass: 'snake-modal',
backdropDismiss: false,
})
modal.onDidDismiss().then(async ret => {
this.code.unlocked = false
if (
ret.data.highScore &&
(ret.data.highScore >
this.patch.data.ui.gaming?.snake?.['high-score'] ||
!this.patch.data.ui.gaming?.snake?.['high-score'])
) {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
message: 'Saving High Score...',
})
await loader.present()
try {
await this.embassyApi.setDbValue({
pointer: '/gaming',
value: { snake: { 'high-score': ret.data.highScore } },
})
} catch (e) {
this.errToast.present(e)
} finally {
this.loadingCtrl.dismiss()
}
}
})
modal.present()
}
// should wipe cache independant of actual BE logout
private async logout() {
this.embassyApi.logout({})

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { SnakePage } from './snake.page'
@NgModule({
declarations: [SnakePage],
imports: [CommonModule, IonicModule],
exports: [SnakePage],
})
export class SnakePageModule {}

View File

@@ -0,0 +1,23 @@
<ion-header>
<ion-toolbar>
<ion-title>Play Snek!</ion-title>
<ion-title slot="end">Score: {{ score }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="canvas-center" style="width: 100%">
<canvas id="game"> </canvas>
</div>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-title slot="start">High Score: {{ highScore }}</ion-title>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button (click)="dismiss()" class="enter-click"
>Byeeeeeee!</ion-button
>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,6 @@
.canvas-center {
padding-top: 20px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,260 @@
import { Component, HostListener } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { pauseFor } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'snake',
templateUrl: './snake.page.html',
styleUrls: ['./snake.page.scss'],
})
export class SnakePage {
speed = 40
width = 40
height = 26
grid
startingLength = 4
score = 0
highScore = 0
xDown: number
yDown: number
canvas: HTMLCanvasElement
image: HTMLImageElement
context
snake
bitcoin
moveQueue: String[] = []
constructor(
private readonly modalCtrl: ModalController,
private readonly patch: PatchDbService,
) {}
ngOnInit() {
if (this.patch.data.ui.gaming?.snake?.['high-score']) {
this.highScore = this.patch.data.ui.gaming?.snake?.['high-score']
}
}
async dismiss() {
return this.modalCtrl.dismiss({ highScore: this.highScore })
}
@HostListener('document:keydown', ['$event'])
keyEvent(e: KeyboardEvent) {
this.moveQueue.push(e.key)
}
@HostListener('touchstart', ['$event'])
touchStart(e: TouchEvent) {
this.handleTouchStart(e)
}
@HostListener('touchmove', ['$event'])
touchMove(e: TouchEvent) {
this.handleTouchMove(e)
}
@HostListener('window:resize', ['$event'])
sizeChange(event) {
this.init()
}
ionViewDidEnter() {
this.init()
this.image = new Image()
this.image.onload = () => {
requestAnimationFrame(async () => await this.loop())
}
this.image.src = '../../../../../../assets/img/icons/bitcoin.svg'
}
init() {
this.canvas = document.getElementById('game') as HTMLCanvasElement
this.canvas.style.border = '1px solid #e0e0e0'
this.context = this.canvas.getContext('2d')
const container = document.getElementsByClassName('canvas-center')[0]
this.grid = Math.floor(container.clientWidth / this.width)
this.snake = {
x: this.grid * (Math.floor(this.width / 2) - this.startingLength),
y: this.grid * Math.floor(this.height / 2),
// snake velocity. moves one grid length every frame in either the x or y direction
dx: this.grid,
dy: 0,
// keep track of all grids the snake body occupies
cells: [],
// length of the snake. grows when eating an bitcoin
maxCells: this.startingLength,
}
this.bitcoin = {
x: this.getRandomInt(0, this.width) * this.grid,
y: this.getRandomInt(0, this.height) * this.grid,
}
this.canvas.width = this.grid * this.width
this.canvas.height = this.grid * this.height
this.context.imageSmoothingEnabled = false
}
getTouches(evt: TouchEvent) {
return evt.touches
}
handleTouchStart(evt) {
const firstTouch = this.getTouches(evt)[0]
this.xDown = firstTouch.clientX
this.yDown = firstTouch.clientY
}
handleTouchMove(evt) {
if (!this.xDown || !this.yDown) {
return
}
var xUp = evt.touches[0].clientX
var yUp = evt.touches[0].clientY
var xDiff = this.xDown - xUp
var yDiff = this.yDown - yUp
if (Math.abs(xDiff) > Math.abs(yDiff)) {
/*most significant*/
if (xDiff > 0) {
this.moveQueue.push('ArrowLeft')
} else {
this.moveQueue.push('ArrowRight')
}
} else {
if (yDiff > 0) {
this.moveQueue.push('ArrowUp')
} else {
this.moveQueue.push('ArrowDown')
}
}
/* reset values */
this.xDown = null
this.yDown = null
}
// game loop
async loop() {
await pauseFor(this.speed)
requestAnimationFrame(async () => await this.loop())
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
// move snake by it's velocity
this.snake.x += this.snake.dx
this.snake.y += this.snake.dy
if (this.moveQueue.length) {
const move = this.moveQueue.shift()
// left arrow key
if (move === 'ArrowLeft' && this.snake.dx === 0) {
this.snake.dx = -this.grid
this.snake.dy = 0
}
// up arrow key
else if (move === 'ArrowUp' && this.snake.dy === 0) {
this.snake.dy = -this.grid
this.snake.dx = 0
}
// right arrow key
else if (move === 'ArrowRight' && this.snake.dx === 0) {
this.snake.dx = this.grid
this.snake.dy = 0
}
// down arrow key
else if (move === 'ArrowDown' && this.snake.dy === 0) {
this.snake.dy = this.grid
this.snake.dx = 0
}
}
// edge death
if (
this.snake.x < 0 ||
this.snake.y < 0 ||
this.snake.x >= this.canvas.width ||
this.snake.y >= this.canvas.height
) {
this.death()
}
// keep track of where snake has been. front of the array is always the head
this.snake.cells.unshift({ x: this.snake.x, y: this.snake.y })
// remove cells as we move away from them
if (this.snake.cells.length > this.snake.maxCells) {
this.snake.cells.pop()
}
// draw bitcoin
this.context.fillStyle = '#ff4961'
this.context.drawImage(
this.image,
this.bitcoin.x - 1,
this.bitcoin.y - 1,
this.grid + 2,
this.grid + 2,
)
// draw snake one cell at a time
this.context.fillStyle = '#2fdf75'
const firstCell = this.snake.cells[0]
for (let index = 0; index < this.snake.cells.length; index++) {
const cell = this.snake.cells[index]
// drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is
this.context.fillRect(cell.x, cell.y, this.grid - 1, this.grid - 1)
// snake ate bitcoin
if (cell.x === this.bitcoin.x && cell.y === this.bitcoin.y) {
this.score++
if (this.score > this.highScore) this.highScore = this.score
this.snake.maxCells++
this.bitcoin.x = this.getRandomInt(0, this.width) * this.grid
this.bitcoin.y = this.getRandomInt(0, this.height) * this.grid
}
if (index > 0) {
// check collision with all cells after this one (modified bubble sort)
// snake occupies same space as a body part. reset game
if (
firstCell.x === this.snake.cells[index].x &&
firstCell.y === this.snake.cells[index].y
) {
this.death()
}
}
}
}
death() {
this.snake.x =
this.grid * (Math.floor(this.width / 2) - this.startingLength)
this.snake.y = this.grid * Math.floor(this.height / 2)
this.snake.cells = []
this.snake.maxCells = this.startingLength
this.snake.dx = this.grid
this.snake.dy = 0
this.bitcoin.x = this.getRandomInt(0, 25) * this.grid
this.bitcoin.y = this.getRandomInt(0, 25) * this.grid
this.score = 0
}
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
}

View File

@@ -14,7 +14,7 @@
<ion-icon slot="start" name="add" size="large" color="dark"></ion-icon>
<ion-label>
<ion-text color="dark">
<b>Add alternative marketplace</b>
<b>Add alt marketplace</b>
</ion-text>
</ion-label>
</ion-item>

View File

@@ -291,7 +291,7 @@ function getMarketplaceValueSpec(): ValueSpecObject {
url: {
type: 'string',
name: 'URL',
description: 'The fully-qualified URL of the alternative marketplace.',
description: 'The fully-qualified URL of the alt marketplace.',
nullable: false,
masked: false,
copyable: false,

View File

@@ -6,6 +6,7 @@ import { ServerShowPage } from './server-show.page'
import { FormsModule } from '@angular/forms'
import { TextSpinnerComponentModule } from '@start9labs/shared'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SnakePageModule } from 'src/app/modals/snake/snake.module'
const routes: Routes = [
{
@@ -22,6 +23,7 @@ const routes: Routes = [
RouterModule.forChild(routes),
TextSpinnerComponentModule,
BadgeMenuComponentModule,
SnakePageModule,
],
declarations: [ServerShowPage],
})

View File

@@ -17,6 +17,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
import { exists, isEmptyObject } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { ServerStatus } from 'src/app/services/patch-db/data-model'
import { SnakePage } from 'src/app/modals/snake/snake.page'
@Component({
selector: 'server-show',

View File

@@ -16,6 +16,7 @@ export const mockPatchData: DataModel = {
'ack-welcome': '1.0.0',
marketplace: undefined,
dev: undefined,
gaming: undefined,
},
'server-info': {
id: 'embassy-abcdefgh',

View File

@@ -16,6 +16,13 @@ export interface UIData {
'ack-welcome': string // EOS version
marketplace: UIMarketplaceData
dev: DevData
gaming:
| {
snake: {
'high-score': number
}
}
| undefined
}
export interface UIMarketplaceData {