Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Status: Current
# Project guidance

- Treat `docs/tool-contracts.json`, `internal/graphql/operations/*.graphql`, `internal/graphql/schema/bootstrap.graphql`, and `workflows/workflows.json` as canonical sources. Do not edit generated outputs directly.
- Run `make generate` after canonical-source changes, `make check-generated` to detect stale artifacts, and `make check` before handoff.
- Run `make generate` after canonical-source changes, `make check-generated` to detect stale artifacts, and both `make check` and `make test-live` before handoff.
- Use conventional commit messages, such as `feat: add optional player rows to match list`.
- Never commit tokens, `.env`, cache databases, introspection, fetched STRATZ schemas/constants, or `.stratz-restricted`.
- Preserve JSON-RPC-only stdout and centralized secret redaction.
- Keep the production STRATZ endpoint fixed; inject executors only in tests.
Expand Down
1 change: 1 addition & 0 deletions docs/generated-tool-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ List normalized matches for a player using bounded filters and an authenticated
| `from` | false | `dateTime` |
| `game_mode_id` | false | `integer` |
| `hero` | false | `heroIdentifier` |
| `include_player` | false | `boolean`; default `false` |
| `include_raw` | false | `boolean`; default `false` |
| `limit` | false | `integer`; default `20`; minimum `1`; maximum `100` |
| `lobby_type_id` | false | `integer` |
Expand Down
72 changes: 70 additions & 2 deletions docs/tool-contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@
"description": "STRATZ Individual Match Performance (IMP) for this player when upstream provides it. Null means unavailable for the match/player."
},
"won": {
"type": "boolean"
"type": ["boolean", "null"]
}
},
"required": [
Expand Down Expand Up @@ -574,6 +574,69 @@
],
"additionalProperties": false
},
"playerMatchSummary": {
"type": "object",
"properties": {
"match_id": {
"$ref": "#/$defs/matchId"
},
"started_at": {
"$ref": "#/$defs/nullableDateTime"
},
"duration_seconds": {
"type": ["integer", "null"],
"minimum": 0
},
"radiant_win": {
"type": ["boolean", "null"]
},
"radiant_score": {
"type": ["integer", "null"],
"minimum": 0
},
"dire_score": {
"type": ["integer", "null"],
"minimum": 0
},
"game_mode_id": {
"type": ["integer", "null"]
},
"lobby_type_id": {
"type": ["integer", "null"]
},
"region_id": {
"type": ["integer", "null"]
},
"league_id": {
"type": ["string", "null"]
},
"patch_id": {
"type": ["string", "null"]
},
"parse_status": {
"type": "string",
"enum": ["parsed", "partial", "pending", "unavailable", "unknown"]
},
"player": {
"$ref": "#/$defs/matchPlayer"
}
},
"required": [
"match_id",
"started_at",
"duration_seconds",
"radiant_win",
"radiant_score",
"dire_score",
"game_mode_id",
"lobby_type_id",
"region_id",
"league_id",
"patch_id",
"parse_status"
],
"additionalProperties": false
},
"matchAvailabilityContext": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1230,6 +1293,11 @@
"type": "string",
"maxLength": 64
},
"include_player": {
"type": "boolean",
"default": false,
"description": "Include the requested player's own match row on each summary when available."
},
"limit": {
"type": "integer",
"minimum": 1,
Expand Down Expand Up @@ -1271,7 +1339,7 @@
"type": "array",
"maxItems": 100,
"items": {
"$ref": "#/$defs/matchSummary"
"$ref": "#/$defs/playerMatchSummary"
}
},
"page": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,10 @@
"type": "string"
},
"won": {
"type": "boolean"
"type": [
"boolean",
"null"
]
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,10 @@
"type": "string"
},
"won": {
"type": "boolean"
"type": [
"boolean",
"null"
]
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
}
]
},
"include_player": {
"default": false,
"description": "Include the requested player's own match row on each summary when available.",
"type": "boolean"
},
"include_raw": {
"default": false,
"type": "boolean"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,87 @@
"null"
]
},
"player": {
"additionalProperties": false,
"properties": {
"account_id": {
"pattern": "^[0-9]{1,10}$",
"type": [
"string",
"null"
]
},
"assists": {
"minimum": 0,
"type": "integer"
},
"deaths": {
"minimum": 0,
"type": "integer"
},
"hero_id": {
"minimum": 1,
"type": "integer"
},
"imp": {
"description": "STRATZ Individual Match Performance (IMP) for this player when upstream provides it. Null means unavailable for the match/player.",
"type": [
"number",
"null"
]
},
"kills": {
"minimum": 0,
"type": "integer"
},
"level": {
"minimum": 0,
"type": [
"integer",
"null"
]
},
"networth": {
"minimum": 0,
"type": [
"integer",
"null"
]
},
"position": {
"maximum": 9,
"minimum": 0,
"type": "integer"
},
"team": {
"enum": [
"radiant",
"dire"
],
"type": "string"
},
"won": {
"type": [
"boolean",
"null"
]
}
},
"required": [
"account_id",
"hero_id",
"team",
"position",
"kills",
"deaths",
"assists",
"networth",
"level",
"imp",
"won"
],
"type": "object"
},
"radiant_score": {
"minimum": 0,
"type": [
Expand Down
23 changes: 20 additions & 3 deletions internal/contracts/zz_generated.contracts.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 59 additions & 4 deletions internal/domain/playermatch/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,56 @@ func mapSummary(source *upstreamMatch) contracts.MatchSummary {
}
}

func mapPlayerMatchSummary(source *upstreamMatch, accountID int64, includePlayer bool) contracts.PlayerMatchSummary {
summary := mapSummary(source)
playerSummary := contracts.PlayerMatchSummary{
MatchID: summary.MatchID,
StartedAt: summary.StartedAt,
DurationSeconds: summary.DurationSeconds,
RadiantWin: summary.RadiantWin,
RadiantScore: summary.RadiantScore,
DireScore: summary.DireScore,
GameModeID: summary.GameModeID,
LobbyTypeID: summary.LobbyTypeID,
RegionID: summary.RegionID,
LeagueID: summary.LeagueID,
PatchID: summary.PatchID,
ParseStatus: summary.ParseStatus,
}
if includePlayer {
playerSummary.Player = findMatchPlayer(source, accountID)
}
return playerSummary
}

func findMatchPlayer(source *upstreamMatch, accountID int64) *contracts.MatchPlayer {
for _, player := range source.Players {
if player.SteamAccountID == nil || *player.SteamAccountID != accountID {
continue
}
team := "dire"
if player.IsRadiant {
team = "radiant"
}
won := playerWon(source.DidRadiantWin, player.IsRadiant)
publicAccountID := strconv.FormatInt(accountID, 10)
return &contracts.MatchPlayer{
AccountID: &publicAccountID,
HeroID: player.HeroID,
Team: team,
Position: normalizedPlayerSlot(player.PlayerSlot),
Kills: maxZero(player.Kills),
Deaths: maxZero(player.Deaths),
Assists: maxZero(player.Assists),
Networth: nonNegative(player.Networth),
Level: nonNegative(player.Level),
Imp: player.IMP,
Won: won,
}
}
return nil
}

var gameModeIDs = map[string]int64{
"NONE": 0, "ALL_PICK": 1, "CAPTAINS_MODE": 2, "RANDOM_DRAFT": 3,
"SINGLE_DRAFT": 4, "ALL_RANDOM": 5, "INTRO": 6, "DIRETIDE": 7,
Expand Down Expand Up @@ -129,10 +179,7 @@ func mapMatch(source *upstreamMatch, detail contracts.DetailLevel) contracts.Mat
if player.IsRadiant {
team = "radiant"
}
won := false
if source.DidRadiantWin != nil {
won = *source.DidRadiantWin == player.IsRadiant
}
won := playerWon(source.DidRadiantWin, player.IsRadiant)
match.Players = append(match.Players, contracts.MatchPlayer{
AccountID: accountID,
HeroID: player.HeroID,
Expand Down Expand Up @@ -208,6 +255,14 @@ func mapTimeline(source *upstreamPlaybackData) []contracts.TimelineEvent {
return mapEvents(events)
}

func playerWon(didRadiantWin *bool, isRadiant bool) *bool {
if didRadiantWin == nil {
return nil
}
won := *didRadiantWin == isRadiant
return &won
}

func normalizedPlayerSlot(slot int64) int64 {
if slot >= 128 && slot <= 132 {
return slot - 123
Expand Down
Loading
Loading