mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
Feature/snake (#1256)
* snake game Co-authored-by: Drew Ansbacher <drew@start9labs.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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({})
|
||||
|
||||
11
frontend/projects/ui/src/app/modals/snake/snake.module.ts
Normal file
11
frontend/projects/ui/src/app/modals/snake/snake.module.ts
Normal 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 {}
|
||||
23
frontend/projects/ui/src/app/modals/snake/snake.page.html
Normal file
23
frontend/projects/ui/src/app/modals/snake/snake.page.html
Normal 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>
|
||||
@@ -0,0 +1,6 @@
|
||||
.canvas-center {
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
260
frontend/projects/ui/src/app/modals/snake/snake.page.ts
Normal file
260
frontend/projects/ui/src/app/modals/snake/snake.page.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -16,6 +16,7 @@ export const mockPatchData: DataModel = {
|
||||
'ack-welcome': '1.0.0',
|
||||
marketplace: undefined,
|
||||
dev: undefined,
|
||||
gaming: undefined,
|
||||
},
|
||||
'server-info': {
|
||||
id: 'embassy-abcdefgh',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user