diff --git a/frontend/assets/img/icons/bitcoin.svg b/frontend/assets/img/icons/bitcoin.svg
new file mode 100644
index 000000000..ca5d37fc1
--- /dev/null
+++ b/frontend/assets/img/icons/bitcoin.svg
@@ -0,0 +1,95 @@
+
+
diff --git a/frontend/assets/img/icons/snek.png b/frontend/assets/img/icons/snek.png
new file mode 100644
index 000000000..26ed2bea5
Binary files /dev/null and b/frontend/assets/img/icons/snek.png differ
diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html
index 59bb22384..dfdc85b72 100644
--- a/frontend/projects/ui/src/app/app.component.html
+++ b/frontend/projects/ui/src/app/app.component.html
@@ -62,6 +62,17 @@
+
Score: {{ score }}
+
+
+
+
+
+
+
+
+
+
+
+ High Score: {{ highScore }}
+
+ Byeeeeeee!
+
+
+
diff --git a/frontend/projects/ui/src/app/modals/snake/snake.page.scss b/frontend/projects/ui/src/app/modals/snake/snake.page.scss
new file mode 100644
index 000000000..c07d3a2b7
--- /dev/null
+++ b/frontend/projects/ui/src/app/modals/snake/snake.page.scss
@@ -0,0 +1,6 @@
+.canvas-center {
+ padding-top: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/frontend/projects/ui/src/app/modals/snake/snake.page.ts b/frontend/projects/ui/src/app/modals/snake/snake.page.ts
new file mode 100644
index 000000000..b997bf366
--- /dev/null
+++ b/frontend/projects/ui/src/app/modals/snake/snake.page.ts
@@ -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
+ }
+}
diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html
index 5f69f031c..744c10c40 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html
+++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html
@@ -14,7 +14,7 @@
- Add alternative marketplace
+ Add alt marketplace
diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
index a940cda31..aef0b4831 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
@@ -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,
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts
index 2efdac950..2ec7b74d4 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts
@@ -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],
})
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts
index 822bf7e1e..866d3c206 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts
@@ -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',
diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts
index 76cf2a969..02e039e07 100644
--- a/frontend/projects/ui/src/app/services/api/mock-patch.ts
+++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts
@@ -16,6 +16,7 @@ export const mockPatchData: DataModel = {
'ack-welcome': '1.0.0',
marketplace: undefined,
dev: undefined,
+ gaming: undefined,
},
'server-info': {
id: 'embassy-abcdefgh',
diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
index bb8c8ddda..e661f32f6 100644
--- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts
+++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
@@ -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 {