Add Democratic fundraising platform.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
228
server.ts
Normal file
228
server.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { createServer } from "http";
|
||||
import { parse } from "url";
|
||||
import next from "next";
|
||||
import { Server as SocketIOServer } from "socket.io";
|
||||
import { prisma } from "./src/lib/prisma";
|
||||
import { debitForBet, creditForWin, refundBet } from "./src/lib/game-ledger";
|
||||
import { generateServerSeed, deriveInt } from "./src/lib/provably-fair";
|
||||
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const app = next({ dev, hostname: "0.0.0.0", port: 8008 });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
const HOUSE_CUT = 0.05; // 5% on PvP
|
||||
|
||||
app.prepare().then(() => {
|
||||
const httpServer = createServer((req, res) => {
|
||||
const parsedUrl = parse(req.url ?? "/", true);
|
||||
handle(req, res, parsedUrl);
|
||||
});
|
||||
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
cors: { origin: "*" },
|
||||
path: "/api/socket",
|
||||
});
|
||||
|
||||
// ── COIN FLIP ──────────────────────────────────────────────────────────────
|
||||
const coinFlipNS = io.of("/coin-flip");
|
||||
|
||||
coinFlipNS.on("connection", (socket) => {
|
||||
socket.on("join_room", async ({ roomId, userId }: { roomId: string; userId: string }) => {
|
||||
const room = await prisma.gameRoom.findUnique({ where: { id: roomId } });
|
||||
if (!room || room.status !== "WAITING") {
|
||||
socket.emit("error", "Room not available");
|
||||
return;
|
||||
}
|
||||
if (room.creatorId === userId) {
|
||||
socket.join(roomId);
|
||||
socket.emit("waiting", { roomId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check balance
|
||||
const wallet = await prisma.wallet.findUnique({ where: { userId } });
|
||||
if (!wallet || wallet.balanceCredits < room.wageBLW) {
|
||||
socket.emit("error", "Insufficient balance");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debit joiner
|
||||
try {
|
||||
await debitForBet(userId, room.wageBLW, "COIN_FLIP");
|
||||
} catch {
|
||||
socket.emit("error", "Debit failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debit creator
|
||||
try {
|
||||
await debitForBet(room.creatorId, room.wageBLW, "COIN_FLIP");
|
||||
} catch {
|
||||
await refundBet(userId, room.wageBLW, "COIN_FLIP");
|
||||
socket.emit("error", "Creator debit failed");
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.gameRoom.update({
|
||||
where: { id: roomId },
|
||||
data: { joinerId: userId, status: "ACTIVE" },
|
||||
});
|
||||
|
||||
socket.join(roomId);
|
||||
coinFlipNS.to(roomId).emit("game_start", { roomId });
|
||||
|
||||
// Resolve
|
||||
const serverSeed = generateServerSeed();
|
||||
const result = deriveInt(serverSeed, roomId, 0, 0, 1); // 0 = heads, 1 = tails
|
||||
const winnerId = result === 0 ? room.creatorId : userId;
|
||||
const pot = room.wageBLW * 2;
|
||||
const payout = Math.floor(pot * (1 - HOUSE_CUT));
|
||||
|
||||
await creditForWin(winnerId, payout, "COIN_FLIP");
|
||||
|
||||
await prisma.gameRoom.update({
|
||||
where: { id: roomId },
|
||||
data: {
|
||||
status: "RESOLVED",
|
||||
resultData: { result, winnerId, serverSeed, payout },
|
||||
},
|
||||
});
|
||||
|
||||
coinFlipNS.to(roomId).emit("result", {
|
||||
result,
|
||||
resultLabel: result === 0 ? "heads" : "tails",
|
||||
winnerId,
|
||||
payout,
|
||||
serverSeed,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── PONG ───────────────────────────────────────────────────────────────────
|
||||
const pongNS = io.of("/pong");
|
||||
const pongRooms = new Map<string, {
|
||||
roomId: string;
|
||||
creatorId: string;
|
||||
joinerId: string | null;
|
||||
wageBLW: number;
|
||||
ball: { x: number; y: number; vx: number; vy: number };
|
||||
paddles: { creator: number; joiner: number };
|
||||
scores: { creator: number; joiner: number };
|
||||
interval: ReturnType<typeof setInterval> | null;
|
||||
}>();
|
||||
|
||||
pongNS.on("connection", (socket) => {
|
||||
socket.on("join_room", async ({ roomId, userId }: { roomId: string; userId: string }) => {
|
||||
const room = await prisma.gameRoom.findUnique({ where: { id: roomId } });
|
||||
if (!room || !["WAITING", "ACTIVE"].includes(room.status)) {
|
||||
socket.emit("error", "Room not available");
|
||||
return;
|
||||
}
|
||||
|
||||
socket.join(roomId);
|
||||
|
||||
if (room.creatorId === userId) {
|
||||
if (!pongRooms.has(roomId)) {
|
||||
pongRooms.set(roomId, {
|
||||
roomId,
|
||||
creatorId: userId,
|
||||
joinerId: null,
|
||||
wageBLW: room.wageBLW,
|
||||
ball: { x: 400, y: 200, vx: 3, vy: 2 },
|
||||
paddles: { creator: 180, joiner: 180 },
|
||||
scores: { creator: 0, joiner: 0 },
|
||||
interval: null,
|
||||
});
|
||||
}
|
||||
socket.emit("waiting", { roomId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Joiner
|
||||
const state = pongRooms.get(roomId);
|
||||
if (!state || state.joinerId) { socket.emit("error", "Room full"); return; }
|
||||
|
||||
const wallet = await prisma.wallet.findUnique({ where: { userId } });
|
||||
if (!wallet || wallet.balanceCredits < room.wageBLW) { socket.emit("error", "Insufficient balance"); return; }
|
||||
|
||||
try {
|
||||
await debitForBet(userId, room.wageBLW, "PONG");
|
||||
await debitForBet(room.creatorId, room.wageBLW, "PONG");
|
||||
} catch {
|
||||
await refundBet(userId, room.wageBLW, "PONG");
|
||||
socket.emit("error", "Debit failed"); return;
|
||||
}
|
||||
|
||||
state.joinerId = userId;
|
||||
await prisma.gameRoom.update({ where: { id: roomId }, data: { joinerId: userId, status: "ACTIVE" } });
|
||||
pongNS.to(roomId).emit("game_start", { roomId, creatorId: room.creatorId, joinerId: userId });
|
||||
|
||||
// Game loop — 20 ticks/sec
|
||||
state.interval = setInterval(() => {
|
||||
const s = pongRooms.get(roomId);
|
||||
if (!s) return;
|
||||
|
||||
s.ball.x += s.ball.vx;
|
||||
s.ball.y += s.ball.vy;
|
||||
|
||||
// Wall bounce
|
||||
if (s.ball.y <= 0 || s.ball.y >= 400) s.ball.vy *= -1;
|
||||
|
||||
// Paddle bounce
|
||||
if (s.ball.x <= 20 && Math.abs(s.ball.y - s.paddles.creator) < 50) s.ball.vx = Math.abs(s.ball.vx);
|
||||
if (s.ball.x >= 780 && Math.abs(s.ball.y - s.paddles.joiner) < 50) s.ball.vx = -Math.abs(s.ball.vx);
|
||||
|
||||
// Score
|
||||
if (s.ball.x <= 0) {
|
||||
s.scores.joiner++;
|
||||
s.ball = { x: 400, y: 200, vx: 3, vy: 2 };
|
||||
}
|
||||
if (s.ball.x >= 800) {
|
||||
s.scores.creator++;
|
||||
s.ball = { x: 400, y: 200, vx: -3, vy: 2 };
|
||||
}
|
||||
|
||||
pongNS.to(roomId).emit("tick", { ball: s.ball, paddles: s.paddles, scores: s.scores });
|
||||
|
||||
// Win condition: first to 5
|
||||
if (s.scores.creator >= 5 || s.scores.joiner >= 5) {
|
||||
clearInterval(s.interval!);
|
||||
s.interval = null;
|
||||
const winnerId = s.scores.creator >= 5 ? s.creatorId : s.joinerId!;
|
||||
const pot = s.wageBLW * 2;
|
||||
const payout = Math.floor(pot * (1 - HOUSE_CUT));
|
||||
creditForWin(winnerId, payout, "PONG").then(() => {
|
||||
prisma.gameRoom.update({
|
||||
where: { id: roomId },
|
||||
data: { status: "RESOLVED", resultData: { winnerId, scores: s.scores, payout } },
|
||||
}).catch(() => {});
|
||||
}).catch(() => {});
|
||||
pongNS.to(roomId).emit("game_over", { winnerId, scores: s.scores, payout });
|
||||
pongRooms.delete(roomId);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
socket.on("paddle_move", ({ roomId, userId, y }: { roomId: string; userId: string; y: number }) => {
|
||||
const state = pongRooms.get(roomId);
|
||||
if (!state) return;
|
||||
const clampedY = Math.max(0, Math.min(360, y));
|
||||
if (state.creatorId === userId) state.paddles.creator = clampedY;
|
||||
else if (state.joinerId === userId) state.paddles.joiner = clampedY;
|
||||
});
|
||||
});
|
||||
|
||||
// Expire abandoned rooms every minute
|
||||
setInterval(async () => {
|
||||
await prisma.gameRoom.updateMany({
|
||||
where: { status: "WAITING", expiresAt: { lt: new Date() } },
|
||||
data: { status: "EXPIRED" },
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
httpServer.listen(8008, "0.0.0.0", () => {
|
||||
console.log(`> Ready on http://0.0.0.0:8008`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user