import pg from "pg"; import parse from "pg-connection-string"; import { createInitialBotState } from "./bot-state.js"; const { Pool } = pg; const connectionString = process.env.DATABASE_URL || "postgres://localhost/builazoo"; const parsed = parse(connectionString); const poolConfig = { host: parsed.host || "localhost", port: Number(parsed.port) || 5432, database: parsed.database || "builazoo", user: parsed.user, password: typeof parsed.password === "string" ? parsed.password : "", }; if (process.env.PGPASSWORD !== undefined && process.env.PGPASSWORD !== null) poolConfig.password = String(process.env.PGPASSWORD); const pool = new Pool(poolConfig); /** * @returns {Promise<{ mapWidth: number, mapHeight: number, minZoos: number }>} */ export async function getMapParams() { const res = await pool.query( "SELECT value FROM map_config WHERE key = 'params'" ); const row = res.rows[0]; if (!row) { return { mapWidth: 100, mapHeight: 100, minZoos: 5 }; } const v = row.value; return { mapWidth: Number(v?.mapWidth) || 100, mapHeight: Number(v?.mapHeight) || 100, minZoos: Number(v?.minZoos) || 5, }; } /** * @param {string} publicKey * @returns {Promise<{ id: string, pseudo: string } | null>} */ export async function getAccountByPublicKey(publicKey) { const res = await pool.query( "SELECT id, pseudo FROM accounts WHERE public_key = $1", [publicKey] ); const row = res.rows[0]; if (!row) return null; return { id: row.id, pseudo: row.pseudo }; } /** * @param {string} publicKey * @param {string} pseudo * @returns {Promise<{ id: string, pseudo: string }>} */ export async function createAccount(publicKey, pseudo) { const res = await pool.query( "INSERT INTO accounts (public_key, pseudo) VALUES ($1, $2) RETURNING id, pseudo", [publicKey, pseudo] ); const row = res.rows[0]; return { id: row.id, pseudo: row.pseudo }; } /** * @param {string} accountId * @returns {Promise} */ export async function updateLastSeen(accountId) { await pool.query( "UPDATE accounts SET last_seen_at = now() WHERE id = $1", [accountId] ); } /** * @param {string} accountId * @returns {Promise<{ id: string, name: string, x: number, y: number, is_bot: boolean, animal_weights: object, game_state: object | null } | null>} */ export async function getZooByAccountId(accountId) { const res = await pool.query( "SELECT id, name, x, y, is_bot, animal_weights, game_state FROM zoos WHERE account_id = $1", [accountId] ); const row = res.rows[0]; if (!row) return null; return { id: row.id, name: row.name, x: Number(row.x), y: Number(row.y), is_bot: row.is_bot, animal_weights: row.animal_weights || {}, game_state: row.game_state, }; } /** * Common zoo row fields: id, name, x, y with numeric coords. * @param {Record} row * @returns {{ id: string, name: string, x: number, y: number }} */ function mapZooRowBase(row) { return { id: row.id, name: row.name, x: Number(row.x), y: Number(row.y), }; } /** * @returns {Promise>} */ export async function getAllZoos() { const res = await pool.query( "SELECT id, name, x, y, animal_weights, game_state FROM zoos ORDER BY is_bot, name" ); return res.rows.map((row) => ({ ...mapZooRowBase(row), animal_weights: row.animal_weights || {}, game_state: row.game_state ?? null, })); } /** * @param {{ accountId: string, name: string, x: number, y: number, gameState: object }} opts * @returns {Promise<{ id: string }>} */ export async function createZoo(opts) { const { accountId, name, x, y, gameState } = opts; const res = await pool.query( "INSERT INTO zoos (account_id, name, x, y, is_bot, animal_weights, game_state) VALUES ($1, $2, $3, $4, false, $5, $6) RETURNING id", [accountId, name, x, y, "{}", gameState] ); return { id: res.rows[0].id }; } /** * @param {string} zooId * @returns {Promise<{ id: string, name: string, x: number, y: number, is_bot: boolean, account_id: string | null, animal_weights: object, game_state: object | null } | null>} */ export async function getZooById(zooId) { const res = await pool.query( "SELECT id, name, x, y, is_bot, account_id, animal_weights, game_state FROM zoos WHERE id = $1", [zooId] ); const row = res.rows[0]; if (!row) return null; return { ...mapZooRowBase(row), is_bot: row.is_bot, account_id: row.account_id, animal_weights: row.animal_weights || {}, game_state: row.game_state, }; } /** * @param {string} zooId * @param {object} gameState * @returns {Promise} */ export async function updateZooGameState(zooId, gameState) { await pool.query( "UPDATE zoos SET game_state = $1, updated_at = now() WHERE id = $2", [JSON.stringify(gameState), zooId] ); } /** * @returns {Promise} */ export async function countPlayerZoos() { const res = await pool.query( "SELECT COUNT(*) AS n FROM zoos WHERE is_bot = false" ); return Number(res.rows[0]?.n) || 0; } /** * @param {number} x * @param {number} y * @param {object} animalWeights * @returns {Promise} zoo id */ export async function createBotZoo(x, y, animalWeights) { const gameState = createInitialBotState(); const res = await pool.query( "INSERT INTO zoos (account_id, name, x, y, is_bot, animal_weights, game_state) VALUES (NULL, $1, $2, $3, true, $4, $5) RETURNING id", [`Zoo bot ${x.toFixed(0)}-${y.toFixed(0)}`, x, y, JSON.stringify(animalWeights), JSON.stringify(gameState)] ); return res.rows[0].id; } /** * Load bot zoos for server-side tick (id, name, x, y, animal_weights, game_state). * @returns {Promise>} */ export async function getBotZoosForTick() { const res = await pool.query( "SELECT id, name, x, y, animal_weights, game_state FROM zoos WHERE is_bot = true" ); return res.rows.map((row) => ({ ...mapZooRowBase(row), animalWeights: row.animal_weights || {}, botState: row.game_state || createInitialBotState(), })); } /** * Persist bot zoo state after tick. * @param {string} zooId * @param {object} animalWeights * @param {object} gameState * @returns {Promise} */ export async function updateBotZooState(zooId, animalWeights, gameState) { await pool.query( "UPDATE zoos SET animal_weights = $1, game_state = $2, updated_at = now() WHERE id = $3 AND is_bot = true", [JSON.stringify(animalWeights), JSON.stringify(gameState), zooId] ); } export { pool };