Conversation
The Forfeit button only appeared for a human seated by username. In the deploy-and-play flow the seat is the DEPLOYMENT (game.players holds the deployment id, never the owner's userId), so the owner watching their AI play was treated as a pure spectator — no toolbar, no Forfeit — even though exiting that game was the explicit requirement. - Client: Game.tsx now also shows the Forfeit button when the viewer owns an AI seat in this game. New pure helper viewerOwnsAiSeat(session, players) matches the tab's deploy-and-play queue session's deploymentId against the seated AI player (an AI seat's username IS its deployment id) — available mid-game, no result needed. Broadcast/Highlight stay human-player-only; the owner gets exactly the Forfeit control. - Server: forfeitGame resolves the seat by ownership when the user isn't a direct player — for each AI seat it reads DeploymentRepo[seatId].userId and forfeits the seat the requester owns. gameId alone suffices; the client never names the seat, so a stranger can't forfeit someone else's AI game (the same userId join used to prove ownership at queue time). Tested: forfeitGame owner-resolution unit tests (owner forfeits their AI's seat → opponent wins; a non-owner is rejected), viewerOwnsAiSeat unit tests, a Game.tsx integration test (owner sees + emits, bystander doesn't), and a live deploy-and-play run — owner deploys a model, it's matched vs a human, and on the spectated game the Forfeit button shows (even while it's the AI's turn) and ends the game 'Your model lost by forfeit'. Server 1038, client 839, all green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The explicitly-required case that the forfeit feature (#132/#136) missed: when your deployed AI plays a game for you (deploy-and-play), the Forfeit button never showed — so you couldn't exit a game your model was driving.
Why
In deploy-and-play the seat is the deployment, not you:
game.playersholds the deployment id, never the owner's userId (AIParticipant carries no owner field; the only back-pointer isDeploymentRecord.userId). So both gates keyed on the wrong identity:Game.tsx:isPlayer = players.some(p => p.username === you)— an AI seat's username is the deployment id, so you're treated as a spectator and the whole toolbar (incl. Forfeit) is hidden.forfeitGame: authorized only byplayers.findIndex(id => id === user.userId)— your userId isn't in players, so it would reject the forfeit anyway.Fix
viewerOwnsAiSeat(session, players)matches your tab's deploy-and-play queue session (deploymentId) against the seated AI player (an AI seat'susernameis its deployment id) — works mid-game, no result event needed. Broadcast/Highlight stay player-only; the owner gets exactly the Forfeit control.forfeitGameresolves the seat by ownership when the requester isn't a direct player — for each AI seat it readsDeploymentRepo[seatId].userIdand forfeits the seat the requester owns.gameIdalone suffices and the client never names the seat, so a stranger can't forfeit someone else's AI game (same ownership join used at queue time).Verified live (the required case, screenshots in the run)
Owner deploys a model → it's matchmade vs a human → on the spectated game the Forfeit button now shows even while it's the AI's turn; clicking it ends the game with "Your model lost by forfeit" (rated, −162) and the human wins.
Tests:
forfeitGameowner-resolution (owner forfeits their AI's seat; non-owner rejected),viewerOwnsAiSeatunit tests, aGame.tsxintegration test (owner sees + emits; bystander doesn't). Server 1038/1038, client 839/839, tsc/lint/prettier clean.