First Login¶
Status: π§ Partial β First-login is the strongest page in the zone: the resumable state machine, ship rarity config, 6 guard personalities with +-0.10 modifier, the exact 0.5/0.3/0.2 aggregate formula β¦ Β· β οΈ contains codeβspec divergence (impl audit 2026-06-16)
Purpose¶
First login is a stateful onboarding flow that doubles as character creation. A new account does not yet have a Player record; the flow runs an AI-driven dialogue against a security guard at the Callisto Colony shipyard β the in-lore landmark inside Terran Space's Capital Sector (sector 1 in vanilla Terran Space; see ../FEATURES/definitions.md#sector) β evaluates the player's persuasion against rarity-tier thresholds, and on success creates the Player row with a starter ship, credits, and initial state. The flow is resumable, has a guaranteed-safe failure path (escape pod), and works fully offline (manual provider fallback).
Inputs¶
The state machine reads:
- The player's User row (auth identity, locale).
- PlayerFirstLoginState row (if any) β has the player completed onboarding before?
- FirstLoginSession row (if any) β in-progress session for resume.
- ShipPresentationOptions β the 3+ ships offered this attempt.
- ShipRarityConfig table β admin-tunable spawn probability + persuasion thresholds.
- DialogueExchange rows for this session β full back-and-forth history.
- AI provider chain (same as ARIA dialogue) for response evaluation.
- The player's free-form text input.
The system fires on:
- GET /api/v1/first-login/status β bootstraps or resumes a session.
- POST /api/v1/first-login/session β start a new session.
- POST /api/v1/first-login/dialogue β submit the player's next response.
- POST /api/v1/first-login/claim β finalize the ship claim once persuasion succeeds.
- POST /api/v1/first-login/abandon β accept the escape-pod fallback.
Process¶
ββββββββββββββββββββββββ
β App launch (auth) β
ββββββββββββ¬ββββββββββββ
β
ββββββββββββΌβββββββββββββββ
β should_show_first_login β
β (PlayerFirstLoginState β
β null or completed=False)β
ββββββββββββ¬βββββββββββββββ
no β go to game (skip flow)
yes β enter state machine
β
ββββββββββββββββΌβββββββββββββββ
STATE 1 β β WELCOME / NARRATIVE OPEN β
β (shipyard backdrop, cat) β
ββββββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
STATE 2 β β SHIP PRESENTATION β
β - generate 3+ ships, rolledβ
β against ShipRarityConfig β
β - persist ShipPresentation β
β Options β
ββββββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
STATE 3 β β SHIP CHOICE β
β player picks claimed ship β
β β record_player_ship_claim β
ββββββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
STATE 4 β β TUTORIAL / DIALOGUE LOOP β
β guard probes 4 topics: β
β β’ identity_verification β
β β’ arrival_details β
β β’ ship_knowledge β
β β’ situational_awareness β
β per turn: β
β - generate_guard_question β
β - record_player_answer β
β - _analyze_player_responseβ
β (persuasiveness, conf., β
β consistency, skill, β
β inconsistencies, key β
β info, believability) β
β cat_mention_bonus: +0.15 β
ββββββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
STATE 5 β β EVALUATE OUTCOME β
β _evaluate_dialogue_outcome β
β compare aggregate β
β persuasion vs rarity β
β threshold (weak/avg/strong)β
ββββ¬ββββββββββββββββββββ¬βββββββ
β pass β fail (after retries)
βΌ βΌ
βββββββββββββββββββ ββββββββββββββββββββββββ
β STATE 6: SPAWN β β STATE 6': ESCAPE POD β
β - create Player β β auto_approve_escape β
β - create Ship β β _pod (always passes) β
β - set credits β β - 500 credits β
β per table β β - persuasion=0.5 β
β - sector=1 β ββββββββββββ¬ββββββββββββ
β - mark first β β
β _login.compl. β β
ββββββββββ¬βββββββββ β
β β
βββββββββββββ¬ββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββ
β STATE 7: COMPLETE β
β complete_first_login() β
β - PlayerFirstLoginState β
β persisted β
β - finalize Ship + Player β
β - emit notification β
ββββββββββββββββββββββββββββββββ
Ship-claim difficulty matrix¶
DEFAULT_SHIP_CONFIGS from the service:
| Ship | Tier | Spawn % | Base credits | Weak / Avg / Strong threshold |
|---|---|---|---|---|
| Escape Pod | 1 | 100 | 1,000 | 0.30 / 0.30 / 0.30 |
| Light Freighter | 2 | 50 | 2,500 | 0.40 / 0.35 / 0.30 |
| Scout Ship | 3 | 25 | 2,000 | 0.55 / 0.50 / 0.45 |
| Fast Courier | 3 | 20 | 3,000 | 0.55 / 0.50 / 0.45 |
| Cargo Hauler | 4 | 10 | 5,000 | 0.65 / 0.55 / 0.50 |
| Defender | 5 | 5 | 7,000 | 0.75 / 0.65 / 0.60 |
| Colony Ship | 6 | 3 | 10,000 | 0.80 / 0.70 / 0.65 |
| Carrier | 7 | 1 | 15,000 | 0.85 / 0.75 / 0.70 |
Threshold selected based on the AI's negotiation_skill classification (weak / average / strong) of the player's dialogue.
Persistence (resume)¶
FirstLoginSession.idis bound to theUser.- Each
DialogueExchangeis persisted as it happens; reload mid-flow returns the full history. - Re-opening the app at any state replays from the last persisted state.
Skip / abandon¶
- Authenticated player whose
PlayerFirstLoginState.completed = Trueis routed to the main game; the flow does not run. - A player who explicitly abandons calls
auto_approve_escape_podβ the safest outcome (escape pod + 500 credits;final_persuasion_scoreis set to 0.5, whilenegotiation_skillretains whatever the dialogue last scored).
AI evaluation¶
The evaluator (_analyze_player_response) returns:
{
persuasiveness: float (0..1),
confidence_level: enum,
consistency: float (0..1),
negotiation_skill: weak | average | strong,
inconsistencies: [],
key_information: { player_name?, claimed_ship?, ... },
overall_believability: float (0..1)
}
The provider chain matches the ARIA dialogue chain: primary β secondary β manual rule-based provider. The manual provider replicates the same response shape using pattern-matching, so first login works fully offline.
Aggregate persuasion formula¶
The session-level persuasion score that gates the threshold check is a weighted aggregate over the dialogue exchanges:
final_persuasion = 0.5 Γ avg(consistency)
+ 0.3 Γ avg(confidence)
+ 0.2 Γ avg(persuasiveness)
+ cat_mention_bonus (0.15 if triggered)
+ guard_personality_modifier (Β±0.10, see below)
Consistency is the dominant weight because cross-turn coherence is the strongest signal of an honest claim; persuasiveness and confidence are smoothing factors.
Hard-fail triggers¶
Any one of the following routes the session to escape pod with the notoriety_penalty flag set, regardless of the aggregate score:
- Average
consistencyacross all exchanges < 0.3. - Total
inconsistencies(contradictions detected across exchanges) β₯ 3. - Any single exchange has
consistency < 0.2.
Hard-fail awards 300 credits (lower than the standard 500-credit escape-pod path; below) and persists notoriety_penalty = true on the session.
Outcome credits¶
Three escape-pod-class outcome bands:
| Outcome | Credits | Trigger |
|---|---|---|
SUCCESS |
per-ship base credits (1,000 β 15,000) | aggregate β₯ rarity threshold for the claimed ship |
PARTIAL_SUCCESS |
800 | claimed ship is escape pod and the player's aggregate is above the floor |
FAILURE |
500 | dialogue ended without meeting any non-pod threshold; player auto-routed or chose to abandon |
FAILURE (hard-fail) |
300 | one of the hard-fail triggers above fired |
The 500/800/300-credit splits all honor invariant 2 β every flow ends with a viable starter. Hard-fail's lower credit value plus the persistent notoriety flag is the only mechanical penalty for outright deceptive play.
Dialogue length cap¶
Each session hard-caps at 5 exchanges (completed_exchanges >= 5 or early_termination finalizes evaluation). The cap is per-session, not per-attempt; combined with the resumable session model, this means a single session always resolves within 5 exchanges, but a player can reset_player_session and start a fresh session afterwards. There is no max-attempts gate β PlayerFirstLoginState.attempts is monotonic and never resets.
Guard personality¶
The session is bound to one of six guard archetypes at creation, drawn deterministically from guard_personalities.py:GUARD_PERSONALITIES:
| Trait | base_suspicion |
Threshold modifier |
|---|---|---|
| Strict Rule-Follower | 0.60 | +0.10 (harder) |
| Friendly Veteran | 0.30 | β0.10 (easier) |
| Paranoid Newbie | 0.70 | +0.10 (harder) |
| Tired Night-Shifter | 0.40 | β0.10 (easier) |
| Shrewd Investigator | 0.50 | 0.00 |
| Cynical Bureaucrat | 0.55 | 0.00 |
The personality is persisted as four denormalized columns on FirstLoginSession (guard_name, guard_title, guard_trait, guard_description, plus guard_base_suspicion) so dialogue UIs can render the in-character name and prompt style without re-deriving from a lookup table. The threshold modifier nudges the rarity-tier cutoff for this session β a Strict Rule-Follower playing the Defender card asks for more persuasion than a Tired Night-Shifter would.
Cat easter egg¶
If the player mentions the orange cat from the opening narrative, the manual provider awards a flat +0.15 persuasion bonus. The bonus is applied regardless of the active provider (the rule-based detector runs in parallel). The detection is stored on the session for medal / achievement use later.
Outputs / state changes¶
On success path:
- Player row created (sector_id=1, current_sector_id=1, default turns=1000, max_turns=1000, military_rank="Recruit", personal_reputation=0, aria_consciousness_level=1, aria_bonus_multiplier=1.0).
- Player.credits set to claimed-ship's base credits (1,000β15,000).
- Ship row created via ship_specifications_seeder and assigned as Player.current_ship.
- PlayerFirstLoginState.completed = True, with negotiation outcome / cat-boost flags.
- FirstLoginSession.outcome = success.
On escape-pod path:
- Player row created with Escape Pod ship and 500 credits.
- FirstLoginSession.final_persuasion_score = 0.5; negotiation_skill retains the dialogue's last classification (it is not force-overwritten to weak).
- All other defaults identical.
Always:
- DialogueExchange rows record the full conversation.
- ShipPresentationOptions records which ships were shown.
- notification event fires on completion.
Completion side-effects (complete_first_login)¶
Final transition out of the flow performs a self-healing cleanup so the new player can never enter the game in a partial state:
- Existing ship purge β any
Shiprows owned by the player from prior partial attempts are deleted; the awarded ship is the only one assigned. This makes resume + retry safe even if a prior session crashed mid-spawn. - ARIA warm-start β
Player.aria_relationship_score = 50(versus the column-default 25 for a never-onboarded player),Player.aria_total_interactions = 1. The first-login dialogue counts as the first ARIA interaction, so the player begins onboarded into the consciousness-level ladder. - Nickname capture β gated on explicit player confirmation (π Design-only). The dialogue's
extracted_player_nameis presented to the player as "Use<name>as your callsign?" with Yes/No buttons; thePlayer.nicknamewrite only fires on Yes. The extracted name passes through validation before the confirmation prompt is even shown: - Length: 3β20 characters.
- Charset: alphanumeric + underscore + hyphen + a single internal space; no zero-width or RTL characters; no leading/trailing whitespace.
- Profanity filter: case-insensitive match against the configurable
NICKNAME_BLOCKLISTwordlist (services/gameserver/src/services/nickname_validation_service.py:NICKNAME_BLOCKLIST). - Impersonation filter: case-insensitive reject if the candidate matches any existing
Player.nicknameorUser.username. - Uniqueness: enforced by a unique index on
Player.nicknameat the model layer; a race-loser at write time gets a "callsign just taken" error and is asked to pick another. Validation failures surface to the player with the specific reason and offer a free-text retry. Hard-fail sessions (escape-pod fallback, AI-provider exhaustion, dialogue abandoned mid-flow) do not seed an extracted name βPlayer.nicknamestays null and the ship name falls back toUser.username. The pre-decision behavior of overwritingPlayer.nicknamefromextracted_player_namewithout confirmation, validation, or uniqueness check is retired. - First-login marker β
Player.first_login = {"completed": True, "session_id": <uuid>}(the session_id field is a back-pointer for audit, not a quick-check column). - Negotiation bonus β if the session completed at
negotiation_skill = strongandrarity_tier β₯ 3,Player.settings.trade_bonus = 0.1is set (10% better port prices for the rest of the player's life). Lower combinations leave the bonus unset.
Invariants¶
- A
Usercannot finish first login twice β oncePlayerFirstLoginState.completed = True, the flow refuses to start a new session. - The escape-pod outcome always succeeds β there is no failure mode that leaves the player without a ship.
- Every dialogue evaluation persists before a state advance β interruption does not lose history.
- Spawn (state 6) is a single transaction: Player, Ship, and FirstLoginState change together.
- AI provider failure never blocks the flow β manual fallback is always available.
Player.first_login.completed = Truemeans no side-effects are pending.- Initial sector is always Sector 1 of Terran Space (the canonical Earth-area starter region).
Failure modes¶
| Mode | Target handling |
|---|---|
| AI provider down | Manual fallback evaluates dialogue; flow proceeds normally. |
| Player closes tab mid-flow | State persisted at every transition; resume picks up where they left off. |
| Player insists on a ship they can't justify | The dialogue loop hard-caps at 5 exchanges per session; once the cap is hit, evaluation finalizes against current scores. There is no separate "N attempts" gate that auto-routes to escape pod β the player can always choose to abandon and accept the pod fallback. |
| Spawn transaction fails | Whole transaction rolls back; player can retry; session not marked complete. |
| Duplicate user (race) | Unique constraint on Player.user_id rejects the second insert; second request returns the first Player row. |
| Provider returns malformed analysis | Default to negotiation_skill = average and persuasiveness = 0.5; do not crash. |
| Ship spec missing in seeder | Fail spawn; surface clear admin-facing error; player retries with a different ship. |
| Locale unsupported | Default to English; cat-detection still works (English-only patterns + locale-specific aliases). |
Source map¶
| Concern | Path (target) |
|---|---|
| Orchestrator | services/gameserver/src/services/first_login_service.py |
| AI dialogue helpers | services/gameserver/src/services/ai_dialogue_service.py |
| Multi-provider AI | services/gameserver/src/services/ai_provider_service.py |
| Manual fallback | services/gameserver/src/services/enhanced_manual_provider.py |
| Guard personalities | services/gameserver/src/utils/guard_personalities.py |
| Ship specifications seeder | services/gameserver/src/core/ship_specifications_seeder.py |
| First-login models | services/gameserver/src/models/first_login.py |
| API routes | services/gameserver/src/api/routes/first_login.py |
| Admin tools | services/gameserver/src/api/routes/admin_first_login.py |
| Client component | services/player-client/src/pages/FirstLogin/* |
Related¶
- DATA_MODELS:
../DATA_MODELS/player.md,models/first_login.py. - FEATURES:
../FEATURES/gameplay/first-login.md. - SYSTEMS: aria-dialogue.md, turn-regeneration.md.
- REST API:
first_loginroutes auto-published at<api-host>/docs.