Files
kickkingsapi/docs/unity_mmr_winner_prompt.md
2026-09-04 19:11:49 +00:00

166 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prompt for Unity Cursor — hidden MMR winner report
Copy everything below the line into Unity Cursor. Do not change join/collect, rematch, or client UI.
---
You are working in the Kick Kings **dedicated server** Unity project.
The matchmaker API now computes **hidden MMR** on settle. Players must **never** see MMR. Do not add MMR to HUD, post-match screens, profile, or any client-bound packet.
Your only job: extend the existing `PATCH /internal/match/{matchId}/winner` JSON body so every settle includes scores and a forfeit flag. Keep the existing header `X-Dedicated-Server-Secret`. Call winner **exactly once** when the outcome is known (retries with the same `winner` are safe).
## Endpoint (unchanged URL)
`PATCH /internal/match/{matchId}/winner`
## New required JSON body
Always send all six fields. `winner`, `red_connected`, and `blue_connected` already exist — do not remove them.
```json
{
"winner": "red",
"red_connected": true,
"blue_connected": true,
"red_score": 3,
"blue_score": 1,
"forfeit": false
}
```
| Field | Type | Rules |
|-------|------|--------|
| `winner` | `"red"` or `"blue"` | Winning side. Unchanged. |
| `red_connected` | bool | Red still had a live connection at settle. Do **not** infer from `winner`. |
| `blue_connected` | bool | Blue still had a live connection at settle. |
| `red_score` | int | Rounds/sets red has won (first-to-3). Send on **every** settle, including forfeits. Use the score at the moment the match ended (03 typical). |
| `blue_score` | int | Same for blue. |
| `forfeit` | bool | `true` for leave, disconnect, last-disconnect-wins, **and skip/auto-forfeit** (two skipped turns). `false` only for a normal score win. |
JSON types must be real booleans and integers, not strings (`true` not `"true"`, `3` not `"3"`).
## How the API uses this (do not reimplement)
The API applies hidden MMR. You only report facts.
| What you send | MMR % the API uses |
|---------------|--------------------|
| `forfeit: true` **and** loser `*_connected: false`, or both disconnected | 20% |
| `forfeit: true` **and** both still connected (skip/auto-forfeit) | score table (100% / 90% / 75%) |
| Normal finish, loser score 0 (30) | 100% |
| Normal finish, loser score 1 (31) | 90% |
| Normal finish, loser score 2+ (32) | 75% |
Skip-forfeit: both still connected → `forfeit: true` and send the actual score. API uses the score table, not 20%. The skipper (loser) does **not** get CC.
If you omit scores, the API treats the match as 30 (100%). That is wrong for 31 and 32. **Always send scores.**
## Connected flags (unchanged meaning)
Set a side to `false` when:
- Mirror reports disconnect and reconnect grace expired
- That player pressed Leave (even if TCP is still up for a moment)
`true` = still in the match when you report the outcome (score win, opponent forfeit, or skip-forfeit while they stayed).
CC (100) is awarded only to connected finishers who did **not** forfeit. Last-disconnect winner can be disconnected: they get RC, not CC. Skip/auto-forfeit: skipper gets no CC even if still connected. Do not infer `*_connected` from `winner`.
## What to send in each end condition
### 1. Normal score win (first to 3)
Both still in the match.
```json
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 3, "blue_score": 1, "forfeit": false }
```
Use the real score: 30, 31, or 32. `forfeit` is **false**.
### 2. One player leaves or disconnects
Remaining player wins. Disconnected side `false`. `forfeit: true`. Scores = rounds already won when they left.
```json
{ "winner": "red", "red_connected": true, "blue_connected": false, "red_score": 1, "blue_score": 0, "forfeit": true }
```
### 3. Both disconnect (last-disconnect-wins)
Last player who disconnected is `winner`. Both `*_connected: false`. `forfeit: true`. Keep disconnect-order tracking until the winner call is acknowledged.
```json
{ "winner": "blue", "red_connected": false, "blue_connected": false, "red_score": 0, "blue_score": 2, "forfeit": true }
```
### 4. Skip-forfeit (two skipped turns)
Not a disconnect. If that player is still connected, keep `*_connected: true`. Send `forfeit: true`. They do **not** get CC. Send the actual score. Winner is whoever the game rules already pick. Hidden MMR uses the score table.
### 5. Process crash / forced shutdown after fees were collected
Same as remaining-player or last-disconnect. Send accurate `*_connected`, scores, `forfeit: true` if it was a disconnect path, and call winner **before** exit whenever possible.
Never skip the winner call after collect. Never invent a draw.
## Success response (ignore MMR on clients)
```json
{
"ok": true,
"id": 123,
"winner": "red",
"winner_id": 456,
"already_settled": false,
"economy": { "entry_fee_rc": 21, "rc_prize": 34, "participant_cc": 100, "cc_awarded_red": true, "cc_awarded_blue": false },
"mmr_delta": 27,
"forfeit": false
}
```
- Keep using `economy` / `cc_awarded_*` as today.
- `mmr_delta` and `forfeit` are for server logs only. **Do not forward them to player clients. Do not show them in UI.**
- Honor response `winner` / `winner_id`. Simultaneous Leave/skip: the second `forfeit: true` PATCH is **200** `already_settled` with the first settles winner, not 409.
HTTP: 200 = settled, already settled same winner, or a second forfeit that lost the race. 409 = entries not collected, or winner already set to the other side on a **non-forfeit** conflict.
## Suggested C# payload
Extend the existing winner DTO / anonymous object. Do not stringify bools/ints.
```csharp
public sealed class WinnerReportBody
{
public string winner; // "red" | "blue"
public bool red_connected;
public bool blue_connected;
public int red_score;
public int blue_score;
public bool forfeit;
}
```
Serialize with standard JSON (`true`/`false`, numeric scores). Field names must match exactly (snake_case as above).
Find the dedicated-server method that currently PATCHes `/internal/match/{id}/winner` and add `red_score`, `blue_score`, `forfeit` there. Pass the live round scoreboard and whether this settle is a leave, disconnect, or skip/auto-forfeit.
## Do not
- Show MMR, `mmr_delta`, or rating of any kind to players
- Change join PATCH (`red_joined_at` / `blue_joined_at`) or rematch
- Omit scores on forfeit (still send the score at leave)
- Set `forfeit: false` for skip-forfeit / auto-forfeit (must be `true` so the skipper gets no CC)
- Infer `red_connected` / `blue_connected` from who won
- Leave a collected match without a winner call
## Done when
- [ ] Winner PATCH body always includes `red_score`, `blue_score`, `forfeit`
- [ ] Score wins send `forfeit: false` and the real 30 / 31 / 32
- [ ] Leave / disconnect / last-disconnect send `forfeit: true` and `*_connected: false` on leavers
- [ ] Skip-forfeit / auto-forfeit sends `forfeit: true` (skipper gets no CC even if still connected)
- [ ] No client UI or gameplay packet exposes MMR
- [ ] Winner is still always called after fees were collected