2026-07-31 12:54:46 +09:00
'use strict' ;
function createPlayerService ( deps ) {
const {
randomHex , now , cleanName , defaultName , hashToken , writePlayer , readPlayer ,
withPlayerQueue , withWorldQueue , readWorld , readWorldBoard , normalizeBonuses ,
earnedScore , publicState , bonusAmount , bonusDelayMs , notifyProfile ,
2026-08-01 16:06:14 +09:00
purchaseContext , findPurchase , assertAffordable , createPurchase , starterLineColor , boardPattern
2026-07-31 12:54:46 +09:00
} = deps || { } ;
const createSession = async nameInput => {
const playerId = randomHex ( 12 ) , token = randomHex ( 32 ) , timestamp = now ( ) , name = cleanName ( nameInput , defaultName ( playerId ) ) ;
2026-08-01 16:06:14 +09:00
const record = {
playerId , name , tokenHash : hashToken ( token ) , purchases : [ ] , generationBonuses : [ ] ,
starterLineColor : starterLineColor ? . ( playerId ) || null , earnedScore : 0 , economyRevision : 0 ,
createdAt : timestamp , updatedAt : timestamp
} ;
2026-07-31 12:54:46 +09:00
await writePlayer ( record ) ;
2026-08-01 16:06:14 +09:00
return { status : 201 , body : { playerId , token , name , starterLineColor : record . starterLineColor , revision : 0 , serverTime : timestamp } } ;
2026-07-31 12:54:46 +09:00
} ;
const updateProfile = async ( playerId , nameInput ) => {
const name = cleanName ( nameInput ) ; if ( ! name ) throw Object . assign ( new Error ( 'Player name is required' ) , { status : 400 } ) ;
return withPlayerQueue ( playerId , async ( ) => {
const record = await readPlayer ( playerId ) ; record . name = name ; record . updatedAt = now ( ) ; await writePlayer ( record ) ; notifyProfile ? . ( playerId , name ) ;
return { status : 200 , body : { playerId , name , serverTime : record . updatedAt } } ;
} ) ;
} ;
const getState = record => ( { status : 200 , body : { player : publicState ( record ) , serverTime : now ( ) } } ) ;
const awardGenerationBonus = async ( playerId , boardId ) => {
if ( ! boardPattern . test ( boardId ) ) throw Object . assign ( new Error ( 'Invalid board id' ) , { status : 400 } ) ;
return withPlayerQueue ( playerId , ( ) => withWorldQueue ( async ( ) => {
const record = await readPlayer ( playerId ) , world = await readWorld ( ) , row = await readWorldBoard ( world , boardId ) , timestamp = now ( ) ;
if ( ! row ? . state ? . solved || row . state . solvedById !== playerId ) throw Object . assign ( new Error ( 'Only the solver can receive this bonus' ) , { status : 403 } ) ;
if ( row . state . expanded === true ) throw Object . assign ( new Error ( 'Expansion already succeeded' ) , { status : 409 } ) ;
if ( timestamp - ( Number ( row . state . solvedAt ) || 0 ) < bonusDelayMs ) throw Object . assign ( new Error ( 'Generation attempt is still in progress' ) , { status : 409 } ) ;
const grant = world . expansionGrants ? . [ playerId ] ;
if ( ! grant || grant . boardId !== boardId || grant . expiresAt < timestamp || ! ( grant . maxBoards > 0 ) ) throw Object . assign ( new Error ( 'No failed expansion is eligible' ) , { status : 409 } ) ;
record . generationBonuses = normalizeBonuses ( record . generationBonuses ) ;
if ( ! record . generationBonuses . includes ( boardId ) ) {
record . generationBonuses . push ( boardId ) ;
record . earnedScore = Math . min ( Number . MAX _SAFE _INTEGER , earnedScore ( record ) + bonusAmount ) ;
record . economyRevision = ( record . economyRevision || 0 ) + 1 ; record . updatedAt = timestamp ; await writePlayer ( record ) ;
}
return { status : 200 , body : { bonus : bonusAmount , player : publicState ( record ) , serverTime : timestamp } } ;
} ) ) ;
} ;
const purchase = async ( playerId , boardId , itemId ) => withPlayerQueue ( playerId , async ( ) => {
const record = await readPlayer ( playerId ) , world = await readWorld ( ) , context = await purchaseContext ( world , boardId , itemId ) , existing = findPurchase ( record , boardId , itemId ) ;
if ( existing ) return { status : 200 , body : { purchase : existing , player : publicState ( record ) , serverTime : now ( ) } } ;
await assertAffordable ( world , record , context . price ) ;
const row = createPurchase ( record , boardId , context . item , context . price ) ; await writePlayer ( record ) ;
return { status : 201 , body : { purchase : row , player : publicState ( record ) , serverTime : record . updatedAt } } ;
} ) ;
return Object . freeze ( { createSession , updateProfile , getState , awardGenerationBonus , purchase } ) ;
}
module . exports = Object . freeze ( { createPlayerService } ) ;