@@ -15,11 +15,29 @@ import {
1515/* =========================
1616 Enums
1717 ========================= */
18- export const relationship_type_enum = pgEnum ( 'relationship_type' , [ 'FOLLOW' , 'FRIEND' ] ) ;
19- export const target_type_enum = pgEnum ( 'target_type' , [ 'CODE' , 'IMAGE' , 'VIDEO' ] ) ;
20- export const battle_status_enum = pgEnum ( 'battle_status' , [ 'PENDING' , 'ACTIVE' , 'COMPLETED' ] ) ;
18+ export const relationship_type_enum = pgEnum ( 'relationship_type' , [
19+ 'FOLLOW' ,
20+ 'FRIEND'
21+ ] ) ;
22+ export const target_type_enum = pgEnum ( 'target_type' , [
23+ 'CODE' ,
24+ 'IMAGE' ,
25+ 'VIDEO'
26+ ] ) ;
27+ export const battle_status_enum = pgEnum ( 'battle_status' , [
28+ 'PENDING' ,
29+ 'ACTIVE' ,
30+ 'COMPLETED'
31+ ] ) ;
2132export const visibility_enum = pgEnum ( 'visibility' , [ 'PUBLIC' , 'PRIVATE' ] ) ;
22- export const battle_type_enum = pgEnum ( 'battle_type' , [ 'TIME_TRIAL' , 'TIMED_MATCH' ] ) ;
33+ export const battle_type_enum = pgEnum ( 'battle_type' , [
34+ 'TIME_TRIAL' ,
35+ 'TIMED_MATCH'
36+ ] ) ;
37+ export const win_condition_enum = pgEnum ( 'win_condition' , [
38+ 'VOTING' , // Traditional: battle ends, then voting determines winner
39+ 'FIRST_TO_PERFECT' // Race: first to 100% diff_score wins instantly
40+ ] ) ;
2341export const participant_status_enum = pgEnum ( 'participant_status' , [
2442 'PENDING' ,
2543 'READY' ,
@@ -28,8 +46,16 @@ export const participant_status_enum = pgEnum('participant_status', [
2846 'FINISHED'
2947] ) ;
3048
31- export const user_award_enum = pgEnum ( 'user_award' , [ 'MOST_ACCURATE' , 'REAL_WORLD' , 'BEST_FEEL' ] ) ;
32- export const award_outcome_enum = pgEnum ( 'award_outcome' , [ 'AWARDED' , 'VOID_TIE' , 'VOID_OTHER' ] ) ;
49+ export const user_award_enum = pgEnum ( 'user_award' , [
50+ 'MOST_ACCURATE' ,
51+ 'REAL_WORLD' ,
52+ 'BEST_FEEL'
53+ ] ) ;
54+ export const award_outcome_enum = pgEnum ( 'award_outcome' , [
55+ 'AWARDED' ,
56+ 'VOID_TIE' ,
57+ 'VOID_OTHER'
58+ ] ) ;
3359export const hax_type_enum = pgEnum ( 'hax_type' , [ 'BATTLE' , 'SOLO' ] ) ;
3460
3561/* =========================
@@ -38,6 +64,7 @@ export const hax_type_enum = pgEnum('hax_type', ['BATTLE', 'SOLO']);
3864export const user = pgTable ( 'user' , {
3965 id : text ( 'id' ) . primaryKey ( ) ,
4066 name : text ( ) . notNull ( ) ,
67+ username : text ( ) ,
4168 email : text ( ) . notNull ( ) . unique ( ) ,
4269 emailVerified : boolean ( 'email_verified' )
4370 . $defaultFn ( ( ) => false )
@@ -96,8 +123,12 @@ export const verification = pgTable('verification', {
96123 identifier : text ( 'identifier' ) . notNull ( ) ,
97124 value : text ( 'value' ) . notNull ( ) ,
98125 expiresAt : timestamp ( 'expires_at' ) . notNull ( ) ,
99- createdAt : timestamp ( 'created_at' ) . $defaultFn ( ( ) => /* @__PURE__ */ new Date ( ) ) ,
100- updatedAt : timestamp ( 'updated_at' ) . $defaultFn ( ( ) => /* @__PURE__ */ new Date ( ) )
126+ createdAt : timestamp ( 'created_at' ) . $defaultFn (
127+ ( ) => /* @__PURE__ */ new Date ( )
128+ ) ,
129+ updatedAt : timestamp ( 'updated_at' ) . $defaultFn (
130+ ( ) => /* @__PURE__ */ new Date ( )
131+ )
101132} ) ;
102133
103134/* =========================
@@ -114,11 +145,20 @@ export const user_relationships = pgTable(
114145 . notNull ( )
115146 . references ( ( ) => user . id , { onDelete : 'cascade' } ) ,
116147 type : relationship_type_enum ( 'type' ) . notNull ( ) ,
117- created_at : timestamp ( 'created_at' , { withTimezone : true } ) . notNull ( ) . defaultNow ( )
148+ created_at : timestamp ( 'created_at' , { withTimezone : true } )
149+ . notNull ( )
150+ . defaultNow ( )
118151 } ,
119152 ( t ) => [
120- uniqueIndex ( 'user_relationships_unique' ) . on ( t . user_id , t . related_user_id , t . type ) ,
121- check ( 'user_relationships_no_self' , sql `${ t . user_id } <> ${ t . related_user_id } ` )
153+ uniqueIndex ( 'user_relationships_unique' ) . on (
154+ t . user_id ,
155+ t . related_user_id ,
156+ t . type
157+ ) ,
158+ check (
159+ 'user_relationships_no_self' ,
160+ sql `${ t . user_id } <> ${ t . related_user_id } `
161+ )
122162 ]
123163) ;
124164
@@ -166,11 +206,15 @@ export const battles = pgTable(
166206 visibility : visibility_enum ( ) . notNull ( ) . default ( 'PRIVATE' ) ,
167207 zero_room_id : text ( ) . notNull ( ) ,
168208 type : battle_type_enum ( ) . notNull ( ) ,
209+ win_condition : win_condition_enum ( ) . notNull ( ) . default ( 'FIRST_TO_PERFECT' ) ,
169210 total_time_seconds : integer ( ) . notNull ( ) ,
170211 overtime_seconds : integer ( ) ,
171212 starts_at : timestamp ( { withTimezone : true } ) ,
172213 ends_at : timestamp ( { withTimezone : true } ) ,
214+ allow_time_extension : boolean ( ) . notNull ( ) . default ( true ) ,
173215 revealed_at : timestamp ( { withTimezone : true } ) ,
216+ // Winner tracking (for FIRST_TO_PERFECT mode) - stores the hax.id of the winner
217+ winner_hax_id : uuid ( ) ,
174218 created_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( ) ,
175219 updated_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( )
176220 } ,
@@ -229,6 +273,9 @@ export const hax = pgTable(
229273 submission_locked_at : timestamp ( { withTimezone : true } ) ,
230274 is_final : boolean ( ) . notNull ( ) . default ( false ) ,
231275 rendered_preview_url : text ( ) ,
276+ // Live diff score (0-100) comparing user output to target
277+ diff_score : integer ( ) ,
278+ diff_score_updated_at : timestamp ( { withTimezone : true } ) ,
232279 created_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( ) ,
233280 updated_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( )
234281 } ,
@@ -248,6 +295,31 @@ export const hax_unique_user_target_solo = sql`
248295 where "type" = 'SOLO';
249296` ;
250297
298+ /* =========================
299+ HAX_HISTORY (for playback/replay)
300+ ========================= */
301+ export const hax_history = pgTable (
302+ 'hax_history' ,
303+ {
304+ id : uuid ( ) . primaryKey ( ) . defaultRandom ( ) ,
305+ hax_id : uuid ( )
306+ . notNull ( )
307+ . references ( ( ) => hax . id , { onDelete : 'cascade' } ) ,
308+ html : text ( ) . notNull ( ) ,
309+ css : text ( ) . notNull ( ) ,
310+ // Milliseconds since battle started - for playback timing
311+ elapsed_ms : integer ( ) . notNull ( ) ,
312+ // Sequence number for ordering saves within a hax
313+ sequence : integer ( ) . notNull ( ) ,
314+ created_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( )
315+ } ,
316+ ( t ) => [
317+ index ( 'hax_history_by_hax' ) . on ( t . hax_id ) ,
318+ // Composite index for efficient playback queries (ordered by sequence)
319+ index ( 'hax_history_playback' ) . on ( t . hax_id , t . sequence )
320+ ]
321+ ) ;
322+
251323/* =========================
252324 RATINGS
253325 ========================= */
@@ -333,7 +405,10 @@ export const awards = pgTable(
333405 updated_at : timestamp ( { withTimezone : true } ) . notNull ( ) . defaultNow ( )
334406 } ,
335407 ( t ) => [
336- uniqueIndex ( 'awards_unique_battle_award_type' ) . on ( t . battle_id , t . award_type ) ,
408+ uniqueIndex ( 'awards_unique_battle_award_type' ) . on (
409+ t . battle_id ,
410+ t . award_type
411+ ) ,
337412 index ( 'awards_user_idx' ) . on ( t . user_id ) ,
338413 index ( 'awards_battle_idx' ) . on ( t . battle_id )
339414 ]
@@ -382,18 +457,21 @@ export const user_relations = relations(user, ({ many }) => ({
382457} ) ) ;
383458
384459// UserRelationships → User
385- export const user_relationships_relations = relations ( user_relationships , ( { one } ) => ( {
386- user : one ( user , {
387- fields : [ user_relationships . user_id ] ,
388- references : [ user . id ] ,
389- relationName : 'fromUser'
390- } ) ,
391- relatedUser : one ( user , {
392- fields : [ user_relationships . related_user_id ] ,
393- references : [ user . id ] ,
394- relationName : 'toUser'
460+ export const user_relationships_relations = relations (
461+ user_relationships ,
462+ ( { one } ) => ( {
463+ user : one ( user , {
464+ fields : [ user_relationships . user_id ] ,
465+ references : [ user . id ] ,
466+ relationName : 'fromUser'
467+ } ) ,
468+ relatedUser : one ( user , {
469+ fields : [ user_relationships . related_user_id ] ,
470+ references : [ user . id ] ,
471+ relationName : 'toUser'
472+ } )
395473 } )
396- } ) ) ;
474+ ) ;
397475
398476// Battles → Target and Referee (User)
399477export const battle_relations = relations ( battles , ( { one, many } ) => ( {
@@ -410,22 +488,25 @@ export const battle_relations = relations(battles, ({ one, many }) => ({
410488} ) ) ;
411489
412490// BattleParticipants → Battle + User + Hax
413- export const battle_participants_relations = relations ( battle_participants , ( { one } ) => ( {
414- battle : one ( battles , {
415- fields : [ battle_participants . battle_id ] ,
416- references : [ battles . id ]
417- } ) ,
418- user : one ( user , {
419- fields : [ battle_participants . user_id ] ,
420- references : [ user . id ]
421- } ) ,
422- hax : one ( hax , {
423- fields : [ battle_participants . user_id , battle_participants . battle_id ] ,
424- references : [ hax . user_id , hax . battle_id ]
491+ export const battle_participants_relations = relations (
492+ battle_participants ,
493+ ( { one } ) => ( {
494+ battle : one ( battles , {
495+ fields : [ battle_participants . battle_id ] ,
496+ references : [ battles . id ]
497+ } ) ,
498+ user : one ( user , {
499+ fields : [ battle_participants . user_id ] ,
500+ references : [ user . id ]
501+ } ) ,
502+ hax : one ( hax , {
503+ fields : [ battle_participants . user_id , battle_participants . battle_id ] ,
504+ references : [ hax . user_id , hax . battle_id ]
505+ } )
425506 } )
426- } ) ) ;
507+ ) ;
427508
428- // Hax → User, Battle, Target, Votes
509+ // Hax → User, Battle, Target, Votes, History
429510export const hax_relations = relations ( hax , ( { one, many } ) => ( {
430511 user : one ( user , {
431512 fields : [ hax . user_id ] ,
@@ -439,7 +520,16 @@ export const hax_relations = relations(hax, ({ one, many }) => ({
439520 fields : [ hax . target_id ] ,
440521 references : [ targets . id ]
441522 } ) ,
442- votes : many ( battle_votes )
523+ votes : many ( battle_votes ) ,
524+ history : many ( hax_history )
525+ } ) ) ;
526+
527+ // HaxHistory → Hax
528+ export const hax_history_relations = relations ( hax_history , ( { one } ) => ( {
529+ hax : one ( hax , {
530+ fields : [ hax_history . hax_id ] ,
531+ references : [ hax . id ]
532+ } )
443533} ) ) ;
444534
445535// --- Ratings relations
0 commit comments