Initial commit: 2048 game with Knative and Kourier deployment

- Complete 2048 game implementation with responsive design
- Knative Serving manifests for dev/staging/prod environments
- Scale-to-zero configuration with environment-specific settings
- Custom domain mapping for wa.darknex.us subdomains
- GitHub Actions workflows for CI/CD
- Docker container with nginx and health checks
- Setup scripts for Knative and Kourier installation
- GHCR integration for container registry
This commit is contained in:
greg
2025-06-30 20:43:19 -07:00
commit c3b227b7d7
26 changed files with 2244 additions and 0 deletions

82
src/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048 Game - Knative Edition</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<div class="container">
<div class="header">
<h1>2048</h1>
<div class="environment-badge" id="env-badge"></div>
<div class="scores-container">
<div class="score-container">
<div class="score-title">SCORE</div>
<div class="score" id="score">0</div>
</div>
<div class="score-container">
<div class="score-title">BEST</div>
<div class="score" id="best">0</div>
</div>
</div>
</div>
<div class="above-game">
<p class="game-intro">
<strong>HOW TO PLAY:</strong> Use your <strong>arrow keys</strong> to move the tiles.
When two tiles with the same number touch, they <strong>merge into one!</strong>
</p>
<button class="restart-button" id="restart-button">New Game</button>
</div>
<div class="game-container">
<div class="game-message" id="game-message">
<p></p>
<div class="lower">
<button class="keep-playing-button" id="keep-playing-button">Keep going</button>
<button class="retry-button" id="retry-button">Try again</button>
</div>
</div>
<div class="grid-container">
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
<div class="grid-row">
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
<div class="grid-cell"></div>
</div>
</div>
<div class="tile-container" id="tile-container"></div>
</div>
<div class="game-explanation">
<p><strong>Knative Edition:</strong> This game is deployed using Knative Serving with scale-to-zero capabilities on Kubernetes!</p>
<p>Environment: <span id="environment">Production</span></p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

348
src/script.js Normal file
View File

@@ -0,0 +1,348 @@
// 2048 Game JavaScript - Knative Edition
class Game2048 {
constructor() {
this.grid = [];
this.score = 0;
this.best = localStorage.getItem('best2048') || 0;
this.gameWon = false;
this.gameOver = false;
this.keepPlaying = false;
this.init();
this.setupEventListeners();
this.setEnvironment();
}
init() {
this.grid = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
this.score = 0;
this.gameWon = false;
this.gameOver = false;
this.keepPlaying = false;
this.updateScore();
this.addRandomTile();
this.addRandomTile();
this.updateDisplay();
}
setEnvironment() {
const envElement = document.getElementById('environment');
const envBadge = document.getElementById('env-badge');
// Try to detect environment from hostname
const hostname = window.location.hostname;
let environment = 'production';
if (hostname.includes('dev')) {
environment = 'development';
} else if (hostname.includes('staging')) {
environment = 'staging';
}
envElement.textContent = environment.charAt(0).toUpperCase() + environment.slice(1);
envBadge.textContent = environment;
envBadge.className = `environment-badge ${environment}`;
}
setupEventListeners() {
document.addEventListener('keydown', (e) => this.handleKeyPress(e));
document.getElementById('restart-button').addEventListener('click', () => this.restart());
document.getElementById('keep-playing-button').addEventListener('click', () => this.keepPlayingGame());
document.getElementById('retry-button').addEventListener('click', () => this.restart());
// Touch/swipe support for mobile
let startX, startY;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
});
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const diffX = startX - endX;
const diffY = startY - endY;
if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 0) {
this.move('left');
} else {
this.move('right');
}
} else {
if (diffY > 0) {
this.move('up');
} else {
this.move('down');
}
}
});
}
handleKeyPress(e) {
if (this.gameOver && !this.keepPlaying) return;
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this.move('up');
break;
case 'ArrowDown':
e.preventDefault();
this.move('down');
break;
case 'ArrowLeft':
e.preventDefault();
this.move('left');
break;
case 'ArrowRight':
e.preventDefault();
this.move('right');
break;
}
}
move(direction) {
const previousGrid = this.grid.map(row => [...row]);
let moved = false;
switch (direction) {
case 'left':
moved = this.moveLeft();
break;
case 'right':
moved = this.moveRight();
break;
case 'up':
moved = this.moveUp();
break;
case 'down':
moved = this.moveDown();
break;
}
if (moved) {
this.addRandomTile();
this.updateDisplay();
this.checkGameState();
}
}
moveLeft() {
let moved = false;
for (let row = 0; row < 4; row++) {
const newRow = this.slideArray(this.grid[row]);
if (!this.arraysEqual(this.grid[row], newRow)) {
moved = true;
this.grid[row] = newRow;
}
}
return moved;
}
moveRight() {
let moved = false;
for (let row = 0; row < 4; row++) {
const reversed = [...this.grid[row]].reverse();
const newRow = this.slideArray(reversed).reverse();
if (!this.arraysEqual(this.grid[row], newRow)) {
moved = true;
this.grid[row] = newRow;
}
}
return moved;
}
moveUp() {
let moved = false;
for (let col = 0; col < 4; col++) {
const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]];
const newColumn = this.slideArray(column);
if (!this.arraysEqual(column, newColumn)) {
moved = true;
for (let row = 0; row < 4; row++) {
this.grid[row][col] = newColumn[row];
}
}
}
return moved;
}
moveDown() {
let moved = false;
for (let col = 0; col < 4; col++) {
const column = [this.grid[0][col], this.grid[1][col], this.grid[2][col], this.grid[3][col]];
const reversed = [...column].reverse();
const newColumn = this.slideArray(reversed).reverse();
if (!this.arraysEqual(column, newColumn)) {
moved = true;
for (let row = 0; row < 4; row++) {
this.grid[row][col] = newColumn[row];
}
}
}
return moved;
}
slideArray(arr) {
const filtered = arr.filter(val => val !== 0);
const missing = 4 - filtered.length;
const zeros = Array(missing).fill(0);
const newArray = filtered.concat(zeros);
for (let i = 0; i < 3; i++) {
if (newArray[i] !== 0 && newArray[i] === newArray[i + 1]) {
newArray[i] *= 2;
newArray[i + 1] = 0;
this.score += newArray[i];
}
}
const filtered2 = newArray.filter(val => val !== 0);
const missing2 = 4 - filtered2.length;
const zeros2 = Array(missing2).fill(0);
return filtered2.concat(zeros2);
}
arraysEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
addRandomTile() {
const emptyCells = [];
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 0) {
emptyCells.push({row, col});
}
}
}
if (emptyCells.length > 0) {
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
this.grid[randomCell.row][randomCell.col] = Math.random() < 0.9 ? 2 : 4;
}
}
updateDisplay() {
const container = document.getElementById('tile-container');
container.innerHTML = '';
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] !== 0) {
const tile = document.createElement('div');
tile.className = `tile tile-${this.grid[row][col]}`;
tile.textContent = this.grid[row][col];
tile.style.left = `${col * 121.25}px`;
tile.style.top = `${row * 121.25}px`;
if (this.grid[row][col] > 2048) {
tile.className = 'tile tile-super';
}
container.appendChild(tile);
}
}
}
}
updateScore() {
document.getElementById('score').textContent = this.score;
if (this.score > this.best) {
this.best = this.score;
localStorage.setItem('best2048', this.best);
}
document.getElementById('best').textContent = this.best;
}
checkGameState() {
this.updateScore();
// Check for 2048 tile (game won)
if (!this.gameWon && !this.keepPlaying) {
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 2048) {
this.gameWon = true;
this.showMessage('You Win!', 'game-won');
return;
}
}
}
}
// Check for game over
if (this.isGameOver()) {
this.gameOver = true;
this.showMessage('Game Over!', 'game-over');
}
}
isGameOver() {
// Check for empty cells
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (this.grid[row][col] === 0) {
return false;
}
}
}
// Check for possible merges
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const current = this.grid[row][col];
if (
(row < 3 && current === this.grid[row + 1][col]) ||
(col < 3 && current === this.grid[row][col + 1])
) {
return false;
}
}
}
return true;
}
showMessage(text, className) {
const messageElement = document.getElementById('game-message');
messageElement.querySelector('p').textContent = text;
messageElement.className = `game-message ${className}`;
messageElement.style.display = 'block';
}
hideMessage() {
const messageElement = document.getElementById('game-message');
messageElement.style.display = 'none';
}
restart() {
this.hideMessage();
this.init();
}
keepPlayingGame() {
this.hideMessage();
this.keepPlaying = true;
}
}
// Initialize the game when the page loads
document.addEventListener('DOMContentLoaded', () => {
new Game2048();
});

382
src/style.css Normal file
View File

@@ -0,0 +1,382 @@
/* 2048 Game CSS - Knative Edition */
html, body {
margin: 0;
padding: 20px;
background: #faf8ef;
color: #776e65;
font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
body {
margin: 80px 0;
}
.heading {
margin-bottom: 30px;
}
h1.title {
font-size: 80px;
font-weight: bold;
margin: 0;
display: inline-block;
}
.container {
width: 500px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h1 {
color: #776e65;
font-size: 80px;
font-weight: bold;
margin: 0;
}
.environment-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
color: white;
margin-left: 20px;
}
.environment-badge.development {
background: #ff6b6b;
}
.environment-badge.staging {
background: #ffa726;
}
.environment-badge.production {
background: #66bb6a;
}
.scores-container {
display: flex;
gap: 10px;
}
.score-container {
position: relative;
display: inline-block;
background: #bbada0;
padding: 10px 20px;
font-size: 25px;
height: 60px;
line-height: 47px;
font-weight: bold;
border-radius: 3px;
color: white;
text-align: center;
min-width: 80px;
}
.score-title {
position: absolute;
width: 100%;
top: 10px;
left: 0;
text-transform: uppercase;
font-size: 13px;
line-height: 13px;
text-align: center;
color: #eee4da;
}
.score {
font-size: 25px;
}
.above-game {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.game-intro {
line-height: 1.65;
margin: 0;
flex: 1;
margin-right: 20px;
}
.restart-button {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
border: none;
cursor: pointer;
font-size: 18px;
}
.restart-button:hover {
background: #9f8a76;
}
.game-container {
position: relative;
padding: 15px;
cursor: default;
user-select: none;
touch-action: none;
background: #bbada0;
border-radius: 10px;
width: 500px;
height: 500px;
box-sizing: border-box;
}
.game-message {
display: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.73);
z-index: 100;
text-align: center;
border-radius: 10px;
}
.game-message p {
font-size: 60px;
font-weight: bold;
height: 60px;
line-height: 60px;
margin-top: 150px;
}
.game-message .lower {
display: block;
margin-top: 30px;
}
.game-message a {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
margin-left: 9px;
}
.game-won {
background: rgba(237, 194, 46, 0.5);
color: #f9f6f2;
}
.game-won .game-message p {
color: #f9f6f2;
}
.game-over {
background: rgba(238, 228, 218, 0.73);
color: #776e65;
}
.game-over .game-message p {
color: #776e65;
}
.grid-container {
position: absolute;
z-index: 1;
}
.grid-row {
margin-bottom: 15px;
}
.grid-row:last-child {
margin-bottom: 0;
}
.grid-cell {
width: 106.25px;
height: 106.25px;
background: rgba(238, 228, 218, 0.35);
border-radius: 6px;
margin-right: 15px;
float: left;
}
.grid-cell:last-child {
margin-right: 0;
}
.tile-container {
position: absolute;
z-index: 2;
}
.tile {
width: 106.25px;
height: 106.25px;
background: #eee4da;
color: #776e65;
border-radius: 6px;
font-weight: bold;
text-align: center;
vertical-align: middle;
line-height: 106.25px;
font-size: 55px;
position: absolute;
transition: 0.15s ease-in-out;
transform-origin: center center;
}
.tile-2 { background: #eee4da; color: #776e65; }
.tile-4 { background: #ede0c8; color: #776e65; }
.tile-8 { color: #f9f6f2; background: #f2b179; }
.tile-16 { color: #f9f6f2; background: #f59563; }
.tile-32 { color: #f9f6f2; background: #f67c5f; }
.tile-64 { color: #f9f6f2; background: #f65e3b; }
.tile-128 { color: #f9f6f2; background: #edcf72; font-size: 45px; }
.tile-256 { color: #f9f6f2; background: #edcc61; font-size: 45px; }
.tile-512 { color: #f9f6f2; background: #edc850; font-size: 45px; }
.tile-1024 { color: #f9f6f2; background: #edc53f; font-size: 35px; }
.tile-2048 { color: #f9f6f2; background: #edc22e; font-size: 35px; }
.tile-super { color: #f9f6f2; background: #3c3a32; font-size: 30px; }
.tile-new {
animation: appear 200ms ease-in-out;
animation-fill-mode: backwards;
}
.tile-merged {
z-index: 20;
animation: pop 200ms ease-in-out;
animation-fill-mode: backwards;
}
@keyframes appear {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes pop {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.game-explanation {
margin-top: 30px;
text-align: center;
color: #776e65;
}
.game-explanation p {
margin: 10px 0;
}
.keep-playing-button, .retry-button {
display: inline-block;
background: #8f7a66;
border-radius: 3px;
padding: 0 20px;
text-decoration: none;
color: #f9f6f2;
height: 40px;
line-height: 42px;
border: none;
cursor: pointer;
font-size: 18px;
margin: 0 5px;
}
.keep-playing-button:hover, .retry-button:hover {
background: #9f8a76;
}
/* Responsive design */
@media screen and (max-width: 520px) {
.container {
width: 280px;
margin: 0 auto;
}
.header h1 {
font-size: 50px;
}
.scores-container {
flex-direction: column;
gap: 5px;
}
.above-game {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.game-container {
width: 280px;
height: 280px;
padding: 10px;
}
.grid-cell {
width: 60px;
height: 60px;
margin-right: 10px;
margin-bottom: 10px;
}
.tile {
width: 60px;
height: 60px;
line-height: 60px;
font-size: 35px;
}
.tile-128, .tile-256, .tile-512 {
font-size: 25px;
}
.tile-1024, .tile-2048 {
font-size: 20px;
}
.tile-super {
font-size: 18px;
}
}