Compare commits
3 Commits
3a96f71760
...
8a5c015e58
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5c015e58 | ||
|
|
e2e7095b7d | ||
|
|
eb1f14d19c |
501
dstetris/raw/index.html
Normal file
501
dstetris/raw/index.html
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div>Status: <span id="connectionStatus" class="connection-status">Connecting...</span></div>
|
||||||
|
<div>Game: <span id="gameStatus">Not Started</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
<button onclick="location.reload()">Refresh</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<p>Level Reached: <span id="finalLevel">1</span></p>
|
||||||
|
<p>Lines Cleared: <span id="finalLines">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering (only first rotation needed for display)
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current piece shapes with all rotations
|
||||||
|
const currentPieceShapes = {
|
||||||
|
1: [ // I piece
|
||||||
|
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
|
||||||
|
[[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]
|
||||||
|
],
|
||||||
|
2: [ // O piece
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
3: [ // T piece
|
||||||
|
[[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
4: [ // S piece
|
||||||
|
[[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,1,0],[0,0,1,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[0,1,1,0],[1,1,0,0],[0,0,0,0]],
|
||||||
|
[[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
5: [ // Z piece
|
||||||
|
[[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,0,0],[0,1,1,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[1,1,0,0],[1,0,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
6: [ // J piece
|
||||||
|
[[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[0,0,1,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[1,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
7: [ // L piece
|
||||||
|
[[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[1,0,0,0],[0,0,0,0]],
|
||||||
|
[[1,1,0,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
|
||||||
|
eventSource.onopen = function() {
|
||||||
|
updateConnectionStatus('Connected', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
updateConnectionStatus('Disconnected', false);
|
||||||
|
setTimeout(connectEventSource, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus(status, connected) {
|
||||||
|
const statusElement = document.getElementById('connectionStatus');
|
||||||
|
statusElement.textContent = status;
|
||||||
|
statusElement.className = connected ? 'connection-status' : 'connection-status disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = currentPieceShapes[piece.type][piece.rotation];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
nextCtx.strokeStyle = '#666';
|
||||||
|
nextCtx.strokeRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = gameState.can_hold ? COLORS[pieceType] : '#666';
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
holdCtx.strokeStyle = '#444';
|
||||||
|
holdCtx.strokeRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
// Update game status
|
||||||
|
let gameStatus = 'Not Started';
|
||||||
|
if (gameState.game_over) {
|
||||||
|
gameStatus = 'Game Over';
|
||||||
|
} else if (gameState.game_running) {
|
||||||
|
gameStatus = 'Playing';
|
||||||
|
}
|
||||||
|
document.getElementById('gameStatus').textContent = gameStatus;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('finalLevel').textContent = gameState.level;
|
||||||
|
document.getElementById('finalLines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
case 'KeyR':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent context menu on right click
|
||||||
|
document.addEventListener('contextmenu', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize connection when page loads
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
connectEventSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle page visibility changes
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectEventSource();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
464
dstetris/raw/main.go
Normal file
464
dstetris/raw/main.go
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
fmt.Println("Open http://localhost:8080 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
53
dstetris/specs.txt
Normal file
53
dstetris/specs.txt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
Here's the simplified technical information from the Tetris guidelines:
|
||||||
|
|
||||||
|
**Playfield & Display**
|
||||||
|
- 10×40 grid (rows above 20 are hidden, appearing as 10×20)
|
||||||
|
- Ghost piece enabled by default
|
||||||
|
- 1-6 next pieces shown (6 recommended)
|
||||||
|
|
||||||
|
**Tetrimino Colors**
|
||||||
|
- I: Cyan
|
||||||
|
- O: Yellow
|
||||||
|
- T: Purple
|
||||||
|
- S: Green
|
||||||
|
- Z: Red
|
||||||
|
- J: Blue
|
||||||
|
- L: Orange
|
||||||
|
|
||||||
|
**Spawn Rules**
|
||||||
|
- I and O spawn in middle columns
|
||||||
|
- J, L, T, S, Z spawn in left-middle columns
|
||||||
|
- All spawn horizontally at row 21 (I) or 21/22 (others)
|
||||||
|
- Drop one space immediately if path is clear
|
||||||
|
|
||||||
|
**Controls (Standard)**
|
||||||
|
- Hard drop: Up arrow/Space
|
||||||
|
- Soft drop: Down arrow
|
||||||
|
- Move: Left/Right arrows
|
||||||
|
- Rotate clockwise: X
|
||||||
|
- Rotate counterclockwise: Z/Ctrl
|
||||||
|
- Hold: Shift/C
|
||||||
|
|
||||||
|
**Core Mechanics**
|
||||||
|
- Super Rotation System (SRS)
|
||||||
|
- 7-bag random generator
|
||||||
|
- Hold piece function (default enabled)
|
||||||
|
- Half-second lock delay
|
||||||
|
- 15 moves/rotations before lock (multiplayer/arcade)
|
||||||
|
|
||||||
|
**Scoring & Progression**
|
||||||
|
- Level up by clearing lines or T-spins
|
||||||
|
- Variable goal: 5× level number lines needed
|
||||||
|
- Line values: Single=1, Double=3, Triple=5, Tetris=8
|
||||||
|
- T-spin recognition (3-corner rule)
|
||||||
|
- Back-to-back bonus chains
|
||||||
|
|
||||||
|
**Game Modes**
|
||||||
|
- Marathon: 15 levels
|
||||||
|
- Sprint: 40 lines
|
||||||
|
- Ultra: 2-3 minutes timed
|
||||||
|
|
||||||
|
**Top Out Conditions**
|
||||||
|
- Block out: piece spawns overlapping existing blocks
|
||||||
|
- Lock out: piece locks completely above visible area
|
||||||
|
|
||||||
841
dstetris/temp/tetris_main (1).go
Normal file
841
dstetris/temp/tetris_main (1).go
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", serveHTML)
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
fmt.Fprint(w, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
setTimeout(connectEventSource, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = shapes[piece.type];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = COLORS[pieceType];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score;
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
connectEventSource();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
842
dstetris/temp/tetris_main.go
Normal file
842
dstetris/temp/tetris_main.go
Normal file
@ -0,0 +1,842 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", serveHTML)
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
fmt.Fprint(w, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
setTimeout(connectEventSource, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = shapes[piece.type];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = COLORS[pieceType];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score;
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
connectEventSource();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
66
dstetris/v1/frontend/index.html
Normal file
66
dstetris/v1/frontend/index.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div>Status: <span id="connectionStatus" class="connection-status">Connecting...</span></div>
|
||||||
|
<div>Game: <span id="gameStatus">Not Started</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
<button onclick="location.reload()">Refresh</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<p>Level Reached: <span id="finalLevel">1</span></p>
|
||||||
|
<p>Lines Cleared: <span id="finalLines">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
316
dstetris/v1/frontend/main.js
Normal file
316
dstetris/v1/frontend/main.js
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering (only first rotation needed for display)
|
||||||
|
const shapes = {
|
||||||
|
1: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I
|
||||||
|
2: [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // O
|
||||||
|
3: [[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // T
|
||||||
|
4: [[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // S
|
||||||
|
5: [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // Z
|
||||||
|
6: [[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // J
|
||||||
|
7: [[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current piece shapes with all rotations
|
||||||
|
const currentPieceShapes = {
|
||||||
|
1: [ // I piece
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]],
|
||||||
|
[[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]]
|
||||||
|
],
|
||||||
|
2: [ // O piece
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
3: [ // T piece
|
||||||
|
[[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
4: [ // S piece
|
||||||
|
[[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
5: [ // Z piece
|
||||||
|
[[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
6: [ // J piece
|
||||||
|
[[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
7: [ // L piece
|
||||||
|
[[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
|
||||||
|
eventSource.onopen = function () {
|
||||||
|
updateConnectionStatus('Connected', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function (event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
updateConnectionStatus('Disconnected', false);
|
||||||
|
setTimeout(connectEventSource, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus(status, connected) {
|
||||||
|
const statusElement = document.getElementById('connectionStatus');
|
||||||
|
statusElement.textContent = status;
|
||||||
|
statusElement.className = connected ? 'connection-status' : 'connection-status disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = currentPieceShapes[piece.type][piece.rotation];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
nextCtx.strokeStyle = '#666';
|
||||||
|
nextCtx.strokeRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = gameState.can_hold ? COLORS[pieceType] : '#666';
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
holdCtx.strokeStyle = '#444';
|
||||||
|
holdCtx.strokeRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
// Update game status
|
||||||
|
let gameStatus = 'Not Started';
|
||||||
|
if (gameState.game_over) {
|
||||||
|
gameStatus = 'Game Over';
|
||||||
|
} else if (gameState.game_running) {
|
||||||
|
gameStatus = 'Playing';
|
||||||
|
}
|
||||||
|
document.getElementById('gameStatus').textContent = gameStatus;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('finalLevel').textContent = gameState.level;
|
||||||
|
document.getElementById('finalLines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
case 'KeyR':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent context menu on right click
|
||||||
|
document.addEventListener('contextmenu', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize connection when page loads
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
connectEventSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle page visibility changes
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectEventSource();
|
||||||
|
}
|
||||||
|
});
|
||||||
119
dstetris/v1/frontend/style.css
Normal file
119
dstetris/v1/frontend/style.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas,
|
||||||
|
.hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
3
dstetris/v1/go.mod
Normal file
3
dstetris/v1/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module tetris
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
501
dstetris/v1/main.go
Normal file
501
dstetris/v1/main.go
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Teris rules https://tetris.fandom.com/wiki/Tetris_Guideline
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, err := json.Marshal(g.state)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marshaling game state: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
// Non-blocking broadcast to all clients
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
// Message sent successfully
|
||||||
|
default:
|
||||||
|
// Channel full or client blocked, skip this client
|
||||||
|
log.Printf("Dropping message for slow client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
//go:embed frontend/*
|
||||||
|
var frontendFS embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
// http.Handle("/", http.FileServer(http.Dir("./frontend")))
|
||||||
|
|
||||||
|
// Create a sub-filesystem to serve files from the 'frontend' directory.
|
||||||
|
fs, err := fs.Sub(frontendFS, "frontend")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files from the embedded filesystem
|
||||||
|
http.Handle("/", http.FileServer(http.FS(fs)))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :4444")
|
||||||
|
fmt.Println("Open http://localhost:4444 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":4444", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Critical SSE headers for nginx compatibility
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := make(chan string, 10) // Buffered channel
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Heartbeat to keep connection alive
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
flusher.Flush()
|
||||||
|
case <-ticker.C:
|
||||||
|
// Send comment as heartbeat
|
||||||
|
fmt.Fprintf(w, ": heartbeat\n\n")
|
||||||
|
flusher.Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
BIN
dstetris/v1/tetris
Executable file
BIN
dstetris/v1/tetris
Executable file
Binary file not shown.
486
dstetris/v1/tetris_backend (1).go.txt
Normal file
486
dstetris/v1/tetris_backend (1).go.txt
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, err := json.Marshal(g.state)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marshaling game state: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
// Non-blocking broadcast to all clients
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
// Message sent successfully
|
||||||
|
default:
|
||||||
|
// Channel full or client blocked, skip this client
|
||||||
|
log.Printf("Dropping message for slow client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
fmt.Println("Open http://localhost:8080 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Critical SSE headers for nginx compatibility
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := make(chan string, 10) // Buffered channel
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Heartbeat to keep connection alive
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
flusher.Flush()
|
||||||
|
case <-ticker.C:
|
||||||
|
// Send comment as heartbeat
|
||||||
|
fmt.Fprintf(w, ": heartbeat\n\n")
|
||||||
|
flusher.Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
3
lcars/.log
Normal file
3
lcars/.log
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
2025/09/30 11:18:25 Failed to create internal StateDB: SQL logic error: near ")": syntax error (1)
|
||||||
|
2025/09/30 11:20:30 Failed to create internal StateDB: SQL logic error: near ")": syntax error (1)
|
||||||
|
client_max_body_size 100m;
|
||||||
14
lcars/embed/create_state_db.sql
Normal file
14
lcars/embed/create_state_db.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE ship_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
subsystem TEXT NOT NULL,
|
||||||
|
severity TEXT CHECK(severity IN ('CRITICAL', 'ALERT', 'WARNING', 'NOTICE', 'INFO')) NOT NULL DEFAULT 'INFO',
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE crew_member (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
rank TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
95
lcars/embed/crew.json
Normal file
95
lcars/embed/crew.json
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
[
|
||||||
|
"Ensign Beckett Mariner",
|
||||||
|
"Ensign Brad Boimler",
|
||||||
|
"Captain Carol Freeman",
|
||||||
|
"Commander Jack Ransom",
|
||||||
|
"Ensign Barsa Orsino",
|
||||||
|
"Lieutenant Commander D'Vana Tendi",
|
||||||
|
"Lieutenant Commander Sam Rutherford",
|
||||||
|
"Lieutenant Commander Shaxs",
|
||||||
|
"Ensign Liora Vance",
|
||||||
|
"Ensign Rylan Sato",
|
||||||
|
"Lieutenant Jarek Torin",
|
||||||
|
"Lieutenant Kira Dallin",
|
||||||
|
"Lieutenant T'Lara Venn",
|
||||||
|
"Lieutenant Shonnie Velar",
|
||||||
|
"Lieutenant Commander Aric Thorne",
|
||||||
|
"Lieutenant Commander Selene Marvik",
|
||||||
|
"Lieutenant Commander Jovan Kreel",
|
||||||
|
"Lieutenant Orin Kallis",
|
||||||
|
"Ensign Mira Talon",
|
||||||
|
"Ensign Fynn Darvik",
|
||||||
|
"Lieutenant Commander Elara Voss",
|
||||||
|
"Lieutenant Zev Ralyn",
|
||||||
|
"Ensign Daxia Morn",
|
||||||
|
"Lieutenant Varek Solis",
|
||||||
|
"Ensign Tylen Kael",
|
||||||
|
"Lieutenant Commander Nira Falco",
|
||||||
|
"Lieutenant Kael Dorran",
|
||||||
|
"Ensign Saren Vale",
|
||||||
|
"Ensign Tova Lin",
|
||||||
|
"Lieutenant Commander Ryn Talor",
|
||||||
|
"Lieutenant Draven Korr",
|
||||||
|
"Ensign Lyra Kenning",
|
||||||
|
"Ensign Joren Pax",
|
||||||
|
"Lieutenant Commander Calix Arden",
|
||||||
|
"Lieutenant Selan Vey",
|
||||||
|
"Ensign Aricel Taren",
|
||||||
|
"Ensign Velin Daro",
|
||||||
|
"Lieutenant Caris Vennor",
|
||||||
|
"Lieutenant Kellen Dray",
|
||||||
|
"Lieutenant Risa Talven",
|
||||||
|
"Lieutenant Commander Thalen Voss",
|
||||||
|
"Lieutenant Commander Sariah Quell",
|
||||||
|
"Ensign Orin Talvik",
|
||||||
|
"Ensign Lyric Selden",
|
||||||
|
"Lieutenant Commander Varen Korr",
|
||||||
|
"Lieutenant Elara Vynn",
|
||||||
|
"Ensign Jax Talmar",
|
||||||
|
"Lieutenant Commander Neris Vay",
|
||||||
|
"Lieutenant Draven Solis",
|
||||||
|
"Ensign Tavia Korlen",
|
||||||
|
"Ensign Ryn Paxil",
|
||||||
|
"Lieutenant Commander Kira Dalen",
|
||||||
|
"Lieutenant Zev Ardin",
|
||||||
|
"Ensign Lyra Taven",
|
||||||
|
"Ensign Fynn Velar",
|
||||||
|
"Lieutenant Commander Calen Rhos",
|
||||||
|
"Lieutenant Selan Vaylen",
|
||||||
|
"Ensign Aricel Dorran",
|
||||||
|
"Ensign Tylen Korr",
|
||||||
|
"Lieutenant Commander Nira Talos",
|
||||||
|
"Lieutenant Kael Venn",
|
||||||
|
"Ensign Saren Daro",
|
||||||
|
"Ensign Tova Vennor",
|
||||||
|
"Lieutenant Commander Ryn Arden",
|
||||||
|
"Lieutenant Draven Voss",
|
||||||
|
"Ensign Lyra Talin",
|
||||||
|
"Ensign Joren Vay",
|
||||||
|
"Lieutenant Commander Calix Talven",
|
||||||
|
"Lieutenant Selan Korr",
|
||||||
|
"Ensign Aricel Vynn",
|
||||||
|
"Ensign Velin Talor",
|
||||||
|
"Lieutenant Caris Vaylen",
|
||||||
|
"Lieutenant Kellen Rhos",
|
||||||
|
"Lieutenant Risa Vennor",
|
||||||
|
"Lieutenant Commander Thalen Daro",
|
||||||
|
"Lieutenant Commander Sariah Voss",
|
||||||
|
"Ensign Orin Vaylen",
|
||||||
|
"Ensign Lyric Talven",
|
||||||
|
"Lieutenant Commander Varen Talos",
|
||||||
|
"Lieutenant Elara Solis",
|
||||||
|
"Ensign Jax Vennor",
|
||||||
|
"Lieutenant Commander Neris Talor",
|
||||||
|
"Lieutenant Draven Vaylen",
|
||||||
|
"Ensign Tavia Dorran",
|
||||||
|
"Ensign Ryn Voss",
|
||||||
|
"Lieutenant Commander Kira Arden",
|
||||||
|
"Lieutenant Zev Vay",
|
||||||
|
"Ensign Lyra Daro",
|
||||||
|
"Ensign Fynn Talor",
|
||||||
|
"Lieutenant Commander Calen Venn",
|
||||||
|
"Lieutenant Selan Talvik",
|
||||||
|
"Ensign Aricel Rhos"
|
||||||
|
]
|
||||||
|
|
||||||
1066
lcars/embed/messages.json
Normal file
1066
lcars/embed/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
79
lcars/experiments.go
Normal file
79
lcars/experiments.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readCrew() (string, error) {
|
||||||
|
|
||||||
|
content, err := embedded.ReadFile("embed/crew.json")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a slice to hold the parsed names
|
||||||
|
var crewNames []string
|
||||||
|
|
||||||
|
// Parse the JSON
|
||||||
|
err = json.Unmarshal(content, &crewNames)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing JSON:", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the results
|
||||||
|
for i, text := range crewNames {
|
||||||
|
rank, name := splitRank(text)
|
||||||
|
if rank == "" {
|
||||||
|
rank = "ERROR"
|
||||||
|
}
|
||||||
|
fmt.Printf("%d: rank: %s name: %s\n", i+1, rank , name)
|
||||||
|
}
|
||||||
|
// fmt.Println(string(content))
|
||||||
|
return "", nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitRank separates the rank (all tokens except the last two) from the crewman's name (last two tokens)
|
||||||
|
func splitRank(fullName string) (rank, name string) {
|
||||||
|
tokens := strings.Fields(fullName)
|
||||||
|
if len(tokens) < 2 {
|
||||||
|
return fullName, "" // fallback if malformed
|
||||||
|
}
|
||||||
|
|
||||||
|
nameTokens := tokens[len(tokens)-2:] // last 2 tokens as name
|
||||||
|
rankTokens := tokens[:len(tokens)-2] // everything else as rank
|
||||||
|
name = strings.Join(nameTokens, " ")
|
||||||
|
rank = strings.Join(rankTokens, " ")
|
||||||
|
|
||||||
|
return rank, name
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMessages() ([]Message, error) {
|
||||||
|
content, err := embedded.ReadFile("embed/messages.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []Message
|
||||||
|
if err := json.Unmarshal(content, &messages); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For demonstration, print the parsed messages
|
||||||
|
for _, m := range messages {
|
||||||
|
fmt.Printf("[%s] %s (%s) - %s\n", m.Timestamp, m.Subsystem, m.Severity, m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages, nil
|
||||||
|
}
|
||||||
BIN
lcars/frontend/assets/Antonio-Bold.woff
Normal file
BIN
lcars/frontend/assets/Antonio-Bold.woff
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/Antonio-Bold.woff2
Normal file
BIN
lcars/frontend/assets/Antonio-Bold.woff2
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/Antonio-Regular.woff
Normal file
BIN
lcars/frontend/assets/Antonio-Regular.woff
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/Antonio-Regular.woff2
Normal file
BIN
lcars/frontend/assets/Antonio-Regular.woff2
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/beep1.mp3
Normal file
BIN
lcars/frontend/assets/beep1.mp3
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/beep2.mp3
Normal file
BIN
lcars/frontend/assets/beep2.mp3
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/beep3.mp3
Normal file
BIN
lcars/frontend/assets/beep3.mp3
Normal file
Binary file not shown.
BIN
lcars/frontend/assets/beep4.mp3
Normal file
BIN
lcars/frontend/assets/beep4.mp3
Normal file
Binary file not shown.
3011
lcars/frontend/assets/classic.css
Normal file
3011
lcars/frontend/assets/classic.css
Normal file
File diff suppressed because it is too large
Load Diff
50
lcars/frontend/assets/lcars.js
Normal file
50
lcars/frontend/assets/lcars.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
document.addEventListener("touchstart", function() {},false);
|
||||||
|
let mybutton = document.getElementById("topBtn");
|
||||||
|
window.onscroll = function() {scrollFunction()};
|
||||||
|
function scrollFunction() {
|
||||||
|
if (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) {
|
||||||
|
mybutton.style.display = "block";
|
||||||
|
} else {
|
||||||
|
mybutton.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function topFunction() {
|
||||||
|
document.body.scrollTop = 0;
|
||||||
|
document.documentElement.scrollTop = 0;
|
||||||
|
}
|
||||||
|
function playSoundAndRedirect(audioId, url) {
|
||||||
|
var audio = document.getElementById(audioId);
|
||||||
|
audio.play();
|
||||||
|
|
||||||
|
audio.onended = function() {
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function goToAnchor(anchorId) {
|
||||||
|
window.location.hash = anchorId;
|
||||||
|
}
|
||||||
|
// Accordion drop-down
|
||||||
|
var acc = document.getElementsByClassName("accordion");
|
||||||
|
var i;
|
||||||
|
|
||||||
|
for (i = 0; i < acc.length; i++) {
|
||||||
|
acc[i].addEventListener("click", function() {
|
||||||
|
this.classList.toggle("active");
|
||||||
|
var accordionContent = this.nextElementSibling;
|
||||||
|
if (accordionContent.style.maxHeight){
|
||||||
|
accordionContent.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
accordionContent.style.maxHeight = accordionContent.scrollHeight + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// LCARS keystroke sound (not to be used with hyperlinks)
|
||||||
|
const LCARSkeystroke = document.getElementById('LCARSkeystroke');
|
||||||
|
const allPlaySoundButtons = document.querySelectorAll('.playSoundButton');
|
||||||
|
allPlaySoundButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
LCARSkeystroke.pause();
|
||||||
|
LCARSkeystroke.currentTime = 0; // Reset to the beginning of the sound
|
||||||
|
LCARSkeystroke.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
1881
lcars/frontend/assets/lower-decks-padd.css
Normal file
1881
lcars/frontend/assets/lower-decks-padd.css
Normal file
File diff suppressed because it is too large
Load Diff
1856
lcars/frontend/assets/lower-decks.css
Normal file
1856
lcars/frontend/assets/lower-decks.css
Normal file
File diff suppressed because it is too large
Load Diff
2830
lcars/frontend/assets/nemesis-blue.css
Normal file
2830
lcars/frontend/assets/nemesis-blue.css
Normal file
File diff suppressed because it is too large
Load Diff
321
lcars/frontend/index copy 2.html
Normal file
321
lcars/frontend/index copy 2.html
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Lower Decks PADD</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<meta name="format-detection" content="date=no">
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/lower-decks-padd.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
|
||||||
|
<div class="wrap-all">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame-top">
|
||||||
|
<!--
|
||||||
|
*** LCARS PANEL BUTTON ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||||
|
|
||||||
|
<div class="panel-1">
|
||||||
|
<a href="#">LCARS</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
|
||||||
|
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame-top">
|
||||||
|
<div class="banner">LCARS 57436.2</div>
|
||||||
|
<div class="data-cascade-button-group">
|
||||||
|
<div class="data-wrapper">
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||||
|
<div class="dc-row-2">31</div>
|
||||||
|
<div class="dc-row-3">28</div>
|
||||||
|
<div class="dc-row-4">94</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">329</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">128</div>
|
||||||
|
<div class="dc-row-3">605</div>
|
||||||
|
<div class="dc-row-4">704</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||||
|
<div class="dc-row-4">40372566301</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">04</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">35</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||||
|
<div class="dc-row-4">297</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">48</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">07</div>
|
||||||
|
<div class="dc-row-3">38</div>
|
||||||
|
<div class="dc-row-4">62</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">416</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">001</div>
|
||||||
|
<div class="dc-row-3">888</div>
|
||||||
|
<div class="dc-row-4">442</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||||
|
<div class="dc-row-4">54048523421</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||||
|
<div class="dc-row-2">80</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">31</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||||
|
<div class="dc-row-2">71</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">26</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">98</div>
|
||||||
|
<div class="dc-row-2">63</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">52</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">118</div>
|
||||||
|
<div class="dc-row-2">270</div>
|
||||||
|
<div class="dc-row-3">395</div>
|
||||||
|
<div class="dc-row-4">260</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">8675309</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||||
|
<div class="dc-row-3">9282721</div>
|
||||||
|
<div class="dc-row-4">4981518</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">65821407321</div>
|
||||||
|
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||||
|
<div class="dc-row-4">38954062564</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||||
|
<div class="dc-row-4">293</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">3872</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">1105</div>
|
||||||
|
<div class="dc-row-3">1106</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<!--
|
||||||
|
*** MAIN NAVIGATION BUTTONS ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not).
|
||||||
|
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||||
|
<a href="#">01</a>
|
||||||
|
<a href="#">02</a>
|
||||||
|
<a href="#">03</a>
|
||||||
|
<a href="#">04</a>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="bar-panel first-bar-panel">
|
||||||
|
<div class="bar-1"> </div>
|
||||||
|
<div class="bar-2"> </div>
|
||||||
|
<div class="bar-3"> </div>
|
||||||
|
<div class="bar-4"> </div>
|
||||||
|
<div class="bar-5"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<div class="block-left"> </div>
|
||||||
|
<div class="block-right">
|
||||||
|
<div class="block-row">
|
||||||
|
<div class="bar-11"> </div>
|
||||||
|
<div class="bar-12"> </div>
|
||||||
|
<div class="bar-13"> </div>
|
||||||
|
<div class="bar-14">
|
||||||
|
<div class="blockhead"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame">
|
||||||
|
<!--
|
||||||
|
** SCROLL TO TOP OF PAGE BUTTON **
|
||||||
|
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||||
|
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||||
|
-->
|
||||||
|
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||||
|
class="hop">screen</span> top</button>
|
||||||
|
<div>
|
||||||
|
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||||
|
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||||
|
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||||
|
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame">
|
||||||
|
<div class="bar-panel">
|
||||||
|
<div class="bar-6"> </div>
|
||||||
|
<div class="bar-7"> </div>
|
||||||
|
<div class="bar-8"> </div>
|
||||||
|
<div class="bar-9"> </div>
|
||||||
|
<div class="bar-10"> </div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div class="lcars-message-stack"
|
||||||
|
style="display: flex; flex-direction: column; gap: 0.75rem; width: 100%; padding: 1rem;">
|
||||||
|
|
||||||
|
<!-- CRITICAL -->
|
||||||
|
<div class="lcars-message critical pulse-rate-high">
|
||||||
|
<div class="lcars-text-bar background-sunset-red">
|
||||||
|
<span class="font-sunset-red">CRITICAL: Core logic node recursion detected! ACK
|
||||||
|
required!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALERT -->
|
||||||
|
<div class="lcars-message alert pulse">
|
||||||
|
<div class="lcars-text-bar background-radioactive">
|
||||||
|
<span class="font-radioactive">ALERT: Warp coil 7C emitting sparks</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WARNING -->
|
||||||
|
<div class="lcars-message warning blink">
|
||||||
|
<div class="lcars-text-bar background-arctic-ice">
|
||||||
|
<span class="font-arctic-ice">WARNING: Deck 4 humidity spike detected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOTICE -->
|
||||||
|
<div class="lcars-message notice blink-slow">
|
||||||
|
<div class="lcars-text-bar background-beta-blue">
|
||||||
|
<span class="font-beta-blue">NOTICE: Deck 8 greenhouse light cycling</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INFO -->
|
||||||
|
<div class="lcars-message info smoke-glass">
|
||||||
|
<div class="lcars-text-bar background-alpha-blue">
|
||||||
|
<span class="font-alpha-blue">INFO: Crewman Mariner spilled coffee</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Another CRITICAL -->
|
||||||
|
<div class="lcars-message critical pulse-rate-high">
|
||||||
|
<div class="lcars-text-bar background-sunset-red">
|
||||||
|
<span class="font-sunset-red">CRITICAL: Warp field instability detected! Immediate
|
||||||
|
action!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALERT -->
|
||||||
|
<div class="lcars-message alert pulse">
|
||||||
|
<div class="lcars-text-bar background-radioactive">
|
||||||
|
<span class="font-radioactive">ALERT: Transporter buffer overload</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WARNING -->
|
||||||
|
<div class="lcars-message warning blink">
|
||||||
|
<div class="lcars-text-bar background-arctic-ice">
|
||||||
|
<span class="font-arctic-ice">WARNING: Engineering turbolift malfunctioning</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOTICE -->
|
||||||
|
<div class="lcars-message notice blink-slow">
|
||||||
|
<div class="lcars-text-bar background-beta-blue">
|
||||||
|
<span class="font-beta-blue">NOTICE: Hydration levels on Deck 5 nominal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INFO -->
|
||||||
|
<div class="lcars-message info smoke-glass">
|
||||||
|
<div class="lcars-text-bar background-alpha-blue">
|
||||||
|
<span class="font-alpha-blue">INFO: Crewman Boimler practicing tap-dancing in
|
||||||
|
holodeck</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Optional JavaScript for auto-fade INFO / NOTICE messages -->
|
||||||
|
<script>
|
||||||
|
const fadeMessages = () => {
|
||||||
|
document.querySelectorAll('.lcars-message.info, .lcars-message.notice').forEach(msg => {
|
||||||
|
setTimeout(() => {
|
||||||
|
msg.style.transition = "opacity 8s";
|
||||||
|
msg.style.opacity = 0;
|
||||||
|
setTimeout(() => msg.remove(), 8000);
|
||||||
|
}, 5000); // fade after 5 seconds
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fadeMessages();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||||
|
Content Copyright © 2025 ld.hedeler.com <br>
|
||||||
|
|
||||||
|
<!-- The following attribution must not be removed: -->
|
||||||
|
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||||
|
<div class="headtrim"> </div>
|
||||||
|
<div class="baseboard"> </div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
235
lcars/frontend/index copy.html
Normal file
235
lcars/frontend/index copy.html
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Lower Decks PADD</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<meta name="format-detection" content="date=no">
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/lower-decks-padd.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
|
||||||
|
<div class="wrap-all">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame-top">
|
||||||
|
<!--
|
||||||
|
*** LCARS PANEL BUTTON ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||||
|
|
||||||
|
<div class="panel-1">
|
||||||
|
<a href="#">LCARS</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
|
||||||
|
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame-top">
|
||||||
|
<div class="banner">LCARS 57436.2</div>
|
||||||
|
<div class="data-cascade-button-group">
|
||||||
|
<div class="data-wrapper">
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||||
|
<div class="dc-row-2">31</div>
|
||||||
|
<div class="dc-row-3">28</div>
|
||||||
|
<div class="dc-row-4">94</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">329</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">128</div>
|
||||||
|
<div class="dc-row-3">605</div>
|
||||||
|
<div class="dc-row-4">704</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||||
|
<div class="dc-row-4">40372566301</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">04</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">35</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||||
|
<div class="dc-row-4">297</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">48</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">07</div>
|
||||||
|
<div class="dc-row-3">38</div>
|
||||||
|
<div class="dc-row-4">62</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">416</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">001</div>
|
||||||
|
<div class="dc-row-3">888</div>
|
||||||
|
<div class="dc-row-4">442</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||||
|
<div class="dc-row-4">54048523421</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||||
|
<div class="dc-row-2">80</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">31</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||||
|
<div class="dc-row-2">71</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">26</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">98</div>
|
||||||
|
<div class="dc-row-2">63</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">52</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">118</div>
|
||||||
|
<div class="dc-row-2">270</div>
|
||||||
|
<div class="dc-row-3">395</div>
|
||||||
|
<div class="dc-row-4">260</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">8675309</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||||
|
<div class="dc-row-3">9282721</div>
|
||||||
|
<div class="dc-row-4">4981518</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">65821407321</div>
|
||||||
|
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||||
|
<div class="dc-row-4">38954062564</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||||
|
<div class="dc-row-4">293</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">3872</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">1105</div>
|
||||||
|
<div class="dc-row-3">1106</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<!--
|
||||||
|
*** MAIN NAVIGATION BUTTONS ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not).
|
||||||
|
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||||
|
<a href="#">01</a>
|
||||||
|
<a href="#">02</a>
|
||||||
|
<a href="#">03</a>
|
||||||
|
<a href="#">04</a>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="bar-panel first-bar-panel">
|
||||||
|
<div class="bar-1"> </div>
|
||||||
|
<div class="bar-2"> </div>
|
||||||
|
<div class="bar-3"> </div>
|
||||||
|
<div class="bar-4"> </div>
|
||||||
|
<div class="bar-5"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<div class="block-left"> </div>
|
||||||
|
<div class="block-right">
|
||||||
|
<div class="block-row">
|
||||||
|
<div class="bar-11"> </div>
|
||||||
|
<div class="bar-12"> </div>
|
||||||
|
<div class="bar-13"> </div>
|
||||||
|
<div class="bar-14">
|
||||||
|
<div class="blockhead"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame">
|
||||||
|
<!--
|
||||||
|
** SCROLL TO TOP OF PAGE BUTTON **
|
||||||
|
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||||
|
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||||
|
-->
|
||||||
|
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
|
||||||
|
<div>
|
||||||
|
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||||
|
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||||
|
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||||
|
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame">
|
||||||
|
<div class="bar-panel">
|
||||||
|
<div class="bar-6"> </div>
|
||||||
|
<div class="bar-7"> </div>
|
||||||
|
<div class="bar-8"> </div>
|
||||||
|
<div class="bar-9"> </div>
|
||||||
|
<div class="bar-10"> </div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<!-- Start your content here. -->
|
||||||
|
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<h2>Welcome to LCARS • Lower Decks PADD Theme</h2>
|
||||||
|
<h3 class="font-radioactive">Version 24.2</h3>
|
||||||
|
<h4>Replace This Content With Your Own</h4>
|
||||||
|
<p class="go-big">Live long and prosper.</p>
|
||||||
|
|
||||||
|
<!-- End content area. -->
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||||
|
Content Copyright © 2025 ld.hedeler.com <br>
|
||||||
|
|
||||||
|
<!-- The following attribution must not be removed: -->
|
||||||
|
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||||
|
<div class="headtrim"> </div>
|
||||||
|
<div class="baseboard"> </div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
345
lcars/frontend/index.html
Normal file
345
lcars/frontend/index.html
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Lower Decks PADD</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<meta name="format-detection" content="date=no">
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/lower-decks-padd.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||||
|
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
|
||||||
|
<div class="wrap-all">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame-top">
|
||||||
|
<!--
|
||||||
|
*** LCARS PANEL BUTTON ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||||
|
|
||||||
|
<div class="panel-1">
|
||||||
|
<a href="#">LCARS</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
|
||||||
|
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame-top">
|
||||||
|
<div class="banner">LCARS 57436.2</div>
|
||||||
|
<div class="data-cascade-button-group">
|
||||||
|
<div class="data-wrapper">
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||||
|
<div class="dc-row-2">31</div>
|
||||||
|
<div class="dc-row-3">28</div>
|
||||||
|
<div class="dc-row-4">94</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">329</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">128</div>
|
||||||
|
<div class="dc-row-3">605</div>
|
||||||
|
<div class="dc-row-4">704</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||||
|
<div class="dc-row-4">40372566301</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">04</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">35</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||||
|
<div class="dc-row-4">297</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">48</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">07</div>
|
||||||
|
<div class="dc-row-3">38</div>
|
||||||
|
<div class="dc-row-4">62</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">416</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">001</div>
|
||||||
|
<div class="dc-row-3">888</div>
|
||||||
|
<div class="dc-row-4">442</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||||
|
<div class="dc-row-4">54048523421</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||||
|
<div class="dc-row-2">80</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">31</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||||
|
<div class="dc-row-2">71</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">40</div>
|
||||||
|
<div class="dc-row-4 font-night-rain">26</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">98</div>
|
||||||
|
<div class="dc-row-2">63</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">52</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">118</div>
|
||||||
|
<div class="dc-row-2">270</div>
|
||||||
|
<div class="dc-row-3">395</div>
|
||||||
|
<div class="dc-row-4">260</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">8675309</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||||
|
<div class="dc-row-3">9282721</div>
|
||||||
|
<div class="dc-row-4">4981518</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||||
|
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||||
|
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||||
|
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">65821407321</div>
|
||||||
|
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||||
|
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||||
|
<div class="dc-row-4">38954062564</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||||
|
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||||
|
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||||
|
<div class="dc-row-4">293</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-column">
|
||||||
|
<div class="dc-row-1">3872</div>
|
||||||
|
<div class="dc-row-2 font-night-rain">1105</div>
|
||||||
|
<div class="dc-row-3">1106</div>
|
||||||
|
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<!--
|
||||||
|
*** MAIN NAVIGATION BUTTONS ***
|
||||||
|
Replace the hashtag '#' with a real URL (or not).
|
||||||
|
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||||
|
<a href="#">01</a>
|
||||||
|
<a href="#">02</a>
|
||||||
|
<a href="#">03</a>
|
||||||
|
<a href="#">04</a>
|
||||||
|
-->
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||||
|
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="bar-panel first-bar-panel">
|
||||||
|
<div class="bar-1"> </div>
|
||||||
|
<div class="bar-2"> </div>
|
||||||
|
<div class="bar-3"> </div>
|
||||||
|
<div class="bar-4"> </div>
|
||||||
|
<div class="bar-5"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<div class="block-left"> </div>
|
||||||
|
<div class="block-right">
|
||||||
|
<div class="block-row">
|
||||||
|
<div class="bar-11"> </div>
|
||||||
|
<div class="bar-12"> </div>
|
||||||
|
<div class="bar-13"> </div>
|
||||||
|
<div class="bar-14">
|
||||||
|
<div class="blockhead"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="left-frame">
|
||||||
|
<!--
|
||||||
|
** SCROLL TO TOP OF PAGE BUTTON **
|
||||||
|
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||||
|
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||||
|
-->
|
||||||
|
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||||
|
class="hop">screen</span> top</button>
|
||||||
|
<div>
|
||||||
|
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||||
|
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||||
|
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||||
|
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-frame">
|
||||||
|
<div class="bar-panel">
|
||||||
|
<div class="bar-6"> </div>
|
||||||
|
<div class="bar-7"> </div>
|
||||||
|
<div class="bar-8"> </div>
|
||||||
|
<div class="bar-9"> </div>
|
||||||
|
<div class="bar-10"> </div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div class="lcars-message-stack"
|
||||||
|
style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%; padding: 1rem;">
|
||||||
|
|
||||||
|
<!-- CRITICAL -->
|
||||||
|
<div class="lcars-message critical pulse-rate-high" data-count="1">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-sunset-red" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-sunset-red">
|
||||||
|
<span class="font-sunset-red">CRITICAL: Core logic node recursion detected! ACK
|
||||||
|
required!</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-sunset-red" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALERT -->
|
||||||
|
<div class="lcars-message alert pulse" data-count="1">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-radioactive" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-radioactive">
|
||||||
|
<span class="font-radioactive">ALERT: Warp coil 7C emitting sparks</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-radioactive" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WARNING -->
|
||||||
|
<div class="lcars-message warning blink" data-count="1">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-arctic-ice" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-arctic-ice">
|
||||||
|
<span class="font-arctic-ice">WARNING: Deck 4 humidity spike detected</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-arctic-ice" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOTICE -->
|
||||||
|
<div class="lcars-message notice blink-slow" data-count="1">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-beta-blue" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-beta-blue">
|
||||||
|
<span class="font-beta-blue">NOTICE: Deck 8 greenhouse light cycling</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-beta-blue" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INFO -->
|
||||||
|
<div class="lcars-message info smoke-glass" data-count="1">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-alpha-blue" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-alpha-blue">
|
||||||
|
<span class="font-alpha-blue">INFO: Crewman Mariner spilled coffee</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-alpha-blue" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const stack = document.querySelector('.lcars-message-stack');
|
||||||
|
|
||||||
|
// Function to add a new message
|
||||||
|
function addMessage(level, text) {
|
||||||
|
const existing = [...stack.children].find(msg =>
|
||||||
|
msg.classList.contains(level) && msg.querySelector('span').innerText === text
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Increment stack counter
|
||||||
|
let countSpan = existing.querySelector('.counter');
|
||||||
|
let count = parseInt(existing.dataset.count) + 1;
|
||||||
|
existing.dataset.count = count;
|
||||||
|
countSpan.innerText = `×${count}`;
|
||||||
|
|
||||||
|
// Animate a tiny shake to highlight repeat
|
||||||
|
existing.style.transform = "translateX(10px)";
|
||||||
|
setTimeout(() => existing.style.transform = "translateX(0)", 150);
|
||||||
|
} else {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `lcars-message ${level}`;
|
||||||
|
div.dataset.count = "1";
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div class="data-bullet bullet-${level}" style="margin-right: 0.75rem;"></div>
|
||||||
|
<div class="lcars-text-bar background-${level}">
|
||||||
|
<span class="font-${level}">${text}</span>
|
||||||
|
</div>
|
||||||
|
<span class="counter font-${level}" style="margin-left: 0.5rem;">×1</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
stack.prepend(div); // CRITICAL messages appear on top
|
||||||
|
if (level === "critical") {
|
||||||
|
div.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fade INFO / NOTICE
|
||||||
|
if (level === "info" || level === "notice") {
|
||||||
|
setTimeout(() => {
|
||||||
|
div.style.transition = "opacity 2s";
|
||||||
|
div.style.opacity = 0;
|
||||||
|
setTimeout(() => div.remove(), 2000);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo: add repeated messages over time
|
||||||
|
setTimeout(() => addMessage('critical', 'Core logic node recursion detected! ACK required!'), 2000);
|
||||||
|
setTimeout(() => addMessage('alert', 'Warp coil 7C emitting sparks'), 3500);
|
||||||
|
setTimeout(() => addMessage('info', 'Crewman Mariner spilled coffee'), 5000);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||||
|
Content Copyright © 2025 ld.hedeler.com <br>
|
||||||
|
|
||||||
|
<!-- The following attribution must not be removed: -->
|
||||||
|
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||||
|
<div class="headtrim"> </div>
|
||||||
|
<div class="baseboard"> </div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
18
lcars/go.mod
Normal file
18
lcars/go.mod
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module ld
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require modernc.org/sqlite v1.39.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
modernc.org/libc v1.66.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
49
lcars/go.sum
Normal file
49
lcars/go.sum
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
|
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||||
|
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||||
|
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||||
|
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
94
lcars/interval/interval.go
Normal file
94
lcars/interval/interval.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package interval
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MutexMap[K comparable, V any] struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
m map[K]V
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MutexMap[K, V]) Get(key K) (V, error) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
v, ok := m.m[key]
|
||||||
|
if !ok {
|
||||||
|
return v, errors.New("unknown key")
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MutexMap[K, V]) Set(key K, value V) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
m.m[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MutexMap[K, V]) Delete(key K) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
delete(m.m, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopChannels = MutexMap[int, chan bool]{
|
||||||
|
m: make(map[int]chan bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInterval schedules a repeating task to be executed at a specified interval.
|
||||||
|
func SetInterval(f func(), milliseconds int) (id int) {
|
||||||
|
for {
|
||||||
|
id = rand.Int()
|
||||||
|
if _, err := stopChannels.Get(id); err == nil {
|
||||||
|
continue // ID collision, keep looking for another unique random value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := make(chan bool)
|
||||||
|
stopChannels.Set(id, stop)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(milliseconds) * time.Millisecond)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearInterval stops a scheduled interval identified by the specified interval ID.
|
||||||
|
func ClearInterval(id int) error {
|
||||||
|
stop, err := stopChannels.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stop <- true
|
||||||
|
stopChannels.Delete(id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeout schedules a one-time task to be executed after a specified interval.
|
||||||
|
func SetTimeout(f func(), milliseconds int) {
|
||||||
|
timer := time.NewTimer(time.Duration(milliseconds) * time.Millisecond)
|
||||||
|
go func() {
|
||||||
|
<-timer.C
|
||||||
|
timer.Stop()
|
||||||
|
f()
|
||||||
|
}()
|
||||||
|
}
|
||||||
0
lcars/ld.log
Normal file
0
lcars/ld.log
Normal file
153
lcars/main.go
Normal file
153
lcars/main.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"ld/server"
|
||||||
|
"ld/sqlite"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exitCodeErr = 1
|
||||||
|
exitCodeInterrupt = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppRoot = "./" // path for supporting files that sit in app root folder in production
|
||||||
|
|
||||||
|
//go:embed frontend/*
|
||||||
|
var frontend embed.FS
|
||||||
|
|
||||||
|
//go:embed embed
|
||||||
|
var embedded embed.FS
|
||||||
|
|
||||||
|
// main specific variables
|
||||||
|
var ExecutableName string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Here")
|
||||||
|
err := run()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(exitCodeErr)
|
||||||
|
}
|
||||||
|
ExecutableName, err = getExecutableName()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(exitCodeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
readCrew()
|
||||||
|
|
||||||
|
readMessages()
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
|
||||||
|
// setting up logging to file
|
||||||
|
logFileName := AppRoot + ExecutableName + ".log"
|
||||||
|
logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error opening file: %v", err)
|
||||||
|
os.Exit(exitCodeErr)
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
|
||||||
|
stateDB, err := createStateDB(true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create internal StateDB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
// setting up the server
|
||||||
|
server, err := server.New(
|
||||||
|
logFileName,
|
||||||
|
stateDB,
|
||||||
|
embedded,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("server-app not created - error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tasks.SetupTasks(server)
|
||||||
|
|
||||||
|
err = server.Start()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("server not started - error: %v \n", err)
|
||||||
|
log.Fatalf("server not started - error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for os shutdown events, report them into log file and exit application
|
||||||
|
chanOS := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(chanOS, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-chanOS
|
||||||
|
log.Println("shutting down request signal received")
|
||||||
|
server.Close()
|
||||||
|
os.Exit(exitCodeInterrupt)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// setting up the routes, hooking up API endpoints with backend functions
|
||||||
|
// routes.SetupRoutes(server)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// some internal functions
|
||||||
|
|
||||||
|
func getExecutableName() (string, error) {
|
||||||
|
name, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
name = filepath.Base(name)
|
||||||
|
return strings.TrimSuffix(name, filepath.Ext(name)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStateDB(StateDBDelete bool) (*sqlite.Database, error) {
|
||||||
|
|
||||||
|
// fileName := fmt.Sprintf("state-%s.db", ulid.Make())
|
||||||
|
|
||||||
|
fileName := "state.db"
|
||||||
|
|
||||||
|
if StateDBDelete {
|
||||||
|
_, err := os.Stat(fileName)
|
||||||
|
if err == nil {
|
||||||
|
err := os.Remove(fileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("error deleting statedb-file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sqlite.New(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := embedded.ReadFile("embed/create_state_db.sql")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.DB().Exec(string(query))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
103
lcars/server/server.go
Normal file
103
lcars/server/server.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2024 codeM GmbH
|
||||||
|
// Author: Thomas Hedeler
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"ld/interval"
|
||||||
|
"ld/sqlite"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
SQLiteVersion string
|
||||||
|
ServerInfo map[string]any
|
||||||
|
StateDB *sqlite.Database
|
||||||
|
Embedded embed.FS
|
||||||
|
LogFileName string
|
||||||
|
TokenDuration int // TODO einbauen
|
||||||
|
Secret []byte
|
||||||
|
Header string
|
||||||
|
intervalID int
|
||||||
|
Tasks map[string]TaskFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
logfilename string,
|
||||||
|
StateDB *sqlite.Database,
|
||||||
|
embedded embed.FS,
|
||||||
|
|
||||||
|
) (*Server, error) {
|
||||||
|
|
||||||
|
// creating the server
|
||||||
|
return &Server{
|
||||||
|
LogFileName: logfilename,
|
||||||
|
StateDB: StateDB,
|
||||||
|
Embedded: embedded,
|
||||||
|
Tasks: make(map[string]func(s *Server) error),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
|
||||||
|
// query, err := s.Embedded.ReadFile("embed/win/server_info.sql")
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// res, err := s.StundenDB.ReadRecords(string(query))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// s.ServerInfo = res[0]
|
||||||
|
|
||||||
|
// err = inits.LoadLogins(s.StundenDB, s.StateDB)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// err = inits.LoadTasks(s.StundenDB, s.StateDB)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // start the task engine
|
||||||
|
// if s.Production {
|
||||||
|
// s.intervalID = interval.SetInterval(s.interval, 60000) // check for executable tasks every 60 seconds
|
||||||
|
// } else {
|
||||||
|
// s.intervalID = interval.SetInterval(s.interval, 30000) // check for executable tasks every 30 seconds
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
|
||||||
|
// stop the task engine
|
||||||
|
err := interval.ClearInterval(s.intervalID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.StateDB.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// func to dispatch routes to all parts of the application:
|
||||||
|
// they receive references to the server and the current fiber context via closures
|
||||||
|
// this way all functions have access to server properties and can handle the
|
||||||
|
// incoming requests themselves.
|
||||||
|
|
||||||
|
// type HandlerFunc = func(s *Server, c *fiber.Ctx) error
|
||||||
|
|
||||||
|
// func (s *Server) Handler(handler HandlerFunc) func(c *fiber.Ctx) error {
|
||||||
|
// return func(c *fiber.Ctx) error {
|
||||||
|
// return handler(s, c)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// signature for internal tasks
|
||||||
|
type TaskFunc = func(s *Server) error
|
||||||
118
lcars/server/taskengine.go
Normal file
118
lcars/server/taskengine.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// this function schedules the tasks and will be called periodically, see server.Start()
|
||||||
|
func (s *Server) interval() {
|
||||||
|
|
||||||
|
// read scheduled task list from stateDB
|
||||||
|
// check for next executable task:
|
||||||
|
// - if there is one or more tasks ready for execution then select one of them.
|
||||||
|
// - if there is a selected task, update its next execution field and execute the task
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// fmt.Println("Recovered from panic in worker:", r)
|
||||||
|
log.Printf("recovered from panic in taskengine: %v ", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tasks, err := s.StateDB.ReadRecords("SELECT * FROM tasks ORDER by next_execution limit 1;")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error in taskengine: %s ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) < 1 {
|
||||||
|
log.Printf("error in taskengine: %s ", "found no task for execution")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task := tasks[0] // pick the one task with the smallest next execution time, see previous sql statement
|
||||||
|
|
||||||
|
task_name, haveTask := task["task_name"].(string)
|
||||||
|
if !haveTask {
|
||||||
|
log.Printf("error in taskengine: task %s is of wrong type", task["task_name"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextExecution := task["next_execution"].(int64)
|
||||||
|
startTime := task["start_time"].(int64)
|
||||||
|
execInterval := task["interval"].(int64)
|
||||||
|
nowSeconds := time.Now().Unix()
|
||||||
|
|
||||||
|
if nowSeconds < nextExecution { // task execution is not yet due
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate next execution time
|
||||||
|
for nextExecution = startTime; nowSeconds > nextExecution; nextExecution += execInterval {
|
||||||
|
// add as many intervals to the starttime until the next execution lies in the future
|
||||||
|
}
|
||||||
|
|
||||||
|
task["start_time"] = task["next_execution"]
|
||||||
|
task["next_execution"] = nextExecution
|
||||||
|
|
||||||
|
/*
|
||||||
|
no_executions INTEGER, -- how often executed
|
||||||
|
duration INTEGER, -- duration of the last exec in ms
|
||||||
|
no_errors INTEGER, -- error count
|
||||||
|
last_error_text TEXT,
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
if count, ok := task["no_executions"].(int64); ok {
|
||||||
|
task["no_executions"] = count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// update next_execution in state database
|
||||||
|
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error in taskengine: cannot update task record - before execution %s ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task_func, haveTask := s.Tasks[task_name] // select the function with the matching name
|
||||||
|
|
||||||
|
if !haveTask {
|
||||||
|
log.Printf("error in taskengine: task %s is not defined", task_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if haveTask {
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
// if !s.Production {
|
||||||
|
// fmt.Println("Taskengine: executing task:", task_name, start)
|
||||||
|
// }
|
||||||
|
err = task_func(s) // finally execute the task; attention: a task that panics will kill the server!
|
||||||
|
|
||||||
|
task["duration"] = int(time.Since(start).Milliseconds())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("taskengine: execution task: %s failed with error: %s ", task_name, err)
|
||||||
|
task["last_error_text"] = err.Error()
|
||||||
|
if count, ok := task["no_errors"].(int64); ok {
|
||||||
|
task["no_errors"] = count + 1
|
||||||
|
}
|
||||||
|
// if !s.Production {
|
||||||
|
// fmt.Println("Taskengine: failed task:", task_name, err)
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
// if !s.Production {
|
||||||
|
// fmt.Println("Taskengine: successfully completed task:", task_name, time.Now())
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error in taskengine: cannot update task record - after execution %s ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
428
lcars/sqlite/database.go
Normal file
428
lcars/sqlite/database.go
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
package sqlite // name the package as you see fit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is the data type to exchange data with the database
|
||||||
|
type Record = map[string]any
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
databaseName string
|
||||||
|
database *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
tx *sql.Tx
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action func(tx *sql.Tx) error
|
||||||
|
|
||||||
|
func New(DBName string) (*Database, error) {
|
||||||
|
return &Database{databaseName: DBName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Close() error {
|
||||||
|
return d.database.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// provides access to the internal database object
|
||||||
|
func (d *Database) DB() *sql.DB {
|
||||||
|
return d.database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Name() string {
|
||||||
|
return d.databaseName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Open() (err error) {
|
||||||
|
d.database, err = openSqliteDB(d.databaseName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) OpenInMemory() (err error) {
|
||||||
|
d.database, err = sql.Open("sqlite", ":memory:")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSqliteDB(databasefilename string) (*sql.DB, error) {
|
||||||
|
|
||||||
|
_, err := os.Stat(databasefilename)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return createDB(databasefilename)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sql.Open("sqlite", databasefilename)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDB(dbfileName string) (*sql.DB, error) {
|
||||||
|
|
||||||
|
query := `
|
||||||
|
PRAGMA page_size = 4096;
|
||||||
|
PRAGMA synchronous = off;
|
||||||
|
PRAGMA foreign_keys = off;
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA user_version = 1;
|
||||||
|
`
|
||||||
|
db, err := sql.Open("sqlite", dbfileName)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = db.Exec(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) TableList() (result []Record, err error) {
|
||||||
|
return d.ReadRecords("select name from sqlite_master where type='table';")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) ReadTable(tablename string) (result []Record, err error) {
|
||||||
|
|
||||||
|
return d.ReadRecords(fmt.Sprintf("select * from '%s';", tablename))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) ReadRecords(query string, args ...any) (result []Record, err error) {
|
||||||
|
|
||||||
|
rows, err := d.DB().Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return Rows2records(rows)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetRecord(tablename string, idfield string, key any) (result Record, err error) {
|
||||||
|
|
||||||
|
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||||
|
res, err := d.DB().Query(query, key)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
return Rows2record(res)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpsertRecord(tablename string, idfield string, record Record) (result Record, err error) {
|
||||||
|
|
||||||
|
return upsert(d.DB(), tablename, idfield, record)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) DeleteRecord(tablename string, idfield string, id any) (err error) {
|
||||||
|
|
||||||
|
return deleteRecord(d.DB(), tablename, idfield, id)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// *sql.DB and *sql.Tx both have a method named 'Query',
|
||||||
|
// this way they can both be passed into upsert and deleteRecord function
|
||||||
|
type iquery interface {
|
||||||
|
Query(query string, args ...any) (*sql.Rows, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsert(t iquery, tablename string, idfield string, record Record) (result Record, err error) {
|
||||||
|
|
||||||
|
fields := []string{}
|
||||||
|
data := []any{}
|
||||||
|
for k, v := range record {
|
||||||
|
fields = append(fields, k)
|
||||||
|
data = append(data, v)
|
||||||
|
}
|
||||||
|
query, err := buildUpsertCommand(tablename, idfield, fields)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
res, err := t.Query(query, data...) // res contains the full record - see SQLite: RETURNING *
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
return Rows2record(res)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRecord(t iquery, tablename string, idfield string, id any) (err error) {
|
||||||
|
|
||||||
|
query := fmt.Sprintf("DELETE FROM \"%s\" WHERE \"%s\" = ?;", tablename, idfield)
|
||||||
|
_, err = t.Query(query, id)
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUpsertCommand(tablename string, idfield string, fields []string) (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(256 + len(fields)*20) // rough preallocation
|
||||||
|
|
||||||
|
// INSERT INTO
|
||||||
|
sb.WriteString(`INSERT INTO "`)
|
||||||
|
sb.WriteString(tablename)
|
||||||
|
sb.WriteString(`"(`)
|
||||||
|
for i, f := range fields {
|
||||||
|
sb.WriteString(` "`)
|
||||||
|
sb.WriteString(f)
|
||||||
|
sb.WriteByte('"')
|
||||||
|
if i < len(fields)-1 {
|
||||||
|
sb.WriteByte(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(")\n\tVALUES(")
|
||||||
|
|
||||||
|
// VALUES
|
||||||
|
for i := 0; i < len(fields); i++ {
|
||||||
|
sb.WriteString(" ?")
|
||||||
|
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||||
|
if i < len(fields)-1 {
|
||||||
|
sb.WriteByte(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(")\n\tON CONFLICT(\"")
|
||||||
|
sb.WriteString(tablename)
|
||||||
|
sb.WriteString(`"."`)
|
||||||
|
sb.WriteString(idfield)
|
||||||
|
sb.WriteString("\")\n\tDO UPDATE SET ")
|
||||||
|
|
||||||
|
// UPDATE SET
|
||||||
|
for i, f := range fields {
|
||||||
|
sb.WriteByte('"')
|
||||||
|
sb.WriteString(f)
|
||||||
|
sb.WriteString(`"= ?`)
|
||||||
|
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||||
|
if i < len(fields)-1 {
|
||||||
|
sb.WriteByte(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString("\n\tRETURNING *;")
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func buildUpsertCommand(tablename string, idfield string, fields []string) (result string, err error) {
|
||||||
|
|
||||||
|
// pname := map[string]string{} // assign correct index for parameter name
|
||||||
|
// // parameter position, starts at 1 in sql! So it needs to be calculated by function pname inside template
|
||||||
|
|
||||||
|
// for i, k := range fields {
|
||||||
|
// pname[k] = strconv.Itoa(i + 1)
|
||||||
|
// }
|
||||||
|
// funcMap := template.FuncMap{
|
||||||
|
// "pname": func(fieldname string) string {
|
||||||
|
// return pname[fieldname]
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// tableDef := struct {
|
||||||
|
// Tablename string
|
||||||
|
// KeyField string
|
||||||
|
// LastField int
|
||||||
|
// FieldNames []string
|
||||||
|
// }{
|
||||||
|
// Tablename: tablename,
|
||||||
|
// KeyField: idfield,
|
||||||
|
// LastField: len(fields) - 1,
|
||||||
|
// FieldNames: fields,
|
||||||
|
// }
|
||||||
|
// var templString = `{{$last := .LastField}}INSERT INTO "{{ .Tablename }}"({{ range $i,$el := .FieldNames }} "{{$el}}"{{if ne $i $last}},{{end}}{{end}})
|
||||||
|
// VALUES({{ range $i,$el := .FieldNames }} ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}})
|
||||||
|
// ON CONFLICT("{{ .Tablename }}"."{{.KeyField}}")
|
||||||
|
// DO UPDATE SET {{ range $i,$el := .FieldNames }}"{{$el}}"= ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}}
|
||||||
|
// RETURNING *;`
|
||||||
|
|
||||||
|
// dbTempl, err := template.New("upsertDB").Funcs(funcMap).Parse(templString)
|
||||||
|
// if err != nil {
|
||||||
|
// return result, err
|
||||||
|
// }
|
||||||
|
// var templBytes bytes.Buffer
|
||||||
|
// err = dbTempl.Execute(&templBytes, tableDef)
|
||||||
|
// if err != nil {
|
||||||
|
// return result, err
|
||||||
|
// }
|
||||||
|
// return templBytes.String(), nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func Rows2record(rows *sql.Rows) (Record, error) {
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values := make([]any, len(columns))
|
||||||
|
valuePtrs := make([]any, len(columns))
|
||||||
|
for i := range values {
|
||||||
|
valuePtrs[i] = &values[i]
|
||||||
|
}
|
||||||
|
result := Record{}
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(valuePtrs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, col := range columns {
|
||||||
|
result[col] = values[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, errors.New("no rows found")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rows2records(rows *sql.Rows) ([]Record, error) {
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recLength := len(columns)
|
||||||
|
results := []Record{}
|
||||||
|
for rows.Next() {
|
||||||
|
values := make([]any, recLength)
|
||||||
|
valuePtrs := make([]any, recLength)
|
||||||
|
for i := range values {
|
||||||
|
valuePtrs[i] = &values[i]
|
||||||
|
}
|
||||||
|
record := Record{}
|
||||||
|
if err := rows.Scan(valuePtrs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, col := range columns {
|
||||||
|
record[col] = values[i]
|
||||||
|
}
|
||||||
|
results = append(results, record)
|
||||||
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
return nil, errors.New("no rows found")
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Version() (string, error) {
|
||||||
|
result := ""
|
||||||
|
sqliteversion, err := d.ReadRecords("SELECT sqlite_version();")
|
||||||
|
if len(sqliteversion) == 1 {
|
||||||
|
result = sqliteversion[0]["sqlite_version()"].(string)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UserVersion() (int64, error) {
|
||||||
|
|
||||||
|
var result int64
|
||||||
|
userversion, err := d.ReadRecords("PRAGMA user_version;")
|
||||||
|
if len(userversion) == 1 {
|
||||||
|
result = userversion[0]["user_version"].(int64)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Begin() *Transaction {
|
||||||
|
tx, err := d.database.Begin()
|
||||||
|
return &Transaction{tx, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) Next(action Action) *Transaction {
|
||||||
|
if t.err != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
t.err = action(t.tx)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) End() error {
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
err := t.tx.Rollback()
|
||||||
|
if err != nil {
|
||||||
|
t.err = errors.Join(t.err, err)
|
||||||
|
}
|
||||||
|
return t.err
|
||||||
|
}
|
||||||
|
t.err = t.tx.Commit()
|
||||||
|
return t.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) GetRecord(tablename string, idfield string, key any, output Record) *Transaction {
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||||
|
res, err := t.tx.Query(query, key)
|
||||||
|
if err != nil {
|
||||||
|
t.err = err
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
defer res.Close()
|
||||||
|
result, err := Rows2record(res)
|
||||||
|
if err != nil {
|
||||||
|
t.err = err
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
for k := range output {
|
||||||
|
delete(output, k)
|
||||||
|
}
|
||||||
|
for k, v := range result {
|
||||||
|
output[k] = v
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) UpsertRecord(tablename string, idfield string, record Record, output Record) *Transaction {
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
result, err := upsert(t.tx, tablename, idfield, record)
|
||||||
|
if err != nil {
|
||||||
|
t.err = err
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
for k := range output {
|
||||||
|
delete(output, k)
|
||||||
|
}
|
||||||
|
for k, v := range result {
|
||||||
|
output[k] = v
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) DeleteRecord(tablename string, idfield string, id any) *Transaction {
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
err := deleteRecord(t.tx, tablename, idfield, id)
|
||||||
|
if err != nil {
|
||||||
|
t.err = err
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a value of the provided type, if the field exist and if it can be cast into the provided type parameter
|
||||||
|
func Value[T any](rec Record, field string) (value T, ok bool) {
|
||||||
|
var v any
|
||||||
|
if v, ok = rec[field]; ok {
|
||||||
|
value, ok = v.(T)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't report an error if there are simply just 'no rows found'
|
||||||
|
func NoRowsOk(recs []Record, err error) ([]Record, error) {
|
||||||
|
if err != nil && err.Error() != "no rows found" {
|
||||||
|
return recs, err
|
||||||
|
}
|
||||||
|
return recs, nil
|
||||||
|
}
|
||||||
BIN
lcars/state.db
Normal file
BIN
lcars/state.db
Normal file
Binary file not shown.
BIN
lcars/state.db-shm
Normal file
BIN
lcars/state.db-shm
Normal file
Binary file not shown.
BIN
lcars/state.db-wal
Normal file
BIN
lcars/state.db-wal
Normal file
Binary file not shown.
@ -8,13 +8,84 @@ Who knows what this is? Follow up with very short discussian
|
|||||||
|
|
||||||
Starting with Web-Development many years ago, I came up with the following formular, I call it the Drake Formular for Webdevelopment
|
Starting with Web-Development many years ago, I came up with the following formular, I call it the Drake Formular for Webdevelopment
|
||||||
|
|
||||||
It like Drake's but I run it in reverse, it is a big Multiplication of technologies choices. Leading to billions of possible combinations of web applications
|
Insert Drake's formular of web technologies here.
|
||||||
|
|
||||||
No each living web application out there is a little star, showing a viable combination of web technologies.
|
It like Drake's but I run it in reverse, it is a big Multiplication of technologies choices. Leading to billions of possible combinations of web applications. With literally billions of combinations it is quite hard to choose one.
|
||||||
|
|
||||||
One can see this formular as the basis for a large n-dimensional vector space and just like in the real space where we have galaxies and clusters of galaxies, we also have clusters in this space. There is for example the cluster of react and supporting react technologies or there is an angular or vue cluster.
|
Now, each living web application out there is a little star, showing a viable combination of web technologies.
|
||||||
|
|
||||||
|
One can also see this formular as the basis for a large n-dimensional vector space and just like in the real space where we have galaxies and clusters of galaxies, we also have clusters in this space. There is for example the cluster of react and supporting react technologies or there is an angular cluster or vue cluster. I, for example, live in the line-of-business apps with vue neigborhood.
|
||||||
|
|
||||||
And we a basically following our personal journey through this space, gravitating to one cluster at on point in time or gravitating to another cluster later in time.
|
And we a basically following our personal journey through this space, gravitating to one cluster at on point in time or gravitating to another cluster later in time.
|
||||||
|
|
||||||
Today I want to visit with you a already quite old cluster, it is called hypermedia, and all web apps were living in this cluster a few decades ago. This cluster developed a new rather large bulge called htmx and very recently a still very small blob called Datastar and this is where our journey takes us today.
|
Today I want to visit with you a already quite old cluster, it is called hypermedia, and all web apps were living in this cluster a few decades ago. This cluster developed a new rather large bulge called htmx and very recently a still tiny blob called Datastar and this is where our journey takes us today.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
some preparations:
|
||||||
|
- to use Datastar you need to control the backend
|
||||||
|
- learn your prefered templating system
|
||||||
|
- be ready for a major paradigm shift (similar to the one with HTMX)
|
||||||
|
|
||||||
|
Insert Delaney's quote: Datastar is a backend agnostic backend framework with a 10 Kb shim
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Quick Excourse, my motivation: the minimal viable web app:
|
||||||
|
- I believe that the Complexity Budget is a real thing
|
||||||
|
- I believe that constraints can induce creativity: Creativity Paradox
|
||||||
|
- I believe that Simplicity is the ultimate form of perfection
|
||||||
|
|
||||||
|
My self imposed constraints are:
|
||||||
|
- No build step for the frontend part (no npm, no vite.js, no special tooling)
|
||||||
|
- Very few 3rd party libraries (sqlite-driver, datastar-sdk, datastar)
|
||||||
|
- Write the app with a reasonably minimal number of code lines
|
||||||
|
- Reduce the dependancies to a minimum.
|
||||||
|
|
||||||
|
Minimal is also about everything you need to build a complete web app. In this sense the mvwa establishes a baseline of everything you need to build (a great) web application.
|
||||||
|
|
||||||
|
part 1: Databases
|
||||||
|
part 2: The No-ORM ORM
|
||||||
|
part 3: Go-Programming language
|
||||||
|
part 4: Datastar
|
||||||
|
part 5: Modern CSS
|
||||||
|
part 6: Web Components
|
||||||
|
part 7: VPS and Deployment
|
||||||
|
|
||||||
|
To be clear: is true that Datastar is a minimalistic "framework" for web development, but it is by no means limited to minimalistic web apps.
|
||||||
|
Delaneys claim to be able to recreate any web app with datastar, faster with less code.
|
||||||
|
|
||||||
|
The longer you can stay in "declaritive space" and the shorter time you spend in "imparitive space" the faster you can develop you application.
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The core of Datastar
|
||||||
|
|
||||||
|
DS stays true to the web specifications, background the web can be a place for long-living apps/sites. A thirty year-old website can still be used.
|
||||||
|
|
||||||
|
DS uses the custom data-[what-ever] attributes. add info about browsers forgiving behavior of invalid attributes.
|
||||||
|
DS uses SSEs part of the HTTP-spec
|
||||||
|
DS works well with Web Components another web standard.
|
||||||
|
|
||||||
|
It uses Signals (based on Alien Signal) - I showed a minimalist implementation of signals in my last talk. Signals may become a web standard too.
|
||||||
|
It uses a morphing algorithm derived from ideomorph created by the team around HTMX, but interestingly not used my HTMX.
|
||||||
|
It can work with JSON or HTML API responses, but
|
||||||
|
it shines when using SSE.
|
||||||
|
|
||||||
|
In short it is quite simple actually, but a bit paradigm shift in web development
|
||||||
|
|
||||||
|
JSON responses update or create signals
|
||||||
|
HTML responses morph into the DOM based on id attibutes
|
||||||
|
SSEs can contain both JSON and HTML responses
|
||||||
|
|
||||||
|
The team has created an ADR, that show how to develop a backend, there are dozents of backend sdks available already
|
||||||
|
|
||||||
|
Lets revisit Delaney's quote: Datastar is a backend agnostic backend framework with a 10 Kb shim
|
||||||
|
|
||||||
|
insert a quick intro to SSEs and a quick compare to websockets
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user