-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Portalicious: Easter egg Snake (#6399)
* Draw snake * Basic movement * Eat food * Intersection detection * Score dialog * translations * cleanup * chore snake facts (#6401) * chore snake facts * Snake translations * Update interfaces/Portalicious/src/app/pages/snake/game/snake.component.ts Co-authored-by: Peter Smallenbroek <PeterSmallenbroek@users.noreply.github.com> * Update interfaces/Portalicious/src/app/pages/snake/game/snake.component.ts Co-authored-by: Peter Smallenbroek <PeterSmallenbroek@users.noreply.github.com> * Update interfaces/Portalicious/src/app/pages/snake/game/snake.component.ts Co-authored-by: Peter Smallenbroek <PeterSmallenbroek@users.noreply.github.com> * Update interfaces/Portalicious/src/app/pages/snake/game/snake.component.html Co-authored-by: Peter Smallenbroek <PeterSmallenbroek@users.noreply.github.com> --------- Co-authored-by: Ruben <vandervalk@geoit.nl> Co-authored-by: Peter Smallenbroek <PeterSmallenbroek@users.noreply.github.com> --------- Co-authored-by: RubenGeo <34537157+RubenGeo@users.noreply.github.com> Co-authored-by: Ruben <vandervalk@geoit.nl>
- Loading branch information
1 parent
98bbcb9
commit 2277311
Showing
7 changed files
with
358 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
interfaces/Portalicious/src/app/pages/snake/game/snake.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<p-dialog | ||
header="Game over!" | ||
i18n-header | ||
[modal]="true" | ||
[dismissableMask]="true" | ||
[closeOnEscape]="false" | ||
[(visible)]="isGameOver" | ||
[closable]="false" | ||
> | ||
<b | ||
i18n | ||
class="mb-4 block" | ||
>Your score is {{ score() }}. | ||
</b> | ||
<div class="flex justify-end gap-2"> | ||
<p-button | ||
label="Okay" | ||
i18n-label | ||
(click)="initialize()" | ||
/> | ||
</div> | ||
</p-dialog> | ||
|
||
<p-dialog | ||
header="Let's start!" | ||
i18n-header | ||
[modal]="true" | ||
[dismissableMask]="true" | ||
[closeOnEscape]="false" | ||
[visible]="!isGameStarted()" | ||
[closable]="false" | ||
> | ||
<b | ||
i18n | ||
class="mb-4 block" | ||
>Use the arrow keys or W A S D to play the game</b | ||
> | ||
<div class="flex justify-end gap-2"> | ||
<p-button | ||
label="Start!" | ||
i18n-label | ||
(click)="startButtonClick()" | ||
/> | ||
</div> | ||
</p-dialog> | ||
|
||
<div | ||
#board | ||
id="board" | ||
class="border-1 grid aspect-square h-full w-full border-black bg-green-500 bg-opacity-35" | ||
style=" | ||
grid-template-rows: repeat(21, 1fr); | ||
grid-template-columns: repeat(21, 1fr); | ||
" | ||
></div> | ||
<!-- Random Fact Section --> | ||
<div class="mt-4 rounded-lg border bg-white p-4 shadow-md"> | ||
<h3 | ||
i18n | ||
class="mb-2 text-lg font-semibold" | ||
> | ||
Did you know? | ||
</h3> | ||
<p class="text-gray-700">{{ random121Fact() }}</p> | ||
</div> |
220 changes: 220 additions & 0 deletions
220
interfaces/Portalicious/src/app/pages/snake/game/snake.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
import { | ||
AfterViewInit, | ||
ChangeDetectionStrategy, | ||
ChangeDetectorRef, | ||
Component, | ||
ElementRef, | ||
HostListener, | ||
signal, | ||
ViewChild, | ||
} from '@angular/core'; | ||
|
||
import { ButtonModule } from 'primeng/button'; | ||
import { DialogModule } from 'primeng/dialog'; | ||
|
||
interface Vector { | ||
x: number; | ||
y: number; | ||
} | ||
|
||
@Component({ | ||
selector: 'app-snake', | ||
imports: [ButtonModule, DialogModule], | ||
templateUrl: './snake.component.html', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class SnakeComponent implements AfterViewInit { | ||
@HostListener('document:keydown', ['$event']) | ||
handleKeyboardEvent(event: KeyboardEvent) { | ||
switch (event.key) { | ||
case 'w': | ||
case 'ArrowUp': | ||
if (this.lastInputDirection.y !== 0) break; | ||
this.inputDirection = { x: 0, y: -1 }; | ||
break; | ||
case 's': | ||
case 'ArrowDown': | ||
if (this.lastInputDirection.y !== 0) break; | ||
this.inputDirection = { x: 0, y: 1 }; | ||
break; | ||
case 'a': | ||
case 'ArrowLeft': | ||
if (this.lastInputDirection.x !== 0) break; | ||
this.inputDirection = { x: -1, y: 0 }; | ||
break; | ||
case 'd': | ||
case 'ArrowRight': | ||
if (this.lastInputDirection.x !== 0) break; | ||
this.inputDirection = { x: 1, y: 0 }; | ||
break; | ||
} | ||
} | ||
|
||
@ViewChild('board') board: ElementRef<HTMLDivElement>; | ||
constructor(private changeDetectorRef: ChangeDetectorRef) {} | ||
|
||
public isGameStarted = signal(false); | ||
public isGameOver = signal(false); | ||
public score = signal(0); | ||
public random121Fact = signal(''); | ||
|
||
private lastRenderTime = 0; | ||
private inputDirection: Vector; | ||
private lastInputDirection: Vector; | ||
private snakeBody: Vector[]; | ||
private foodPosition: Vector; | ||
private SNAKE_SPEED = 6; | ||
private EXPANSION_RATE = 1; | ||
|
||
ngAfterViewInit(): void { | ||
this.initialize(); | ||
} | ||
|
||
startButtonClick() { | ||
this.isGameStarted.set(true); | ||
this.inputDirection = { x: 0, y: -1 }; | ||
this.lastInputDirection = { x: 0, y: -1 }; | ||
window.requestAnimationFrame(this.gameLoop.bind(this)); | ||
} | ||
|
||
private gameLoop(currentTime: number) { | ||
if (!this.isGameStarted() || this.isGameOver()) return; | ||
window.requestAnimationFrame(this.gameLoop.bind(this)); | ||
const secondsSinceLastRender = (currentTime - this.lastRenderTime) / 1000; | ||
if (secondsSinceLastRender < 1 / this.SNAKE_SPEED) return; | ||
|
||
this.lastRenderTime = currentTime; | ||
|
||
this.updateSnake(); | ||
this.updateFood(); | ||
this.drawSnake(); | ||
this.drawFood(); | ||
this.checkGameOver(); | ||
} | ||
|
||
initialize() { | ||
this.isGameStarted.set(false); | ||
this.isGameOver.set(false); | ||
this.inputDirection = { x: 0, y: 0 }; | ||
this.lastInputDirection = { x: 0, y: 0 }; | ||
this.snakeBody = [ | ||
{ x: 11, y: 11 }, | ||
{ x: 11, y: 12 }, | ||
{ x: 11, y: 13 }, | ||
]; | ||
this.foodPosition = this.getRandomPositionOnGrid(); | ||
|
||
this.drawSnake(); | ||
this.drawFood(); | ||
this.changeDetectorRef.detectChanges(); | ||
this.showRandom121Fact(); | ||
} | ||
|
||
private showRandom121Fact() { | ||
const random121Facts = [ | ||
'The number 121 is the sum of the first 11 prime numberss', | ||
'121 is the 11th number in the Fibonacci sequence', | ||
'121 is the 15th number in the Pell sequence', | ||
'121 is a centered octagonal number', | ||
'121 is easy', | ||
'121 is safe', | ||
'121 is fast', | ||
'Did you know that you can share your registration table filter by copying the URL?', | ||
'Have you ever typed "Henry Dunant" in the search bar?', | ||
'By recording all data entries and changes, 121 enhances accountability and security, making auditing easier through a privacy-by-design system.', | ||
]; | ||
this.random121Fact.set( | ||
random121Facts[Math.floor(Math.random() * random121Facts.length)], | ||
); | ||
} | ||
|
||
private checkGameOver() { | ||
if ( | ||
this.isSnakeOutsideBoard() || | ||
this.isSnakeIntersecting({ | ||
position: this.snakeBody[0], | ||
ignoreHead: true, | ||
}) | ||
) { | ||
this.score.set(this.snakeBody.length - 3); | ||
this.isGameOver.set(true); | ||
} | ||
} | ||
|
||
private updateFood() { | ||
if (this.isSnakeIntersecting({ position: this.foodPosition })) { | ||
this.expandSnake(); | ||
this.foodPosition = this.getRandomPositionOnGrid(); | ||
this.showRandom121Fact(); | ||
} | ||
} | ||
|
||
private drawFood() { | ||
const foodElement = document.createElement('div'); | ||
foodElement.style.gridRowStart = this.foodPosition.y.toString(); | ||
foodElement.style.gridColumnStart = this.foodPosition.x.toString(); | ||
foodElement.classList.add('bg-red-500', 'border-red-700', 'border-2'); | ||
this.board.nativeElement.appendChild(foodElement); | ||
} | ||
|
||
private updateSnake() { | ||
const inputDirection = this.getInputDirection(); | ||
for (let i = this.snakeBody.length - 2; i >= 0; i--) { | ||
this.snakeBody[i + 1] = { ...this.snakeBody[i] }; | ||
} | ||
|
||
this.snakeBody[0].x += inputDirection.x; | ||
this.snakeBody[0].y += inputDirection.y; | ||
} | ||
|
||
private drawSnake() { | ||
this.board.nativeElement.innerHTML = ''; | ||
this.snakeBody.forEach((segment) => { | ||
const snakeElement = document.createElement('div'); | ||
snakeElement.style.gridRowStart = segment.y.toString(); | ||
snakeElement.style.gridColumnStart = segment.x.toString(); | ||
snakeElement.classList.add('bg-grey-500', 'border-black', 'border'); | ||
this.board.nativeElement.appendChild(snakeElement); | ||
}); | ||
} | ||
|
||
private isSnakeIntersecting({ | ||
position, | ||
ignoreHead = false, | ||
}: { | ||
position: { x: number; y: number }; | ||
ignoreHead?: boolean; | ||
}): boolean { | ||
return this.snakeBody.some((segment, index) => { | ||
if (ignoreHead && index === 0) return false; | ||
return segment.x === position.x && segment.y === position.y; | ||
}); | ||
} | ||
|
||
private expandSnake() { | ||
for (let i = 0; i < this.EXPANSION_RATE; i++) { | ||
this.snakeBody.push({ ...this.snakeBody[this.snakeBody.length - 1] }); | ||
} | ||
} | ||
|
||
private isSnakeOutsideBoard() { | ||
return ( | ||
this.snakeBody[0].x < 1 || | ||
this.snakeBody[0].x > 21 || | ||
this.snakeBody[0].y < 1 || | ||
this.snakeBody[0].y > 21 | ||
); | ||
} | ||
|
||
private getInputDirection() { | ||
this.lastInputDirection = this.inputDirection; | ||
return this.inputDirection; | ||
} | ||
|
||
private getRandomPositionOnGrid() { | ||
return { | ||
x: Math.floor(Math.random() * 21) + 1, | ||
y: Math.floor(Math.random() * 21) + 1, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<app-page-layout> | ||
<div class="mx-auto max-w-[47rem]"> | ||
<app-snake /> | ||
</div> | ||
</app-page-layout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { ChangeDetectionStrategy, Component } from '@angular/core'; | ||
|
||
import { PageLayoutComponent } from '~/components/page-layout/page-layout.component'; | ||
import { SnakeComponent } from '~/pages/snake/game/snake.component'; | ||
|
||
@Component({ | ||
selector: 'app-snake-page', | ||
imports: [PageLayoutComponent, SnakeComponent], | ||
templateUrl: './snake.page.html', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class SnakePageComponent {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters