File size: 3,636 Bytes
1c730d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
# you can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
"""Wire models of the Game Mode solution-capitalisation endpoints.

Pydantic request/response models normally live at the top of ``main.py``;
the game group sits here to keep ``main.py`` under the module-size ceiling
(scripts/check_code_quality.py). The store logic itself is in
``services/game_solutions.py``; the frontend mirror is
``frontend/src/types.ts`` (LogGameSolutionRequest / LogGameSolutionResponse
/ GameLeverStatsResponse), machine-checked via ``openapi.snapshot.json``.
"""
from __future__ import annotations

from pydantic import BaseModel


class GameSolutionAction(BaseModel):
    """One retained (starred) remedial action of a Game Mode study.

    ``levers`` are magnitude-free unitary signatures computed by the
    frontend (``redispatch:<gen>``, ``ls:<load>``, ``switch:<id>=<state>``,
    …); an empty list means the catalogue identity ``action:<action_id>``
    is used instead. See services/game_solutions.py.
    """
    action_id: str
    description: str | None = None
    action_type: str | None = None
    levers: list[str] = []
    # True when the action is effective (reduces the baseline worst
    # loading; a combined action must also beat its underlying actions by
    # ≥ 1 loading-point). The novelty bonus is only paid when EVERY
    # retained action is effective.
    effective: bool = True


class LogGameSolutionRequest(BaseModel):
    player: str | None = None
    session_name: str | None = None
    study_id: str | None = None
    study_label: str | None = None
    network_path: str
    contingency_id: str
    solved: bool = False
    final_max_rho: float | None = None
    baseline_max_rho: float | None = None
    actions: list[GameSolutionAction]


class GameSolutionNovelty(BaseModel):
    new_proposition: bool
    new_levers: list[str]
    # False when at least one retained action was not effective — novelty
    # is still reported but bonus_points stays 0.
    effective: bool
    bonus_points: int


class GameSolutionFrequency(BaseModel):
    action_id: str | None
    description: str | None
    signatures: list[str]
    count: int
    total: int
    share: float


class GameSolutionContextStats(BaseModel):
    distinct_propositions: int
    total_retentions: int


class LogGameSolutionResponse(BaseModel):
    stored: bool
    duplicate: bool
    context_key: str
    signature: str
    novelty: GameSolutionNovelty
    frequencies: list[GameSolutionFrequency]
    context_stats: GameSolutionContextStats


class GameLeverStat(BaseModel):
    signature: str
    label: str
    category: str
    count: int
    share: float
    sample_description: str | None


class GameLeverStatsResponse(BaseModel):
    context_key: str
    total_retentions: int
    levers: list[GameLeverStat]


class PlayerSessionsResponse(BaseModel):
    player: str
    # Distinct sessions this player already recorded in the shared base;
    # seeds the default session name (`<player> — session <count+1>`).
    session_count: int
    # The concrete distinct session names (sorted, case-insensitive). The
    # config screen suggests the first free `session <n>` index over these
    # and blocks a name that already exists — a count-plus-one heuristic
    # re-suggests a taken name when the recorded indices have gaps.
    session_names: list[str] = []