feat(games): end games by resignation and by agreement - #204
Open
Utzig26 wants to merge 1 commit into
Open
Conversation
Implements RFC-005. Adds resign, offer a draw and withdraw the offer, and turns termination into one table that every path goes through. The draw keeps the shape the old code had, and it is worth keeping: two booleans, one per colour. Offering is a POST, accepting is the same POST from the other side, and the game ends once both flags are up. It removes an endpoint, removes the intermediate state of an offer pending an answer, and is idempotent for free. Withdrawing clears only your own flag. Detection still beats the requested reason. Before applying what the route asked for, the engine is consulted, so resigning into a position that is already drawn by insufficient material is recorded as that draw rather than as a resignation. The position was already decided before anyone clicked, and the result should not depend on who clicked first. What changed is where that lives. It was an eighty five line switch in a loose function at the bottom of the old service; it is now a pure function of three arguments that returns the result and the message, or nothing when there is nothing to end. The finish() that RFC-003 had introduced is gone, absorbed by the same call with no requested reason. The winner of a checkmate is derived from the side to move rather than from the side that moved. In both entry points the mated side is the one on turn, so a single rule covers the mating move and a resignation in an already mated position, which is what let the two paths collapse into one function. Every mutation now goes through the same gate: resolve the game, check the caller holds a colour, check the clock. Without it, resigning a game whose time had already run out would have been recorded as a resignation instead of a loss on time. A player holding both colours can resign against themselves and can agree a draw in two calls, the offer alternating between the colours on its own. That falls out of the two flag model without a special case. 13 unit tests over the termination table alone, and 22 end-to-end covering the draw cycle, the finished game refusing all four mutations, and playing yourself.
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.
Implements RFC-005.
What lands
POST /games/:id/resignPOST /games/:id/drawDELETE /games/:id/drawThe draw is two flags, not a negotiation
Kept from the old code, and worth keeping.
drawOfferis{ white: boolean, black: boolean }. Offering is a POST, accepting is the same POST from the other side, and the game ends once both flags are up. Withdrawing clears only your own.It removes an endpoint, removes the intermediate state of "offer pending an answer", and is idempotent for free — offering twice does nothing.
Detection still beats the requested reason
Before applying what the route asked for, the engine is consulted. Resigning into a position already drawn by insufficient material is recorded as that draw, not as a resignation:
The position was already decided before anyone clicked, and the result should not depend on who clicked first.
Where that logic now lives
It was an eighty five line switch in a loose function at the bottom of the old service, outside dependency injection and untestable on its own.
It is now
resolveTermination(outcome, sideToMove, requested)— a pure function returning{ result, additionalInfo }, ornullwhen there is nothing to end. Thefinish()RFC-003 had introduced is gone, absorbed by the same call withrequested: null.The checkmate winner is derived from the side to move, not from the side that moved. In both entry points the mated side is the one on turn, so one rule covers the mating move and a resignation in an already mated position. That is what let the two paths collapse into a single function.
One gate for every mutation
openMutationresolves the game, checks the caller holds a colour, and checks the clock — used by move, resign, offer and withdraw alike. Without it, resigning a game whose time had already run out would have been recorded as a resignation instead of a loss on time.Playing yourself
Resigns against itself, and agrees a draw in two calls with the offer alternating between the colours on its own. That falls out of the two flag model with no special case.
Verification
280 tests green, clean
stricttypecheck and lint.