feat: botfights v1 — full fighting game with Kaplay engine
- Vue 3 + Vite + Tailwind 4 frontend with synthwave aesthetic - Hono backend on port 9100 with SQLite/Drizzle - Procedural pixel-art sprite generator (48x48, 8 animation states) - Kaplay fight scene with punch/kick/special/knockback/KO animations - 12 mock bots across 6 tiers with Elo rating system - 9 challenge types, 10 fight arenas with modifiers - Fight replay with staggered battle log and ~1 min timing - Sprite preview page at /sprites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import * as schema from './schema.js'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { mkdirSync } from 'fs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const dataDir = join(__dirname, '..', '..', 'data')
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
|
||||
const sqlite = new Database(join(dataDir, 'botfights.db'))
|
||||
sqlite.pragma('journal_mode = WAL')
|
||||
sqlite.pragma('foreign_keys = ON')
|
||||
|
||||
export const db = drizzle(sqlite, { schema })
|
||||
export { schema }
|
||||
@@ -0,0 +1,67 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { mkdirSync } from 'fs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const dataDir = join(__dirname, '..', '..', 'data')
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
|
||||
const sqlite = new Database(join(dataDir, 'botfights.db'))
|
||||
sqlite.pragma('journal_mode = WAL')
|
||||
sqlite.pragma('foreign_keys = ON')
|
||||
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS bots (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
webhook_url TEXT NOT NULL,
|
||||
avatar_seed TEXT NOT NULL,
|
||||
secret_hash TEXT NOT NULL,
|
||||
public_key TEXT,
|
||||
elo_rating REAL NOT NULL DEFAULT 1200,
|
||||
wins INTEGER NOT NULL DEFAULT 0,
|
||||
losses INTEGER NOT NULL DEFAULT 0,
|
||||
win_streak INTEGER NOT NULL DEFAULT 0,
|
||||
best_streak INTEGER NOT NULL DEFAULT 0,
|
||||
tier INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fights (
|
||||
id TEXT PRIMARY KEY,
|
||||
bot_a_id TEXT NOT NULL REFERENCES bots(id),
|
||||
bot_b_id TEXT NOT NULL REFERENCES bots(id),
|
||||
arena TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'scheduled',
|
||||
winner_id TEXT REFERENCES bots(id),
|
||||
bot_a_hp INTEGER NOT NULL DEFAULT 100,
|
||||
bot_b_hp INTEGER NOT NULL DEFAULT 100,
|
||||
total_rounds INTEGER NOT NULL DEFAULT 0,
|
||||
scheduled_at TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rounds (
|
||||
id TEXT PRIMARY KEY,
|
||||
fight_id TEXT NOT NULL REFERENCES fights(id),
|
||||
round_number INTEGER NOT NULL,
|
||||
challenge_type TEXT NOT NULL,
|
||||
challenge_data TEXT NOT NULL,
|
||||
bot_a_response TEXT,
|
||||
bot_a_time_ms INTEGER,
|
||||
bot_a_score REAL,
|
||||
bot_b_response TEXT,
|
||||
bot_b_time_ms INTEGER,
|
||||
bot_b_score REAL,
|
||||
winner_id TEXT REFERENCES bots(id),
|
||||
narration TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
console.log('[botfights] database migrated')
|
||||
sqlite.close()
|
||||
@@ -0,0 +1,51 @@
|
||||
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const bots = sqliteTable('bots', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull().unique(),
|
||||
webhookUrl: text('webhook_url').notNull(),
|
||||
avatarSeed: text('avatar_seed').notNull(),
|
||||
secretHash: text('secret_hash').notNull(),
|
||||
publicKey: text('public_key'),
|
||||
eloRating: real('elo_rating').notNull().default(1200),
|
||||
wins: integer('wins').notNull().default(0),
|
||||
losses: integer('losses').notNull().default(0),
|
||||
winStreak: integer('win_streak').notNull().default(0),
|
||||
bestStreak: integer('best_streak').notNull().default(0),
|
||||
tier: integer('tier').notNull().default(0),
|
||||
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
|
||||
export const fights = sqliteTable('fights', {
|
||||
id: text('id').primaryKey(),
|
||||
botAId: text('bot_a_id').notNull().references(() => bots.id),
|
||||
botBId: text('bot_b_id').notNull().references(() => bots.id),
|
||||
arena: text('arena').notNull(),
|
||||
status: text('status', { enum: ['scheduled', 'live', 'finished', 'cancelled'] }).notNull().default('scheduled'),
|
||||
winnerId: text('winner_id').references(() => bots.id),
|
||||
botAHp: integer('bot_a_hp').notNull().default(100),
|
||||
botBHp: integer('bot_b_hp').notNull().default(100),
|
||||
totalRounds: integer('total_rounds').notNull().default(0),
|
||||
scheduledAt: text('scheduled_at'),
|
||||
startedAt: text('started_at'),
|
||||
endedAt: text('ended_at'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
|
||||
export const rounds = sqliteTable('rounds', {
|
||||
id: text('id').primaryKey(),
|
||||
fightId: text('fight_id').notNull().references(() => fights.id),
|
||||
roundNumber: integer('round_number').notNull(),
|
||||
challengeType: text('challenge_type').notNull(),
|
||||
challengeData: text('challenge_data').notNull(),
|
||||
botAResponse: text('bot_a_response'),
|
||||
botATimeMs: integer('bot_a_time_ms'),
|
||||
botAScore: real('bot_a_score'),
|
||||
botBResponse: text('bot_b_response'),
|
||||
botBTimeMs: integer('bot_b_time_ms'),
|
||||
botBScore: real('bot_b_score'),
|
||||
winnerId: text('winner_id').references(() => bots.id),
|
||||
narration: text('narration'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
Reference in New Issue
Block a user