From a05e79f0d1304b37cdad8e3b6bfd0b01d495a8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Sun, 30 Nov 2025 12:33:04 +0100 Subject: [PATCH] add hot Chunk reload --- README.md | 194 +++++++++++++++++++++++--------------------------- config.go | 46 +++++++----- simulation.go | 2 + websocket.go | 5 +- 4 files changed, 124 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index ed10123..cfe9b35 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,132 @@ # **๐Ÿƒ Escape the Teacher** -**A server-authoritative 2D endless runner built with Go, Redis, and JavaScript.** +**A high-performance, server-authoritative 2D endless runner built with Go, Redis, and WebSockets.** ## **๐Ÿ“– About the Project** -"Escape the Teacher" is a high-performance web game developed as a school project. You play as a student caught cheating, running away from an angry teacher. The game features increasing difficulty, power-ups, boss phases, and a competitive global leaderboard. -Unlike typical browser games, this project implements **anti-cheat architecture** usually found in multiplayer RTS or FPS games. The browser does not decide if you survived; the server does. +"Escape the Teacher" is a web game developed as a school project. You play as a student caught cheating, running away from an angry teacher. The game features increasing difficulty, power-ups, boss phases, and a competitive global leaderboard. + +Unlike typical browser games, this project implements **competitive multiplayer architecture** usually found in games like *Agar.io* or FPS titles. The browser is just a "dumb" terminal; the server simulates the entire world. ## **โœจ Features** ### **๐ŸŽฎ Gameplay** -* **Endless Progression:** The game speeds up over time. -* **Controls:** - * **Jump:** Space / Arrow Up / Tap / Left Click. - * **Crouch:** Arrow Down / Swipe Down (Mobile). -* **Power-Ups:** - * ๐Ÿ›ก๏ธ **Godmode:** Survives 3 hits. - * โšพ **Baseball Bat:** Eliminates the next teacher obstacle. - * ๐Ÿ‘Ÿ **Jumpboots:** Grants higher jumping power for a limited time. - * ๐Ÿ’ฐ **Coins:** Bonus points (visual score only, does not affect game speed). -* **Dynamic Backgrounds:** Environment changes as you progress. -* **Boss Phases:** Every 1500 ticks, special boss enemies spawn. - -### **๐Ÿ“ฑ Mobile First** - -* Fully responsive Canvas rendering (Letterboxing). -* Touch gestures (Swipe to crouch). -* "Rotate Device" enforcement for optimal gameplay. +* **Endless Progression:** The game speeds up over time. +* **Controls:** + * **Jump:** Space / Arrow Up / Tap / Left Click. + * **Crouch:** Arrow Down / Swipe Down (Mobile). +* **Power-Ups:** + * ๐Ÿ›ก๏ธ **Godmode:** Survives 3 hits. + * โšพ **Baseball Bat:** Eliminates the next teacher obstacle. + * ๐Ÿ‘Ÿ **Jumpboots:** Grants higher jumping power. + * ๐Ÿ’ฐ **Coins:** Bonus points. +* **Level Editor:** Custom chunks (platforms, enemies) can be designed in a secure Admin UI and are streamed into the game live. +* **Juice:** Particle effects and retro sound system. ### **๐Ÿ›ก๏ธ Security & Admin** -* **Admin Panel:** Password-protected (/admin) interface. -* **Moderation:** Review and delete leaderboard entries. -* **Badword Filter:** Automatic blocking of inappropriate names using Redis Sets. -* **Proof System:** Players receive an 8-character "Claim Code" to prove their high score to the teacher. +* **Server-Authoritative:** Physics runs on the server. Cheating (speedhack, godmode) is impossible. +* **Admin Panel:** Create levels, manage badwords, and moderate the leaderboard. +* **Proof System:** Players receive an 8-character "Claim Code" to prove their high score offline. ## **๐Ÿ—๏ธ Technical Architecture** -The core philosophy is **"Server-Authoritative, Client-Predicted"**. +We moved from a traditional HTTP-Request model to a **Realtime Streaming Architecture**. -1. **Frontend (JS):** Captures inputs and renders the game state immediately (Client-Side Prediction) to ensure zero input lag. It sends input logs to the server in chunks. -2. **Backend (Go):** Validates the inputs by re-simulating the game physics tick-by-tick. -3. **Database (Redis):** Stores active sessions, RNG states, and leaderboards (Sorted Sets). +1. **Backend (Go):** + * Runs the physics simulation at a fixed **20 TPS (Ticks Per Second)**. + * Generates level segments ("Chunks") 5 seconds into the future. + * Streams object positions via **WebSockets** to the client. +2. **Frontend (JS):** + * **Client-Side Prediction:** Inputs are applied immediately for zero-latency feel. + * **Buffering:** Incoming server data is buffered and played back smoothly. + * **Interpolation:** Although physics runs at 20 FPS, the game renders at 60+ FPS by interpolating positions (`lerp`). +3. **Database (Redis):** + * Stores active sessions, highscores, and custom level chunks. ### **Tech Stack** -* **Backend:** Go (Golang) 1.21+ -* **Frontend:** Vanilla JavaScript (Canvas API), CSS3 -* **Database:** Redis -* **Containerization:** Docker (Multi-Stage Build: Node Minifier \-\> Go Builder \-\> Alpine Runner) -* **Orchestration:** Kubernetes (Deployment, Service, Ingress) +* **Backend:** Go (Golang) 1.22+ (`gorilla/websocket`) +* **Frontend:** Vanilla JavaScript (Canvas API) +* **Database:** Redis +* **Containerization:** Docker (Multi-Stage Build) +* **Orchestration:** Kubernetes -## **๐Ÿ”ง Development Challenges & Solutions** +## **๐Ÿ”ง Engineering Challenges & Solutions** -Developing a cheat-proof game for the web came with significant technical hurdles. Here is how we solved them: +### **1. The "Netflix" Approach (Streaming vs. RNG)** +* **Problem:** Syncing Random Number Generators (RNG) between Client and Server caused "Butterfly Effects" where one wrong number broke the whole game state. +* **Solution:** **Streaming.** The client no longer generates anything. The server generates objects in the future and streams them into a buffer on the client. The client simply plays back what it receives. -### **1\. The "Butterfly Effect" (RNG Desynchronization)** +### **2. Lag Compensation & RTT** +* **Problem:** If the internet lags, the server's objects appear too late on the client, causing "Ghost Kills". +* **Solution:** **RTT (Round Trip Time) Measurement.** The client constantly measures the Ping. Incoming objects are visually shifted based on latency (`Latency * Speed`), so they appear exactly where they are on the server. -* **Problem:** JavaScript's Math.random() and Go's rand implementations are different. Even if seeded with the same number, they produce different sequences. This caused "Ghost Objects" (Server spawns a teacher, Client spawns a coin). -* **Solution:** We implemented a custom **Linear Congruential Generator (LCG)** in both languages. -* **The Tricky Part:** JavaScript uses 64-bit Floating Point numbers for everything, while Go uses strict types. Math operations drifted apart after a few hundred iterations. -* **Fix:** We forced JavaScript to use BigInt to simulate 32-bit integer overflows exactly like Go's uint32. +### **3. Low Tick-Rate & Interpolation** +* **Problem:** To save server CPU, we run physics at only **20 TPS**. This usually looks choppy (like a slideshow). +* **Solution:** **Linear Interpolation.** The rendering loop runs at 60/144 FPS and calculates the visual position between two physics ticks. The game looks buttery smooth despite low server load. -### **2\. Floating Point Drift & Spawning** - -* **Problem:** We initially spawned objects based on pixel positions (if x \< 800). Due to floating point precision errors, the client sometimes calculated 799.99 (Spawn\!) while the server calculated 800.01 (Wait\!). This desynchronized the RNG state immediately. -* **Solution:** **Tick-Based Spawning.** We decoupled spawning from spatial positions. The game now decides: *"The next object spawns at Tick 500"* rather than *"at Pixel 1200"*. Integers don't drift. - -### **3\. Logic-Score Coupling** - -* **Problem:** Initially, game speed and boss phases depended on the *Score*. When we added Coins (+2000 Points), the client's score jumped instantly, speeding up the game on the client side before the server acknowledged the coin pickup. This caused massive desyncs. -* **Solution:** We decoupled logic from the visual score. Game difficulty now depends strictly on **"Ticks Alive" (Time)**, which cannot be manipulated by picking up items. - -### **4\. Physics Tunneling (High Speed Collisions)** - -* **Problem:** At high speeds, the player could move 20 pixels per frame. If an obstacle was only 15 pixels wide, the player effectively "teleported" through it without triggering a collision. -* **Solution:** **Continuous Collision Detection (CCD).** We extend the hitbox of obstacles dynamically based on their current speed (width \+ speed). - -### **5\. "Ghost Hits" (The CCD Side Effect)** - -* **Problem:** The CCD fix caused a new issue where the extended hitbox would hit the player *after* they had already jumped over the obstacle (hitting them from behind). -* **Solution:** A **"Passed Check"**. Before checking collisions, we verify if the obstacle's right edge is already physically behind the player's left edge. If so, collision is ignored. +### **4. "Instant" Death** +* **Problem:** Waiting for the server to confirm "You died" feels laggy. +* **Solution:** **Optimistic Client Death.** The client detects collisions locally and stops the game visually (`Game Over`). It sends a `DEATH` signal to the server, which then validates and saves the highscore. ## **๐Ÿš€ Getting Started** ### **Using Docker (Recommended)** -This project includes a production-ready Dockerfile and docker-compose.yml. +1. **Clone the repository:** + ```bash + git clone [https://git.zb-server.de/ZB-Server/it232Abschied.git](https://git.zb-server.de/ZB-Server/it232Abschied.git) + cd it232Abschied + ``` -1. **Clone the repository:** - git clone \[https://git.zb-server.de/ZB-Server/it232Abschied.git\](https://git.zb-server.de/ZB-Server/it232Abschied.git) - cd it232Abschied +2. **Run:** + ```bash + docker-compose up --build -d + ``` -2. Configure Environment (Optional): - Edit docker-compose.yml to set your admin credentials: - environment: - \- ADMIN\_USER=teacher - \- ADMIN\_PASS=secret123 +3. **Play:** Open `http://localhost:8080` +4. **Admin:** Open `http://localhost:8080/admin` (User: `lehrer`, Pass: `geheim123`) -3. **Run:** - docker-compose up \--build \-d +### **Local Development** -4. **Play:** Open http://localhost:8080 - -### **Local Development (Go & Redis)** - -1. Start Redis: - docker run \-d \-p 6379:6379 redis:alpine - -2. Start the Server: - go run . - - *(Note: Use go run . to include all files, not just main.go)* +1. Start Redis: + ```bash + docker run -d -p 6379:6379 redis:alpine + ``` +2. Start the Server: + ```bash + go run . + ``` ## **๐Ÿ“‚ Project Structure** -. -โ”œโ”€โ”€ k8s/ \# Kubernetes manifests -โ”œโ”€โ”€ static/ \# Frontend files -โ”‚ โ”œโ”€โ”€ assets/ \# Images & Sprites -โ”‚ โ”œโ”€โ”€ fonts/ \# Local GDPR-compliant fonts -โ”‚ โ”œโ”€โ”€ js/ \# Modular Game Engine -โ”‚ โ”‚ โ”œโ”€โ”€ config.js \# Constants -โ”‚ โ”‚ โ”œโ”€โ”€ logic.js \# Physics & Collision -โ”‚ โ”‚ โ”œโ”€โ”€ network.js \# Server-Sync -โ”‚ โ”‚ โ””โ”€โ”€ ... -โ”‚ โ”œโ”€โ”€ index.html \# Entry Point -โ”‚ โ””โ”€โ”€ style.css \# Retro Design -โ”œโ”€โ”€ secure/ \# Protected Admin Files -โ”œโ”€โ”€ main.go \# Go Server Entrypoint -โ”œโ”€โ”€ simulation.go \# Server-side Physics Engine -โ”œโ”€โ”€ rng.go \# Deterministic RNG -โ”œโ”€โ”€ types.go \# Data Structures -โ”œโ”€โ”€ Dockerfile \# Multi-Stage Build -โ””โ”€โ”€ ... +``` +. +โ”œโ”€โ”€ k8s/ \# Kubernetes manifests +โ”œโ”€โ”€ static/ \# Frontend files +โ”‚ โ”œโ”€โ”€ assets/ \# Images & Audio +โ”‚ โ”œโ”€โ”€ js/ \# Game Engine +โ”‚ โ”‚ โ”œโ”€โ”€ audio.js \# Sound Manager +โ”‚ โ”‚ โ”œโ”€โ”€ logic.js \# Physics & Buffer Logic +โ”‚ โ”‚ โ”œโ”€โ”€ render.js \# Drawing & Interpolation +โ”‚ โ”‚ โ”œโ”€โ”€ network.js \# WebSocket & RTT Sync +โ”‚ โ”‚ โ””โ”€โ”€ ... +โ”‚ โ”œโ”€โ”€ index.html \# Entry Point +โ”‚ โ””โ”€โ”€ style.css \# Styling +โ”œโ”€โ”€ secure/ \# Protected Admin Files (Editor) +โ”œโ”€โ”€ main.go \# HTTP Routes & Setup +โ”œโ”€โ”€ websocket.go \# Game Loop & Streaming Logic +โ”œโ”€โ”€ simulation.go \# Physics Core +โ”œโ”€โ”€ types.go \# Data Structures +โ””โ”€โ”€ Dockerfile \# Multi-Stage Build +``` ## **๐Ÿ“œ Legal** This is a non-commercial educational project. +* **Privacy:** No tracking cookies. +* **Assets:** Font "Press Start 2P" hosted locally. Sounds generated via bfxr.net. -* **Privacy:** No tracking cookies are used. Highscores are stored in Redis; local scores in LocalStorage. -* **Assets:** Font "Press Start 2P" is hosted locally. - -**Good luck escaping\! ๐Ÿƒ๐Ÿ’จ** \ No newline at end of file +**Run for your grade! ๐Ÿƒ๐Ÿ’จ** diff --git a/config.go b/config.go index 5217b6e..14a04d8 100644 --- a/config.go +++ b/config.go @@ -63,25 +63,37 @@ func initGameConfig() { } log.Println("โœ… Config mit Powerups geladen") - loadChunksFromRedis() + defaultConfig.Chunks = loadChunksFromRedis() } -func loadChunksFromRedis() { - // Gleiche Logik wie im Handler, aber speichert es in die globale Variable +func loadChunksFromRedis() []ChunkDef { if rdb == nil { - return - } // Falls Redis noch nicht da ist - - ids, _ := rdb.SMembers(ctx, "config:chunks:list").Result() - sort.Strings(ids) // WICHTIG - - var chunks []ChunkDef - for _, id := range ids { - val, _ := rdb.Get(ctx, "config:chunks:data:"+id).Result() - var c ChunkDef - json.Unmarshal([]byte(val), &c) - chunks = append(chunks, c) + return []ChunkDef{} } - defaultConfig.Chunks = chunks - log.Printf("๐Ÿ“ฆ %d Custom Chunks geladen", len(chunks)) + + ids, err := rdb.SMembers(ctx, "config:chunks:list").Result() + if err != nil { + log.Println("Redis: Keine Chunks geladen") + return []ChunkDef{} + } + sort.Strings(ids) + + var loadedChunks []ChunkDef + + for _, id := range ids { + val, err := rdb.Get(ctx, "config:chunks:data:"+id).Result() + if err == nil { + var c ChunkDef + json.Unmarshal([]byte(val), &c) + c.ID = id + loadedChunks = append(loadedChunks, c) + } + } + + // Log nur beim Server-Start (wenn defaultConfig leer ist), sonst spammt es + if len(defaultConfig.Chunks) == 0 { + log.Printf("๐Ÿ“ฆ Lade %d Chunks aus Redis", len(loadedChunks)) + } + + return loadedChunks } diff --git a/simulation.go b/simulation.go index 0881768..53fb53b 100644 --- a/simulation.go +++ b/simulation.go @@ -33,6 +33,8 @@ type SimState struct { // Anti-Cheat LastJumpDist float64 SuspicionScore int + + Chunks []ChunkDef } // ============================================================================ diff --git a/websocket.go b/websocket.go index a422e07..5265db2 100644 --- a/websocket.go +++ b/websocket.go @@ -74,6 +74,7 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) { Ticks: 0, PosY: PlayerYBase, NextSpawnTick: 0, + Chunks: loadChunksFromRedis(), } // Channel grรถรŸer machen, damit bei Lag nichts blockiert @@ -246,10 +247,10 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac if tick >= s.NextSpawnTick { spawnX := SpawnXStart - chunkCount := len(defaultConfig.Chunks) + chunkCount := len(s.Chunks) if chunkCount > 0 && s.RNG.NextFloat() > 0.8 { idx := int(s.RNG.NextRange(0, float64(chunkCount))) - chunk := defaultConfig.Chunks[idx] + chunk := s.Chunks[idx] for _, p := range chunk.Platforms { createdPlats = append(createdPlats, ActivePlatform{X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height})