Compare commits

..

No commits in common. "8a5c015e58c6df7b5ddd83b12fadc0bdd2e61f61" and "3a96f71760e862775b1877d0ed32c87a1811b0be" have entirely different histories.

45 changed files with 4 additions and 17016 deletions

View File

@ -1,501 +0,0 @@
<!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>

View File

@ -1,464 +0,0 @@
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)
}

View File

@ -1,53 +0,0 @@
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

View File

@ -1,841 +0,0 @@
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++) {

View File

@ -1,842 +0,0 @@
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) {

View File

@ -1,66 +0,0 @@
<!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>

View File

@ -1,316 +0,0 @@
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();
}
});

View File

@ -1,119 +0,0 @@
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;
}

View File

@ -1,3 +0,0 @@
module tetris
go 1.25.0

View File

@ -1,501 +0,0 @@
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)
}

Binary file not shown.

View File

@ -1,486 +0,0 @@
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)
}

View File

@ -1,3 +0,0 @@
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;

View File

@ -1,14 +0,0 @@
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
);

View File

@ -1,95 +0,0 @@
[
"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"
]

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
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
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +0,0 @@
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();
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,321 +0,0 @@
<!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 &#169; 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>

View File

@ -1,235 +0,0 @@
<!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 &#149; 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 &#169; 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>

View File

@ -1,345 +0,0 @@
<!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 &#169; 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>

View File

@ -1,18 +0,0 @@
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
)

View File

@ -1,49 +0,0 @@
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=

View File

@ -1,94 +0,0 @@
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()
}()
}

View File

View File

@ -1,153 +0,0 @@
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
}

View File

@ -1,103 +0,0 @@
// 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

View File

@ -1,118 +0,0 @@
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
}
}
}

View File

@ -1,428 +0,0 @@
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
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -8,84 +8,13 @@ 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
Insert Drake's formular of web technologies here.
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
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.
No each living web application out there is a little star, showing a viable combination of web technologies.
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.
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.
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 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
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.