From abcee32d38cd14ca222d2da0ec2c45d28c7a743c Mon Sep 17 00:00:00 2001 From: Bhavnoor Singh Saroya Date: Mon, 25 Aug 2025 14:33:52 -0700 Subject: [PATCH] initial commit --- .dockerignore | 6 ++++ Dockerfile | 39 +++++++++++++++++++++++ README.md | 3 ++ fly.toml | 22 +++++++++++++ index.js | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 21 +++++++++++++ 6 files changed, 176 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 fly.toml create mode 100644 index.js create mode 100644 package.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..74340d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +/.git +/node_modules +.dockerignore +.env +Dockerfile +fly.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1779aa7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=22.13.1 +FROM node:${NODE_VERSION}-slim AS base + +LABEL fly_launch_runtime="Node.js" + +# Node.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Install node modules +COPY package-lock.json package.json ./ +RUN npm ci + +# Copy application code +COPY . . + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "npm", "run", "start" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..59ea1cb --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## Custom game server +Serve games to the appropriate gameboard service from postgres db +Very basic, threw this together in a day diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..c23c660 --- /dev/null +++ b/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for snake-server on 2025-08-18T22:02:23-07:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'snake-server' +primary_region = 'sea' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'suspend' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '512mb' + cpu_kind = 'shared' + cpus = 1 diff --git a/index.js b/index.js new file mode 100644 index 0000000..760483f --- /dev/null +++ b/index.js @@ -0,0 +1,85 @@ +import express from "express"; +import http from "http"; +import { WebSocketServer } from "ws"; +import cors from "cors"; +import pkg from "pg"; + +const { Pool } = pkg; + +const app = express(); +const PORT = 3000; + +// Setup Postgres connection pool +const pool = new Pool({ + connectionString: process.env.DATABASE_URL || "neverleavetheconnectionstringhereforproduction", +}); + +app.use(cors()); + +// HTTP GET /games/:id → return game info from DB +app.get("/games/:id", async (req, res) => { + const gameId = req.params.id; + try { + const result = await pool.query("SELECT info FROM items WHERE id = $1", [gameId]); + if (result.rows.length === 0) { + return res.redirect(302, "https://engine.battlesnake.com"); + // return res.status(404).json({ error: "Game not found" }); + + } + res.json(result.rows[0].info); // send back the JSONB field + } catch (err) { + console.error(err); + res.status(500).json({ error: "Database error" }); + } +}); + +// Create HTTP server and attach WebSocket server +const server = http.createServer(app); +const wss = new WebSocketServer({ server }); + +// Helper to match the path pattern +function matchGameEventsPath(url) { + if (!url) return null; + const match = url.match(/^\/games\/([^\/]+)\/events$/); + return match ? match[1] : null; +} + +wss.on("connection", async (ws, req) => { + const gameId = matchGameEventsPath(req.url); + if (!gameId) { + ws.close(); + return; + } + + console.log(`WS client connected for game: ${gameId}`); + + try { + // Grab frames array from DB + const result = await pool.query("SELECT frames FROM items WHERE id = $1", [gameId]); + if (result.rows.length === 0) { + ws.send(JSON.stringify({ error: "Game not found" })); + ws.close(); + return; + } + + const frames = result.rows[0].frames || []; + + // Stream frames one by one with delay + frames.forEach((frame, i) => { + setTimeout(() => { + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify(frame)); + } + }, i * 5); // staggered delay + }); + + } catch (err) { + console.error(err); + ws.send(JSON.stringify({ error: "Database error" })); + ws.close(); + } +}); + +server.listen(PORT, "0.0.0.0", () => { + console.log(`Server running at http://localhost:${PORT} (HTTP + WebSocket)`); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ab9ff35 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "game-server", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "cors": "^2.8.5", + "express": "^5.1.0", + "pg": "^8.16.3", + "ws": "^8.18.3" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.7.10" + } +}