58 lines
3.7 KiB
JavaScript
58 lines
3.7 KiB
JavaScript
'use strict';
|
|
|
|
function createPlayerService(deps){
|
|
const {
|
|
randomHex,now,cleanName,defaultName,hashToken,writePlayer,readPlayer,
|
|
withPlayerQueue,withWorldQueue,readWorld,readWorldBoard,normalizeBonuses,
|
|
earnedScore,publicState,bonusAmount,bonusDelayMs,notifyProfile,
|
|
purchaseContext,findPurchase,assertAffordable,createPurchase,starterLineColor,boardPattern
|
|
}=deps||{};
|
|
|
|
const createSession=async nameInput=>{
|
|
const playerId=randomHex(12),token=randomHex(32),timestamp=now(),name=cleanName(nameInput,defaultName(playerId));
|
|
const record={
|
|
playerId,name,tokenHash:hashToken(token),purchases:[],generationBonuses:[],
|
|
starterLineColor:starterLineColor?.(playerId)||null,earnedScore:0,economyRevision:0,
|
|
createdAt:timestamp,updatedAt:timestamp
|
|
};
|
|
await writePlayer(record);
|
|
return{status:201,body:{playerId,token,name,starterLineColor:record.starterLineColor,revision:0,serverTime:timestamp}};
|
|
};
|
|
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});
|