← Zurück zur Übersicht Dein eigenes OGame: Baue ein textbasiertes Idle-Game im Browser

Dein eigenes OGame: Baue ein textbasiertes Idle-Game im Browser

[WERBUNG: CONTENT OBEN]

Dein eigenes OGame: Baue ein textbasiertes Idle-Game im Browser

Erinnerst du dich an OGame? Minen bauen, die pro Sekunde Ressourcen generieren, dann die Erträge in bessere Gebäude investieren – Suchtfaktor pur. Das Beste daran: Die Spielmechanik dahinter ist eigentlich sehr simpel. Kein Canvas, kein WebGL – nur cleveres State Management und ein Timer.

In diesem Tutorial bauen wir den soliden Grundstein für genau so ein Spiel. Am Ende hast du ein lauffähiges Idle-Game mit einer Währung (Credits), drei Gebäuden und einem persistenten Spielstand via localStorage.


🎮 Das Spielkonzept

Feature Details
Währung Credits (1 Typ)
Gebäude 3 Stück: Metallmine, Solarkraftwerk, Forschungslabor
Mechanik Jedes Gebäude generiert Credits pro Sekunde. Upgrades erhöhen den Ertrag, kosten aber exponentiell mehr.
Persistenz Spielstand wird in localStorage gespeichert und beim nächsten Besuch geladen.

🛠️ Los geht's: Projekt aufsetzen

mkdir my-idle-game && cd my-idle-game
touch index.html

Das gesamte Spiel besteht aus einer einzigen Datei. Das macht es perfekt für Anfänger und erlaubt es, den Code in einem Tutorial kompakt zu halten.


Der komplette Code

my-idle-game/index.html:

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Idle Colony</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            font-family: 'Courier New', monospace;
            background: #0a0e17;
            color: #c8d6e5;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #game {
            width: 100%;
            max-width: 500px;
            padding: 2rem;
        }
        h1 {
            color: #f0a500;
            text-align: center;
            margin-bottom: 0.25rem;
            letter-spacing: 2px;
        }
        .subtitle {
            text-align: center;
            color: #576574;
            font-size: 0.8rem;
            margin-bottom: 2rem;
        }
        .resource-bar {
            background: #141a2a;
            border: 1px solid #f0a500;
            border-radius: 8px;
            padding: 1rem 1.5rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 2rem;
        }
        .resource-bar span { font-size: 0.85rem; color: #576574; }
        .resource-bar strong { color: #f0a500; font-size: 1.4rem; }
        .per-sec { font-size: 0.75rem; color: #10ac84; }
        .building {
            background: #141a2a;
            border: 1px solid #222f3e;
            border-radius: 8px;
            padding: 1.25rem;
            margin-bottom: 1rem;
            transition: border-color 0.3s;
        }
        .building:hover { border-color: #f0a500; }
        .building-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 0.5rem;
        }
        .building-name { font-weight: bold; color: #dfe6e9; }
        .building-level { color: #f0a500; font-size: 0.85rem; }
        .building-info {
            font-size: 0.8rem;
            color: #576574;
            margin-bottom: 0.75rem;
        }
        .building-stats {
            display: flex;
            justify-content: space-between;
            font-size: 0.8rem;
            color: #576574;
            margin-bottom: 0.75rem;
        }
        button {
            width: 100%;
            padding: 0.6rem;
            background: #f0a500;
            color: #0a0e17;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
            font-family: inherit;
            font-size: 0.85rem;
            transition: all 0.2s;
        }
        button:hover {
            background: #e58e26;
            transform: translateY(-1px);
        }
        button:disabled {
            background: #222f3e;
            color: #576574;
            cursor: not-allowed;
            transform: none;
        }
        .reset {
            margin-top: 2rem;
            background: transparent;
            border: 1px solid #576574;
            color: #576574;
            font-size: 0.75rem;
        }
        .reset:hover {
            border-color: #ee5253;
            color: #ee5253;
            background: transparent;
        }
    </style>
</head>
<body>
    <div id="game">
        <h1>⛏️ IDLE COLONY</h1>
        <p class="subtitle">Textbasiertes Ressourcen-Management</p>

        <div class="resource-bar">
            <div>
                <span>Credits</span><br>
                <strong id="credits">0</strong>
            </div>
            <div class="per-sec">+<span id="cps">0</span> / Sek.</div>
        </div>

        <div id="buildings"></div>

        <button class="reset" onclick="resetGame()">Spielstand zurücksetzen</button>
    </div>

    <script>
    // === GAME CONFIG ===
    const BUILDINGS = [
        {
            id: 'mine',
            name: '⛏️ Metallmine',
            baseCost: 10,
            baseOutput: 1,
            costMultiplier: 1.5,
            desc: 'Gräbt Erz und wandelt es in Credits um.'
        },
        { id: 'solar',  name: '☀️ Solarkraftwerk',    baseCost: 50,  baseOutput: 4,   costMultiplier: 1.6, desc: 'Erzeugt Energie, die als Credits verkauft wird.' },
        { id: 'lab',    name: '🔬 Forschungslabor',   baseCost: 200, baseOutput: 15,  costMultiplier: 1.8, desc: 'Entwickelt Technologien für hohe Gewinne.' },
    ];

    // === GAME STATE ===
    let state = loadGame();

    function defaultState() {
        const levels = {};
        BUILDINGS.forEach(b => levels[b.id] = 0);
        return { credits: 0, levels };
    }

    function loadGame() {
        try {
            const saved = JSON.parse(localStorage.getItem('idleColony'));
            if (saved && saved.levels) return saved;
        } catch(e) {}
        return defaultState();
    }

    function saveGame() {
        localStorage.setItem('idleColony', JSON.stringify(state));
    }

    // === GAME LOGIC ===
    function getCost(building) {
        return Math.floor(building.baseCost * Math.pow(building.costMultiplier, state.levels[building.id]));
    }

    function getOutput(building) {
        return building.baseOutput * state.levels[building.id];
    }

    function getTotalCPS() {
        return BUILDINGS.reduce((sum, b) => sum + getOutput(b), 0);
    }

    function buyBuilding(buildingId) {
        const building = BUILDINGS.find(b => b.id === buildingId);
        const cost = getCost(building);
        if (state.credits >= cost) {
            state.credits -= cost;
            state.levels[building.id]++;
            saveGame();
            render();
        }
    }

    function resetGame() {
        if (confirm('Spielstand wirklich löschen?')) {
            state = defaultState();
            saveGame();
            render();
        }
    }

    // === RENDERING ===
    function render() {
        document.getElementById('credits').textContent = Math.floor(state.credits).toLocaleString('de-DE');
        document.getElementById('cps').textContent = getTotalCPS();

        const container = document.getElementById('buildings');
        container.innerHTML = BUILDINGS.map(b => {
            const level = state.levels[b.id];
            const cost = getCost(b);
            const output = getOutput(b);
            const canBuy = state.credits >= cost;

            return `
                <div class="building">
                    <div class="building-header">
                        <span class="building-name">${b.name}</span>
                        <span class="building-level">Lv. ${level}</span>
                    </div>
                    <div class="building-info">${b.desc}</div>
                    <div class="building-stats">
                        <span>Ertrag: +${output} /Sek.</span>
                        <span>Nächstes Lv.: +${b.baseOutput} /Sek.</span>
                    </div>
                    <button onclick="buyBuilding('${b.id}')" ${canBuy ? '' : 'disabled'}>
                        Upgrade → ${cost.toLocaleString('de-DE')} Credits
                    </button>
                </div>`;
        }).join('');
    }

    // === GAME LOOP (1x pro Sekunde) ===
    setInterval(() => {
        state.credits += getTotalCPS();
        saveGame();
        render();
    }, 1000);

    // Initial Render
    render();
    </script>
</body>
</html>

🧠 Die Kernmechanik erklärt

Der Game Loop

Das Herzstück ist ein simpler setInterval der jede Sekunde läuft:

  1. Berechne den gesamten Ertrag aller Gebäude (getTotalCPS).
  2. Addiere den Ertrag auf die Kredite.
  3. Speichere den State und rendere die UI neu.

Exponentielle Kosten

Das ist der Trick, der das Spiel "balanciert". Die Formel für die Gebäudekosten lautet:

Kosten = Basiskosten × Multiplikator ^ Level

Eine Metallmine kostet z.B.:

  • Level 1: 10 × 1.5^0 = 10 Credits
  • Level 5: 10 × 1.5^4 = 50 Credits
  • Level 10: 10 × 1.5^9 = 384 Credits

Das sorgt dafür, dass der Spieler immer wieder vor der Entscheidung steht: "Upgrade ich meine billige Mine, oder spare ich auf das teure Labor?"

Persistenz

Der gesamte Spielstand (Credits + alle Gebäude-Level) wird bei jeder Änderung automatisch in localStorage geschrieben. Beim Laden der Seite wird er restauriert. So kann der Spieler jederzeit den Browser schließen und weiterspielen.


🚀 Sofort ausprobieren mit Node.js & Docker

Wir nutzen bewusst Express statt eines statischen Webservers. So kannst du später problemlos eine API für Leaderboards, serverseitige Spielstände oder Multiplayer-Funktionen ergänzen.

my-idle-game/package.json:

{
  "name": "my-idle-game",
  "version": "1.0.0",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.18.2" }
}

my-idle-game/server.js:

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Idle Colony läuft auf http://localhost:${PORT}`));

Verschiebe dein Spiel in einen public/-Ordner:

mkdir public
mv index.html public/
npm install

my-idle-game/docker-compose.yml:

version: '3.8'
services:
  app:
    image: node:18-alpine
    working_dir: /app
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    command: sh -c "npm install && npm start"
docker compose up -d

Öffne http://localhost:3000 im Browser – und fange an zu minen! ⛏️

⚠️ 📸 SCREENSHOT ANFRAGE: Hier einen Screenshot des laufenden Spiels einfügen, am besten mit einigen bereits aufgelevelten Gebäuden und einem sichtbaren Credits-Zähler.


📈 Erweiterungs-Ideen

Du hast jetzt den Grundbaustein. Hier ist, wie man es weiter ausbauen kann:

Feature Schwierigkeit Beschreibung
Offline-Erträge ⭐⭐ Beim Laden die vergangene Zeit berechnen und Credits gutschreiben
Mehrere Ressourcen ⭐⭐⭐ Metall, Energie, Forschungspunkte – jedes Gebäude produziert etwas anderes
Achievements ⭐⭐ "Erste Mine gebaut", "1.000.000 Credits erreicht"
Prestige-System ⭐⭐⭐⭐ Alles zurücksetzen für einen permanenten Bonus

Fazit

Mit weniger als 200 Zeilen Code hast du ein funktionsfähiges Idle-Game gebaut, das die gleichen Kernmechaniken nutzt wie OGame, Cookie Clicker oder Factorio. Die Schönheit liegt in der Einfachheit: Ein Timer, ein State-Objekt und eine Render-Funktion – mehr braucht es nicht, um Suchtpotenzial zu erzeugen.

Viel Spaß beim Minen! ⛏️

[WERBUNG: CONTENT UNTEN]