Atharva commited on
Commit
bc7b7e2
·
1 Parent(s): d1b017b

Switch Space to custom replay app

Browse files
Dockerfile CHANGED
@@ -2,23 +2,20 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- # Install system dependencies
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
  curl \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
- # Copy only what the Space needs
11
  COPY space_app.py .
 
12
  COPY battle_logs/ ./battle_logs/
13
  COPY requirements_space.txt .
14
 
15
- # Install Python dependencies
16
  RUN pip install --no-cache-dir -r requirements_space.txt
17
 
18
- # Healthcheck against the Gradio app
19
  HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
20
  CMD curl -f http://localhost:7860/ || exit 1
21
 
22
  EXPOSE 7860
23
 
24
- CMD ["python", "space_app.py"]
 
2
 
3
  WORKDIR /app
4
 
 
5
  RUN apt-get update && apt-get install -y --no-install-recommends \
6
  curl \
7
  && rm -rf /var/lib/apt/lists/*
8
 
 
9
  COPY space_app.py .
10
+ COPY static/ ./static/
11
  COPY battle_logs/ ./battle_logs/
12
  COPY requirements_space.txt .
13
 
 
14
  RUN pip install --no-cache-dir -r requirements_space.txt
15
 
 
16
  HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
17
  CMD curl -f http://localhost:7860/ || exit 1
18
 
19
  EXPOSE 7860
20
 
21
+ CMD ["uvicorn", "space_app:app", "--host", "0.0.0.0", "--port", "7860"]
battle_logs/example_battle.json DELETED
@@ -1,229 +0,0 @@
1
- {
2
- "model": "Qwen/Qwen3-4B-Instruct-2507 + LoRA grpo-qwen3-4b-run3",
3
- "format": "gen4randombattle",
4
- "outcome": "won",
5
- "total_reward": 4.7,
6
- "total_turns": 12,
7
- "turns": [
8
- {
9
- "turn": 1,
10
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Infernape\n- HP: 100.0%\n- Status: Healthy\n- Ability: Blaze\n- Item: Life Orb\n- Stat Modifiers: None\n### Active Opponent\n- Name: Swampert\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 112-243\n\n## Part B: Full Self Roster\n- Infernape HP:100% OK Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 100.0% | Status: Healthy",
11
- "valid_actions": [
12
- {"action": "move", "choice": "closecombat"},
13
- {"action": "move", "choice": "flamethrower"},
14
- {"action": "move", "choice": "grassknot"},
15
- {"action": "move", "choice": "machpunch"},
16
- {"action": "switch", "choice": "Starmie"},
17
- {"action": "switch", "choice": "Togekiss"},
18
- {"action": "switch", "choice": "Hippowdon"},
19
- {"action": "switch", "choice": "Jolteon"},
20
- {"action": "switch", "choice": "Heracross"}
21
- ],
22
- "chosen_action": {"action": "move", "choice": "grassknot"},
23
- "reward": 1.95,
24
- "cumulative_reward": 1.95,
25
- "action_was_illegal": false
26
- },
27
- {
28
- "turn": 2,
29
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Infernape\n- HP: 90.0%\n- Status: Healthy\n- Ability: Blaze\n- Item: Life Orb\n- Stat Modifiers: None\n### Active Opponent\n- Name: Swampert\n- HP: 32.0%\n- Status: Healthy\n- Speed Range: 112-243\n\n## Part B: Full Self Roster\n- Infernape HP:90% OK Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 32.0% | Status: Healthy\n - Revealed moves: earthquake",
30
- "valid_actions": [
31
- {"action": "move", "choice": "closecombat"},
32
- {"action": "move", "choice": "flamethrower"},
33
- {"action": "move", "choice": "grassknot"},
34
- {"action": "move", "choice": "machpunch"},
35
- {"action": "switch", "choice": "Starmie"},
36
- {"action": "switch", "choice": "Togekiss"},
37
- {"action": "switch", "choice": "Hippowdon"},
38
- {"action": "switch", "choice": "Jolteon"},
39
- {"action": "switch", "choice": "Heracross"}
40
- ],
41
- "chosen_action": {"action": "move", "choice": "grassknot"},
42
- "reward": 3.15,
43
- "cumulative_reward": 5.1,
44
- "action_was_illegal": false
45
- },
46
- {
47
- "turn": 3,
48
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Infernape\n- HP: 80.0%\n- Status: Healthy\n- Ability: Blaze\n- Item: Life Orb\n- Stat Modifiers: None\n### Active Opponent\n- Name: Gengar\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 178-350\n\n## Part B: Full Self Roster\n- Infernape HP:80% OK Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 100.0% | Status: Healthy",
49
- "valid_actions": [
50
- {"action": "move", "choice": "closecombat"},
51
- {"action": "move", "choice": "flamethrower"},
52
- {"action": "move", "choice": "grassknot"},
53
- {"action": "move", "choice": "machpunch"},
54
- {"action": "switch", "choice": "Starmie"},
55
- {"action": "switch", "choice": "Togekiss"},
56
- {"action": "switch", "choice": "Hippowdon"},
57
- {"action": "switch", "choice": "Jolteon"},
58
- {"action": "switch", "choice": "Heracross"}
59
- ],
60
- "chosen_action": {"action": "move", "choice": "flamethrower"},
61
- "reward": 0.85,
62
- "cumulative_reward": 5.95,
63
- "action_was_illegal": false
64
- },
65
- {
66
- "turn": 4,
67
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Infernape\n- HP: 45.0%\n- Status: Healthy\n- Ability: Blaze\n- Item: Life Orb\n- Stat Modifiers: None\n### Active Opponent\n- Name: Gengar\n- HP: 55.0%\n- Status: Healthy\n- Speed Range: 178-350\n\n## Part B: Full Self Roster\n- Infernape HP:45% OK Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 55.0% | Status: Healthy\n - Revealed moves: shadowball",
68
- "valid_actions": [
69
- {"action": "move", "choice": "closecombat"},
70
- {"action": "move", "choice": "flamethrower"},
71
- {"action": "move", "choice": "grassknot"},
72
- {"action": "move", "choice": "machpunch"},
73
- {"action": "switch", "choice": "Starmie"},
74
- {"action": "switch", "choice": "Togekiss"},
75
- {"action": "switch", "choice": "Hippowdon"},
76
- {"action": "switch", "choice": "Jolteon"},
77
- {"action": "switch", "choice": "Heracross"}
78
- ],
79
- "chosen_action": {"action": "move", "choice": "machpunch"},
80
- "reward": -0.55,
81
- "cumulative_reward": 5.4,
82
- "action_was_illegal": false
83
- },
84
- {
85
- "turn": 5,
86
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Jolteon\n- HP: 100.0%\n- Status: Healthy\n- Ability: Volt Absorb\n- Item: Choice Specs\n- Stat Modifiers: None\n### Active Opponent\n- Name: Gengar\n- HP: 55.0%\n- Status: Healthy\n- Speed Range: 178-350\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 55.0% | Status: Healthy\n - Revealed moves: shadowball, focusblast",
87
- "valid_actions": [
88
- {"action": "move", "choice": "thunderbolt"},
89
- {"action": "move", "choice": "shadowball"},
90
- {"action": "move", "choice": "hiddenpowerice"},
91
- {"action": "move", "choice": "voltswitch"},
92
- {"action": "switch", "choice": "Starmie"},
93
- {"action": "switch", "choice": "Togekiss"},
94
- {"action": "switch", "choice": "Hippowdon"},
95
- {"action": "switch", "choice": "Heracross"}
96
- ],
97
- "chosen_action": {"action": "move", "choice": "shadowball"},
98
- "reward": 2.5,
99
- "cumulative_reward": 7.9,
100
- "action_was_illegal": false
101
- },
102
- {
103
- "turn": 6,
104
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Jolteon\n- HP: 100.0%\n- Status: Healthy\n- Ability: Volt Absorb\n- Item: Choice Specs\n- Stat Modifiers: None\n### Active Opponent\n- Name: Skarmory\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 130-262\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:100% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 100.0% | Status: Healthy",
105
- "valid_actions": [
106
- {"action": "move", "choice": "thunderbolt"},
107
- {"action": "move", "choice": "shadowball"},
108
- {"action": "move", "choice": "hiddenpowerice"},
109
- {"action": "move", "choice": "voltswitch"},
110
- {"action": "switch", "choice": "Starmie"},
111
- {"action": "switch", "choice": "Togekiss"},
112
- {"action": "switch", "choice": "Hippowdon"},
113
- {"action": "switch", "choice": "Heracross"}
114
- ],
115
- "chosen_action": {"action": "move", "choice": "thunderbolt"},
116
- "reward": 1.45,
117
- "cumulative_reward": 9.35,
118
- "action_was_illegal": false
119
- },
120
- {
121
- "turn": 7,
122
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Jolteon\n- HP: 85.0%\n- Status: Healthy\n- Ability: Volt Absorb\n- Item: Choice Specs\n- Stat Modifiers: None\n### Active Opponent\n- Name: Skarmory\n- HP: 42.0%\n- Status: Healthy\n- Speed Range: 130-262\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:85% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 42.0% | Status: Healthy\n - Revealed moves: bravebird, roost",
123
- "valid_actions": [
124
- {"action": "move", "choice": "thunderbolt"},
125
- {"action": "move", "choice": "shadowball"},
126
- {"action": "move", "choice": "hiddenpowerice"},
127
- {"action": "move", "choice": "voltswitch"},
128
- {"action": "switch", "choice": "Starmie"},
129
- {"action": "switch", "choice": "Togekiss"},
130
- {"action": "switch", "choice": "Hippowdon"},
131
- {"action": "switch", "choice": "Heracross"}
132
- ],
133
- "chosen_action": {"action": "move", "choice": "thunderbolt"},
134
- "reward": 1.3,
135
- "cumulative_reward": 10.65,
136
- "action_was_illegal": false
137
- },
138
- {
139
- "turn": 8,
140
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Jolteon\n- HP: 70.0%\n- Status: Healthy\n- Ability: Volt Absorb\n- Item: Choice Specs\n- Stat Modifiers: None\n### Active Opponent\n- Name: Flygon\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 152-299\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:100% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:70% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 0.0% | Status: fnt\n - Revealed moves: bravebird, roost\n- Flygon | Last HP: 100.0% | Status: Healthy",
141
- "valid_actions": [
142
- {"action": "move", "choice": "thunderbolt"},
143
- {"action": "move", "choice": "shadowball"},
144
- {"action": "move", "choice": "hiddenpowerice"},
145
- {"action": "move", "choice": "voltswitch"},
146
- {"action": "switch", "choice": "Starmie"},
147
- {"action": "switch", "choice": "Togekiss"},
148
- {"action": "switch", "choice": "Hippowdon"},
149
- {"action": "switch", "choice": "Heracross"}
150
- ],
151
- "chosen_action": {"action": "switch", "choice": "Starmie"},
152
- "reward": -0.55,
153
- "cumulative_reward": 10.1,
154
- "action_was_illegal": false
155
- },
156
- {
157
- "turn": 9,
158
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Starmie\n- HP: 78.0%\n- Status: Healthy\n- Ability: Natural Cure\n- Item: Leftovers\n- Stat Modifiers: None\n### Active Opponent\n- Name: Flygon\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 152-299\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:78% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:70% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 0.0% | Status: fnt\n - Revealed moves: bravebird, roost\n- Flygon | Last HP: 100.0% | Status: Healthy\n - Revealed moves: earthquake, outrage",
159
- "valid_actions": [
160
- {"action": "move", "choice": "surf"},
161
- {"action": "move", "choice": "thunderbolt"},
162
- {"action": "move", "choice": "icebeam"},
163
- {"action": "move", "choice": "recover"},
164
- {"action": "switch", "choice": "Togekiss"},
165
- {"action": "switch", "choice": "Hippowdon"},
166
- {"action": "switch", "choice": "Jolteon"},
167
- {"action": "switch", "choice": "Heracross"}
168
- ],
169
- "chosen_action": {"action": "move", "choice": "icebeam"},
170
- "reward": 3.45,
171
- "cumulative_reward": 13.55,
172
- "action_was_illegal": false
173
- },
174
- {
175
- "turn": 10,
176
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Starmie\n- HP: 78.0%\n- Status: Healthy\n- Ability: Natural Cure\n- Item: Leftovers\n- Stat Modifiers: None\n### Active Opponent\n- Name: Roserade\n- HP: 100.0%\n- Status: Healthy\n- Speed Range: 156-306\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:78% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:70% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 0.0% | Status: fnt\n - Revealed moves: bravebird, roost\n- Flygon | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake, outrage\n- Roserade | Last HP: 100.0% | Status: Healthy",
177
- "valid_actions": [
178
- {"action": "move", "choice": "surf"},
179
- {"action": "move", "choice": "thunderbolt"},
180
- {"action": "move", "choice": "icebeam"},
181
- {"action": "move", "choice": "recover"},
182
- {"action": "switch", "choice": "Togekiss"},
183
- {"action": "switch", "choice": "Hippowdon"},
184
- {"action": "switch", "choice": "Jolteon"},
185
- {"action": "switch", "choice": "Heracross"}
186
- ],
187
- "chosen_action": {"action": "move", "choice": "icebeam"},
188
- "reward": 0.85,
189
- "cumulative_reward": 14.4,
190
- "action_was_illegal": false
191
- },
192
- {
193
- "turn": 11,
194
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Starmie\n- HP: 50.0%\n- Status: psn\n- Ability: Natural Cure\n- Item: Leftovers\n- Stat Modifiers: None\n### Active Opponent\n- Name: Roserade\n- HP: 45.0%\n- Status: Healthy\n- Speed Range: 156-306\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:50% psn Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:100% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:70% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 0.0% | Status: fnt\n - Revealed moves: bravebird, roost\n- Flygon | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake, outrage\n- Roserade | Last HP: 45.0% | Status: Healthy\n - Revealed moves: toxicspikes, energyball",
195
- "valid_actions": [
196
- {"action": "move", "choice": "surf"},
197
- {"action": "move", "choice": "thunderbolt"},
198
- {"action": "move", "choice": "icebeam"},
199
- {"action": "move", "choice": "recover"},
200
- {"action": "switch", "choice": "Togekiss"},
201
- {"action": "switch", "choice": "Hippowdon"},
202
- {"action": "switch", "choice": "Jolteon"},
203
- {"action": "switch", "choice": "Heracross"}
204
- ],
205
- "chosen_action": {"action": "switch", "choice": "Togekiss"},
206
- "reward": -0.55,
207
- "cumulative_reward": 13.85,
208
- "action_was_illegal": false
209
- },
210
- {
211
- "turn": 12,
212
- "state_markdown": "## Part A: Active Field\n### Active Self\n- Name: Togekiss\n- HP: 75.0%\n- Status: Healthy\n- Ability: Serene Grace\n- Item: Choice Scarf\n- Stat Modifiers: None\n### Active Opponent\n- Name: Roserade\n- HP: 45.0%\n- Status: Healthy\n- Speed Range: 156-306\n\n## Part B: Full Self Roster\n- Infernape HP:0% fnt Item:Life Orb\n Moves: closecombat(F120) | flamethrower(F90) | grassknot(G0) | machpunch(F40)\n- Starmie HP:50% OK Item:Leftovers\n Moves: surf(W90) | thunderbolt(E90) | icebeam(I90) | recover(N0)\n- Togekiss HP:75% OK Item:Choice Scarf\n Moves: airslash(F75) | aurasphere(F80) | trick(N0) | roost(F0)\n- Hippowdon HP:100% OK Item:Leftovers\n Moves: earthquake(G100) | stealthrock(R0) | slackoff(N0) | toxic(P0)\n- Jolteon HP:70% OK Item:Choice Specs\n Moves: thunderbolt(E90) | shadowball(G80) | hiddenpowerice(I60) | voltswitch(E70)\n- Heracross HP:100% OK Item:Choice Band\n Moves: closecombat(F120) | megahorn(B120) | stoneedge(R100) | nightslash(D70)\n\n## Part C: Opponent History\n- Swampert | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake\n- Gengar | Last HP: 0.0% | Status: fnt\n - Revealed moves: shadowball, focusblast\n- Skarmory | Last HP: 0.0% | Status: fnt\n - Revealed moves: bravebird, roost\n- Flygon | Last HP: 0.0% | Status: fnt\n - Revealed moves: earthquake, outrage\n- Roserade | Last HP: 45.0% | Status: Healthy\n - Revealed moves: toxicspikes, energyball, sludgebomb",
213
- "valid_actions": [
214
- {"action": "move", "choice": "airslash"},
215
- {"action": "move", "choice": "aurasphere"},
216
- {"action": "move", "choice": "trick"},
217
- {"action": "move", "choice": "roost"},
218
- {"action": "switch", "choice": "Starmie"},
219
- {"action": "switch", "choice": "Hippowdon"},
220
- {"action": "switch", "choice": "Jolteon"},
221
- {"action": "switch", "choice": "Heracross"}
222
- ],
223
- "chosen_action": {"action": "move", "choice": "airslash"},
224
- "reward": 3.45,
225
- "cumulative_reward": 17.3,
226
- "action_was_illegal": false
227
- }
228
- ]
229
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
battle_logs/replay_battle.json ADDED
@@ -0,0 +1,3566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "meta": {
3
+ "title": "OpenEnv-WolfeClick Replay",
4
+ "model": "Qwen/Qwen3-4B-Instruct-2507 + LoRA grpo-qwen3-4b-run3",
5
+ "format": "gen4randombattle",
6
+ "outcome": "won",
7
+ "natural_finish": true,
8
+ "final_reason": "battle_finished",
9
+ "battle_tag": "battle-gen4randombattle-12",
10
+ "room_path": "/battle-gen4randombattle-12",
11
+ "total_turns": 46,
12
+ "total_reward": 4.4258
13
+ },
14
+ "teams": {
15
+ "player": [
16
+ {
17
+ "name": "xatu"
18
+ },
19
+ {
20
+ "name": "gorebyss"
21
+ },
22
+ {
23
+ "name": "corsola"
24
+ },
25
+ {
26
+ "name": "tangrowth"
27
+ },
28
+ {
29
+ "name": "aggron"
30
+ },
31
+ {
32
+ "name": "ambipom"
33
+ }
34
+ ],
35
+ "opponent": [
36
+ {
37
+ "name": "vigoroth"
38
+ },
39
+ {
40
+ "name": "gorebyss"
41
+ },
42
+ {
43
+ "name": "dialga"
44
+ },
45
+ {
46
+ "name": "wailord"
47
+ },
48
+ {
49
+ "name": "ninetales"
50
+ },
51
+ {
52
+ "name": "salamence"
53
+ }
54
+ ]
55
+ },
56
+ "turns": [
57
+ {
58
+ "turn": 1,
59
+ "battle_turn": 1,
60
+ "player_active_before": {
61
+ "name": "xatu",
62
+ "hp": 100.0,
63
+ "status": "Healthy"
64
+ },
65
+ "opponent_active_before": {
66
+ "name": "vigoroth",
67
+ "hp": 100.0,
68
+ "status": "Healthy"
69
+ },
70
+ "player_action": {
71
+ "action": "move",
72
+ "choice": "grassknot"
73
+ },
74
+ "opponent_action": {
75
+ "type": "move",
76
+ "choice": "Earthquake",
77
+ "source": "p2a: Vigoroth",
78
+ "confidence": "logged"
79
+ },
80
+ "player_active_after": {
81
+ "name": "xatu",
82
+ "hp": 100.0,
83
+ "status": "Healthy"
84
+ },
85
+ "opponent_active_after": {
86
+ "name": "vigoroth",
87
+ "hp": 79.0,
88
+ "status": "Healthy"
89
+ },
90
+ "reward": 2.05,
91
+ "cumulative_reward": 2.05,
92
+ "commentary": [
93
+ "p1a: Xatu used Grass Knot on p2a: Vigoroth.",
94
+ "p2a: Vigoroth dropped to 79/100 HP.",
95
+ "p2a: Vigoroth used Earthquake on p1a: Xatu.",
96
+ "The target was immune."
97
+ ],
98
+ "showdown_events": [
99
+ "move | p1a: Xatu | Grass Knot | p2a: Vigoroth",
100
+ "-damage | p2a: Vigoroth | 79/100",
101
+ "move | p2a: Vigoroth | Earthquake | p1a: Xatu",
102
+ "-immune | p1a: Xatu",
103
+ "upkeep",
104
+ "turn | 2"
105
+ ],
106
+ "valid_actions": [
107
+ {
108
+ "action": "move",
109
+ "choice": "grassknot"
110
+ },
111
+ {
112
+ "action": "move",
113
+ "choice": "psychic"
114
+ },
115
+ {
116
+ "action": "move",
117
+ "choice": "trick"
118
+ },
119
+ {
120
+ "action": "move",
121
+ "choice": "heatwave"
122
+ },
123
+ {
124
+ "action": "switch",
125
+ "choice": "ambipom"
126
+ },
127
+ {
128
+ "action": "switch",
129
+ "choice": "tangrowth"
130
+ },
131
+ {
132
+ "action": "switch",
133
+ "choice": "gorebyss"
134
+ },
135
+ {
136
+ "action": "switch",
137
+ "choice": "aggron"
138
+ },
139
+ {
140
+ "action": "switch",
141
+ "choice": "corsola"
142
+ }
143
+ ]
144
+ },
145
+ {
146
+ "turn": 2,
147
+ "battle_turn": 2,
148
+ "player_active_before": {
149
+ "name": "xatu",
150
+ "hp": 100.0,
151
+ "status": "Healthy"
152
+ },
153
+ "opponent_active_before": {
154
+ "name": "vigoroth",
155
+ "hp": 79.0,
156
+ "status": "Healthy"
157
+ },
158
+ "player_action": {
159
+ "action": "move",
160
+ "choice": "grassknot"
161
+ },
162
+ "opponent_action": {
163
+ "type": "switch",
164
+ "choice": "Gorebyss",
165
+ "source": "p2a: Gorebyss",
166
+ "confidence": "logged"
167
+ },
168
+ "player_active_after": {
169
+ "name": "xatu",
170
+ "hp": 100.0,
171
+ "status": "Healthy"
172
+ },
173
+ "opponent_active_after": {
174
+ "name": "gorebyss",
175
+ "hp": 74.0,
176
+ "status": "Healthy"
177
+ },
178
+ "reward": 0.2,
179
+ "cumulative_reward": 2.25,
180
+ "commentary": [
181
+ "p2a: Gorebyss switched to Gorebyss, L88, M (100/100).",
182
+ "p1a: Xatu used Grass Knot on p2a: Gorebyss.",
183
+ "It was super effective.",
184
+ "p2a: Gorebyss dropped to 74/100 HP."
185
+ ],
186
+ "showdown_events": [
187
+ "switch | p2a: Gorebyss | Gorebyss, L88, M | 100/100",
188
+ "move | p1a: Xatu | Grass Knot | p2a: Gorebyss",
189
+ "-supereffective | p2a: Gorebyss",
190
+ "-damage | p2a: Gorebyss | 74/100",
191
+ "upkeep",
192
+ "turn | 3"
193
+ ],
194
+ "valid_actions": [
195
+ {
196
+ "action": "move",
197
+ "choice": "grassknot"
198
+ },
199
+ {
200
+ "action": "switch",
201
+ "choice": "ambipom"
202
+ },
203
+ {
204
+ "action": "switch",
205
+ "choice": "tangrowth"
206
+ },
207
+ {
208
+ "action": "switch",
209
+ "choice": "gorebyss"
210
+ },
211
+ {
212
+ "action": "switch",
213
+ "choice": "aggron"
214
+ },
215
+ {
216
+ "action": "switch",
217
+ "choice": "corsola"
218
+ }
219
+ ]
220
+ },
221
+ {
222
+ "turn": 3,
223
+ "battle_turn": 3,
224
+ "player_active_before": {
225
+ "name": "xatu",
226
+ "hp": 100.0,
227
+ "status": "Healthy"
228
+ },
229
+ "opponent_active_before": {
230
+ "name": "gorebyss",
231
+ "hp": 74.0,
232
+ "status": "Healthy"
233
+ },
234
+ "player_action": {
235
+ "action": "switch",
236
+ "choice": "gorebyss"
237
+ },
238
+ "opponent_action": {
239
+ "type": "move",
240
+ "choice": "Hidden Power",
241
+ "source": "p2a: Gorebyss",
242
+ "confidence": "logged"
243
+ },
244
+ "player_active_after": {
245
+ "name": "gorebyss",
246
+ "hp": 23.3,
247
+ "status": "Healthy"
248
+ },
249
+ "opponent_active_after": {
250
+ "name": "gorebyss",
251
+ "hp": 64.0,
252
+ "status": "Healthy"
253
+ },
254
+ "reward": -0.7,
255
+ "cumulative_reward": 1.55,
256
+ "commentary": [
257
+ "p1a: Gorebyss switched to Gorebyss, L88, M (240/240).",
258
+ "p2a: Gorebyss used Hidden Power on p1a: Gorebyss.",
259
+ "It was super effective.",
260
+ "p1a: Gorebyss dropped to 56/240 HP.",
261
+ "p2a: Gorebyss dropped to 64/100 | [from] item: Life Orb HP."
262
+ ],
263
+ "showdown_events": [
264
+ "switch | p1a: Gorebyss | Gorebyss, L88, M | 240/240",
265
+ "move | p2a: Gorebyss | Hidden Power | p1a: Gorebyss",
266
+ "-supereffective | p1a: Gorebyss",
267
+ "-damage | p1a: Gorebyss | 56/240",
268
+ "-damage | p2a: Gorebyss | 64/100 | [from] item: Life Orb",
269
+ "upkeep",
270
+ "turn | 4"
271
+ ],
272
+ "valid_actions": [
273
+ {
274
+ "action": "move",
275
+ "choice": "grassknot"
276
+ },
277
+ {
278
+ "action": "switch",
279
+ "choice": "ambipom"
280
+ },
281
+ {
282
+ "action": "switch",
283
+ "choice": "tangrowth"
284
+ },
285
+ {
286
+ "action": "switch",
287
+ "choice": "gorebyss"
288
+ },
289
+ {
290
+ "action": "switch",
291
+ "choice": "aggron"
292
+ },
293
+ {
294
+ "action": "switch",
295
+ "choice": "corsola"
296
+ }
297
+ ]
298
+ },
299
+ {
300
+ "turn": 4,
301
+ "battle_turn": 4,
302
+ "player_active_before": {
303
+ "name": "gorebyss",
304
+ "hp": 23.3,
305
+ "status": "Healthy"
306
+ },
307
+ "opponent_active_before": {
308
+ "name": "gorebyss",
309
+ "hp": 64.0,
310
+ "status": "Healthy"
311
+ },
312
+ "player_action": {
313
+ "action": "switch",
314
+ "choice": "corsola"
315
+ },
316
+ "opponent_action": {
317
+ "type": "move",
318
+ "choice": "Ice Beam",
319
+ "source": "p2a: Gorebyss",
320
+ "confidence": "logged"
321
+ },
322
+ "player_active_after": {
323
+ "name": "corsola",
324
+ "hp": 87.2,
325
+ "status": "Healthy"
326
+ },
327
+ "opponent_active_after": {
328
+ "name": "gorebyss",
329
+ "hp": 54.0,
330
+ "status": "Healthy"
331
+ },
332
+ "reward": 0.2375,
333
+ "cumulative_reward": 1.7875,
334
+ "commentary": [
335
+ "p1a: Corsola switched to Corsola, L98, M (266/266).",
336
+ "p2a: Gorebyss used Ice Beam on p1a: Corsola.",
337
+ "It was resisted.",
338
+ "p1a: Corsola dropped to 216/266 HP.",
339
+ "p2a: Gorebyss dropped to 54/100 | [from] item: Life Orb HP.",
340
+ "p1a: Corsola recovered to 232/266 | [from] item: Leftovers HP."
341
+ ],
342
+ "showdown_events": [
343
+ "switch | p1a: Corsola | Corsola, L98, M | 266/266",
344
+ "move | p2a: Gorebyss | Ice Beam | p1a: Corsola",
345
+ "-resisted | p1a: Corsola",
346
+ "-damage | p1a: Corsola | 216/266",
347
+ "-damage | p2a: Gorebyss | 54/100 | [from] item: Life Orb",
348
+ "-heal | p1a: Corsola | 232/266 | [from] item: Leftovers",
349
+ "upkeep",
350
+ "turn | 5"
351
+ ],
352
+ "valid_actions": [
353
+ {
354
+ "action": "move",
355
+ "choice": "raindance"
356
+ },
357
+ {
358
+ "action": "move",
359
+ "choice": "hiddenpowergrass"
360
+ },
361
+ {
362
+ "action": "move",
363
+ "choice": "icebeam"
364
+ },
365
+ {
366
+ "action": "move",
367
+ "choice": "hydropump"
368
+ },
369
+ {
370
+ "action": "switch",
371
+ "choice": "ambipom"
372
+ },
373
+ {
374
+ "action": "switch",
375
+ "choice": "tangrowth"
376
+ },
377
+ {
378
+ "action": "switch",
379
+ "choice": "xatu"
380
+ },
381
+ {
382
+ "action": "switch",
383
+ "choice": "aggron"
384
+ },
385
+ {
386
+ "action": "switch",
387
+ "choice": "corsola"
388
+ }
389
+ ]
390
+ },
391
+ {
392
+ "turn": 5,
393
+ "battle_turn": 5,
394
+ "player_active_before": {
395
+ "name": "corsola",
396
+ "hp": 87.2,
397
+ "status": "Healthy"
398
+ },
399
+ "opponent_active_before": {
400
+ "name": "gorebyss",
401
+ "hp": 54.0,
402
+ "status": "Healthy"
403
+ },
404
+ "player_action": {
405
+ "action": "switch",
406
+ "choice": "tangrowth"
407
+ },
408
+ "opponent_action": {
409
+ "type": "switch",
410
+ "choice": "Dialga",
411
+ "source": "p2a: Dialga",
412
+ "confidence": "logged"
413
+ },
414
+ "player_active_after": {
415
+ "name": "tangrowth",
416
+ "hp": 100.0,
417
+ "status": "Healthy"
418
+ },
419
+ "opponent_active_after": {
420
+ "name": "dialga",
421
+ "hp": 100.0,
422
+ "status": "Healthy"
423
+ },
424
+ "reward": -0.05,
425
+ "cumulative_reward": 1.7375,
426
+ "commentary": [
427
+ "p2a: Dialga switched to Dialga, L70 (100/100).",
428
+ "p1a: Tangrowth switched to Tangrowth, L89, F (323/323)."
429
+ ],
430
+ "showdown_events": [
431
+ "switch | p2a: Dialga | Dialga, L70 | 100/100",
432
+ "-ability | p2a: Dialga | Pressure",
433
+ "switch | p1a: Tangrowth | Tangrowth, L89, F | 323/323",
434
+ "upkeep",
435
+ "turn | 6"
436
+ ],
437
+ "valid_actions": [
438
+ {
439
+ "action": "move",
440
+ "choice": "explosion"
441
+ },
442
+ {
443
+ "action": "move",
444
+ "choice": "recover"
445
+ },
446
+ {
447
+ "action": "move",
448
+ "choice": "surf"
449
+ },
450
+ {
451
+ "action": "move",
452
+ "choice": "stealthrock"
453
+ },
454
+ {
455
+ "action": "switch",
456
+ "choice": "ambipom"
457
+ },
458
+ {
459
+ "action": "switch",
460
+ "choice": "tangrowth"
461
+ },
462
+ {
463
+ "action": "switch",
464
+ "choice": "xatu"
465
+ },
466
+ {
467
+ "action": "switch",
468
+ "choice": "aggron"
469
+ },
470
+ {
471
+ "action": "switch",
472
+ "choice": "gorebyss"
473
+ }
474
+ ]
475
+ },
476
+ {
477
+ "turn": 6,
478
+ "battle_turn": 6,
479
+ "player_active_before": {
480
+ "name": "tangrowth",
481
+ "hp": 100.0,
482
+ "status": "Healthy"
483
+ },
484
+ "opponent_active_before": {
485
+ "name": "dialga",
486
+ "hp": 100.0,
487
+ "status": "Healthy"
488
+ },
489
+ "player_action": {
490
+ "action": "move",
491
+ "choice": "earthquake"
492
+ },
493
+ "opponent_action": {
494
+ "type": "move",
495
+ "choice": "Rest",
496
+ "source": "p2a: Dialga",
497
+ "confidence": "logged"
498
+ },
499
+ "player_active_after": {
500
+ "name": "tangrowth",
501
+ "hp": 100.0,
502
+ "status": "Healthy"
503
+ },
504
+ "opponent_active_after": {
505
+ "name": "vigoroth",
506
+ "hp": 79.0,
507
+ "status": "Healthy"
508
+ },
509
+ "reward": 6.2833,
510
+ "cumulative_reward": 8.0208,
511
+ "commentary": [
512
+ "p2a: Dialga used Rest on p2a: Dialga.",
513
+ "p1a: Tangrowth used Earthquake on p2a: Dialga.",
514
+ "It was super effective.",
515
+ "p2a: Dialga dropped to 0 fnt HP.",
516
+ "p2a: Dialga fainted.",
517
+ "p2a: Vigoroth switched to Vigoroth, L89, M (79/100)."
518
+ ],
519
+ "showdown_events": [
520
+ "move | p2a: Dialga | Rest | p2a: Dialga",
521
+ "-fail | p2a: Dialga | heal",
522
+ "move | p1a: Tangrowth | Earthquake | p2a: Dialga",
523
+ "-supereffective | p2a: Dialga",
524
+ "-crit | p2a: Dialga",
525
+ "-damage | p2a: Dialga | 0 fnt",
526
+ "faint | p2a: Dialga",
527
+ "upkeep",
528
+ "switch | p2a: Vigoroth | Vigoroth, L89, M | 79/100",
529
+ "turn | 7"
530
+ ],
531
+ "valid_actions": [
532
+ {
533
+ "action": "move",
534
+ "choice": "earthquake"
535
+ },
536
+ {
537
+ "action": "move",
538
+ "choice": "swordsdance"
539
+ },
540
+ {
541
+ "action": "move",
542
+ "choice": "rockslide"
543
+ },
544
+ {
545
+ "action": "move",
546
+ "choice": "powerwhip"
547
+ },
548
+ {
549
+ "action": "switch",
550
+ "choice": "ambipom"
551
+ },
552
+ {
553
+ "action": "switch",
554
+ "choice": "corsola"
555
+ },
556
+ {
557
+ "action": "switch",
558
+ "choice": "xatu"
559
+ },
560
+ {
561
+ "action": "switch",
562
+ "choice": "aggron"
563
+ },
564
+ {
565
+ "action": "switch",
566
+ "choice": "gorebyss"
567
+ }
568
+ ]
569
+ },
570
+ {
571
+ "turn": 7,
572
+ "battle_turn": 7,
573
+ "player_active_before": {
574
+ "name": "tangrowth",
575
+ "hp": 100.0,
576
+ "status": "Healthy"
577
+ },
578
+ "opponent_active_before": {
579
+ "name": "vigoroth",
580
+ "hp": 79.0,
581
+ "status": "Healthy"
582
+ },
583
+ "player_action": {
584
+ "action": "move",
585
+ "choice": "earthquake"
586
+ },
587
+ "opponent_action": {
588
+ "type": "switch",
589
+ "choice": "Wailord",
590
+ "source": "p2a: Wailord",
591
+ "confidence": "logged"
592
+ },
593
+ "player_active_after": {
594
+ "name": "tangrowth",
595
+ "hp": 100.0,
596
+ "status": "Healthy"
597
+ },
598
+ "opponent_active_after": {
599
+ "name": "wailord",
600
+ "hp": 73.0,
601
+ "status": "Healthy"
602
+ },
603
+ "reward": -0.05,
604
+ "cumulative_reward": 7.9708,
605
+ "commentary": [
606
+ "p2a: Wailord switched to Wailord, L88, M (100/100).",
607
+ "p1a: Tangrowth used Earthquake on p2a: Wailord.",
608
+ "p2a: Wailord dropped to 73/100 HP."
609
+ ],
610
+ "showdown_events": [
611
+ "switch | p2a: Wailord | Wailord, L88, M | 100/100",
612
+ "move | p1a: Tangrowth | Earthquake | p2a: Wailord",
613
+ "-damage | p2a: Wailord | 73/100",
614
+ "upkeep",
615
+ "turn | 8"
616
+ ],
617
+ "valid_actions": [
618
+ {
619
+ "action": "move",
620
+ "choice": "earthquake"
621
+ },
622
+ {
623
+ "action": "move",
624
+ "choice": "swordsdance"
625
+ },
626
+ {
627
+ "action": "move",
628
+ "choice": "rockslide"
629
+ },
630
+ {
631
+ "action": "move",
632
+ "choice": "powerwhip"
633
+ },
634
+ {
635
+ "action": "switch",
636
+ "choice": "ambipom"
637
+ },
638
+ {
639
+ "action": "switch",
640
+ "choice": "corsola"
641
+ },
642
+ {
643
+ "action": "switch",
644
+ "choice": "xatu"
645
+ },
646
+ {
647
+ "action": "switch",
648
+ "choice": "aggron"
649
+ },
650
+ {
651
+ "action": "switch",
652
+ "choice": "gorebyss"
653
+ }
654
+ ]
655
+ },
656
+ {
657
+ "turn": 8,
658
+ "battle_turn": 8,
659
+ "player_active_before": {
660
+ "name": "tangrowth",
661
+ "hp": 100.0,
662
+ "status": "Healthy"
663
+ },
664
+ "opponent_active_before": {
665
+ "name": "wailord",
666
+ "hp": 73.0,
667
+ "status": "Healthy"
668
+ },
669
+ "player_action": {
670
+ "action": "move",
671
+ "choice": "earthquake"
672
+ },
673
+ "opponent_action": {
674
+ "type": "switch",
675
+ "choice": "Ninetales",
676
+ "source": "p2a: Ninetales",
677
+ "confidence": "logged"
678
+ },
679
+ "player_active_after": {
680
+ "name": "tangrowth",
681
+ "hp": 100.0,
682
+ "status": "Healthy"
683
+ },
684
+ "opponent_active_after": {
685
+ "name": "ninetales",
686
+ "hp": 36.0,
687
+ "status": "Healthy"
688
+ },
689
+ "reward": 0.26,
690
+ "cumulative_reward": 8.2308,
691
+ "commentary": [
692
+ "p2a: Ninetales switched to Ninetales, L85, F (100/100).",
693
+ "p1a: Tangrowth used Earthquake on p2a: Ninetales.",
694
+ "It was super effective.",
695
+ "p2a: Ninetales dropped to 36/100 HP."
696
+ ],
697
+ "showdown_events": [
698
+ "switch | p2a: Ninetales | Ninetales, L85, F | 100/100",
699
+ "move | p1a: Tangrowth | Earthquake | p2a: Ninetales",
700
+ "-supereffective | p2a: Ninetales",
701
+ "-damage | p2a: Ninetales | 36/100",
702
+ "upkeep",
703
+ "turn | 9"
704
+ ],
705
+ "valid_actions": [
706
+ {
707
+ "action": "move",
708
+ "choice": "earthquake"
709
+ },
710
+ {
711
+ "action": "move",
712
+ "choice": "swordsdance"
713
+ },
714
+ {
715
+ "action": "move",
716
+ "choice": "rockslide"
717
+ },
718
+ {
719
+ "action": "move",
720
+ "choice": "powerwhip"
721
+ },
722
+ {
723
+ "action": "switch",
724
+ "choice": "ambipom"
725
+ },
726
+ {
727
+ "action": "switch",
728
+ "choice": "corsola"
729
+ },
730
+ {
731
+ "action": "switch",
732
+ "choice": "xatu"
733
+ },
734
+ {
735
+ "action": "switch",
736
+ "choice": "aggron"
737
+ },
738
+ {
739
+ "action": "switch",
740
+ "choice": "gorebyss"
741
+ }
742
+ ]
743
+ },
744
+ {
745
+ "turn": 9,
746
+ "battle_turn": 9,
747
+ "player_active_before": {
748
+ "name": "tangrowth",
749
+ "hp": 100.0,
750
+ "status": "Healthy"
751
+ },
752
+ "opponent_active_before": {
753
+ "name": "ninetales",
754
+ "hp": 36.0,
755
+ "status": "Healthy"
756
+ },
757
+ "player_action": {
758
+ "action": "move",
759
+ "choice": "earthquake"
760
+ },
761
+ "opponent_action": {
762
+ "type": "move",
763
+ "choice": "Hypnosis",
764
+ "source": "p2a: Ninetales",
765
+ "confidence": "logged"
766
+ },
767
+ "player_active_after": {
768
+ "name": "tangrowth",
769
+ "hp": 100.0,
770
+ "status": "Healthy"
771
+ },
772
+ "opponent_active_after": {
773
+ "name": "vigoroth",
774
+ "hp": 79.0,
775
+ "status": "Healthy"
776
+ },
777
+ "reward": 3.67,
778
+ "cumulative_reward": 11.9008,
779
+ "commentary": [
780
+ "p2a: Ninetales used Hypnosis on p1a: Tangrowth | [miss].",
781
+ "p1a: Tangrowth used Earthquake on p2a: Ninetales.",
782
+ "It was super effective.",
783
+ "p2a: Ninetales dropped to 0 fnt HP.",
784
+ "p2a: Ninetales fainted.",
785
+ "p2a: Vigoroth switched to Vigoroth, L89, M (79/100)."
786
+ ],
787
+ "showdown_events": [
788
+ "move | p2a: Ninetales | Hypnosis | p1a: Tangrowth | [miss]",
789
+ "-miss | p2a: Ninetales | p1a: Tangrowth",
790
+ "move | p1a: Tangrowth | Earthquake | p2a: Ninetales",
791
+ "-supereffective | p2a: Ninetales",
792
+ "-damage | p2a: Ninetales | 0 fnt",
793
+ "faint | p2a: Ninetales",
794
+ "upkeep",
795
+ "switch | p2a: Vigoroth | Vigoroth, L89, M | 79/100",
796
+ "turn | 10"
797
+ ],
798
+ "valid_actions": [
799
+ {
800
+ "action": "move",
801
+ "choice": "earthquake"
802
+ },
803
+ {
804
+ "action": "move",
805
+ "choice": "swordsdance"
806
+ },
807
+ {
808
+ "action": "move",
809
+ "choice": "rockslide"
810
+ },
811
+ {
812
+ "action": "move",
813
+ "choice": "powerwhip"
814
+ },
815
+ {
816
+ "action": "switch",
817
+ "choice": "ambipom"
818
+ },
819
+ {
820
+ "action": "switch",
821
+ "choice": "corsola"
822
+ },
823
+ {
824
+ "action": "switch",
825
+ "choice": "xatu"
826
+ },
827
+ {
828
+ "action": "switch",
829
+ "choice": "aggron"
830
+ },
831
+ {
832
+ "action": "switch",
833
+ "choice": "gorebyss"
834
+ }
835
+ ]
836
+ },
837
+ {
838
+ "turn": 10,
839
+ "battle_turn": 10,
840
+ "player_active_before": {
841
+ "name": "tangrowth",
842
+ "hp": 100.0,
843
+ "status": "Healthy"
844
+ },
845
+ "opponent_active_before": {
846
+ "name": "vigoroth",
847
+ "hp": 79.0,
848
+ "status": "Healthy"
849
+ },
850
+ "player_action": {
851
+ "action": "move",
852
+ "choice": "earthquake"
853
+ },
854
+ "opponent_action": {
855
+ "type": "switch",
856
+ "choice": "Gorebyss",
857
+ "source": "p2a: Gorebyss",
858
+ "confidence": "logged"
859
+ },
860
+ "player_active_after": {
861
+ "name": "tangrowth",
862
+ "hp": 100.0,
863
+ "status": "Healthy"
864
+ },
865
+ "opponent_active_after": {
866
+ "name": "gorebyss",
867
+ "hp": 26.0,
868
+ "status": "Healthy"
869
+ },
870
+ "reward": 0.51,
871
+ "cumulative_reward": 12.4108,
872
+ "commentary": [
873
+ "p2a: Gorebyss switched to Gorebyss, L88, M (54/100).",
874
+ "p1a: Tangrowth used Earthquake on p2a: Gorebyss.",
875
+ "p2a: Gorebyss dropped to 26/100 HP."
876
+ ],
877
+ "showdown_events": [
878
+ "switch | p2a: Gorebyss | Gorebyss, L88, M | 54/100",
879
+ "move | p1a: Tangrowth | Earthquake | p2a: Gorebyss",
880
+ "-damage | p2a: Gorebyss | 26/100",
881
+ "upkeep",
882
+ "turn | 11"
883
+ ],
884
+ "valid_actions": [
885
+ {
886
+ "action": "move",
887
+ "choice": "earthquake"
888
+ },
889
+ {
890
+ "action": "move",
891
+ "choice": "swordsdance"
892
+ },
893
+ {
894
+ "action": "move",
895
+ "choice": "rockslide"
896
+ },
897
+ {
898
+ "action": "move",
899
+ "choice": "powerwhip"
900
+ },
901
+ {
902
+ "action": "switch",
903
+ "choice": "ambipom"
904
+ },
905
+ {
906
+ "action": "switch",
907
+ "choice": "corsola"
908
+ },
909
+ {
910
+ "action": "switch",
911
+ "choice": "xatu"
912
+ },
913
+ {
914
+ "action": "switch",
915
+ "choice": "aggron"
916
+ },
917
+ {
918
+ "action": "switch",
919
+ "choice": "gorebyss"
920
+ }
921
+ ]
922
+ },
923
+ {
924
+ "turn": 11,
925
+ "battle_turn": 11,
926
+ "player_active_before": {
927
+ "name": "tangrowth",
928
+ "hp": 100.0,
929
+ "status": "Healthy"
930
+ },
931
+ "opponent_active_before": {
932
+ "name": "gorebyss",
933
+ "hp": 26.0,
934
+ "status": "Healthy"
935
+ },
936
+ "player_action": {
937
+ "action": "move",
938
+ "choice": "earthquake"
939
+ },
940
+ "opponent_action": {
941
+ "type": "switch",
942
+ "choice": "Wailord",
943
+ "source": "p2a: Wailord",
944
+ "confidence": "logged"
945
+ },
946
+ "player_active_after": {
947
+ "name": "tangrowth",
948
+ "hp": 100.0,
949
+ "status": "Healthy"
950
+ },
951
+ "opponent_active_after": {
952
+ "name": "wailord",
953
+ "hp": 46.0,
954
+ "status": "Healthy"
955
+ },
956
+ "reward": 0.49,
957
+ "cumulative_reward": 12.9008,
958
+ "commentary": [
959
+ "p2a: Wailord switched to Wailord, L88, M (73/100).",
960
+ "p1a: Tangrowth used Earthquake on p2a: Wailord.",
961
+ "p2a: Wailord dropped to 46/100 HP."
962
+ ],
963
+ "showdown_events": [
964
+ "switch | p2a: Wailord | Wailord, L88, M | 73/100",
965
+ "move | p1a: Tangrowth | Earthquake | p2a: Wailord",
966
+ "-damage | p2a: Wailord | 46/100",
967
+ "upkeep",
968
+ "turn | 12"
969
+ ],
970
+ "valid_actions": [
971
+ {
972
+ "action": "move",
973
+ "choice": "earthquake"
974
+ },
975
+ {
976
+ "action": "move",
977
+ "choice": "swordsdance"
978
+ },
979
+ {
980
+ "action": "move",
981
+ "choice": "rockslide"
982
+ },
983
+ {
984
+ "action": "move",
985
+ "choice": "powerwhip"
986
+ },
987
+ {
988
+ "action": "switch",
989
+ "choice": "ambipom"
990
+ },
991
+ {
992
+ "action": "switch",
993
+ "choice": "corsola"
994
+ },
995
+ {
996
+ "action": "switch",
997
+ "choice": "xatu"
998
+ },
999
+ {
1000
+ "action": "switch",
1001
+ "choice": "aggron"
1002
+ },
1003
+ {
1004
+ "action": "switch",
1005
+ "choice": "gorebyss"
1006
+ }
1007
+ ]
1008
+ },
1009
+ {
1010
+ "turn": 12,
1011
+ "battle_turn": 12,
1012
+ "player_active_before": {
1013
+ "name": "tangrowth",
1014
+ "hp": 100.0,
1015
+ "status": "Healthy"
1016
+ },
1017
+ "opponent_active_before": {
1018
+ "name": "wailord",
1019
+ "hp": 46.0,
1020
+ "status": "Healthy"
1021
+ },
1022
+ "player_action": {
1023
+ "action": "move",
1024
+ "choice": "earthquake"
1025
+ },
1026
+ "opponent_action": {
1027
+ "type": "switch",
1028
+ "choice": "Vigoroth",
1029
+ "source": "p2a: Vigoroth",
1030
+ "confidence": "logged"
1031
+ },
1032
+ "player_active_after": {
1033
+ "name": "tangrowth",
1034
+ "hp": 100.0,
1035
+ "status": "Healthy"
1036
+ },
1037
+ "opponent_active_after": {
1038
+ "name": "vigoroth",
1039
+ "hp": 52.0,
1040
+ "status": "Healthy"
1041
+ },
1042
+ "reward": 0.49,
1043
+ "cumulative_reward": 13.3908,
1044
+ "commentary": [
1045
+ "p2a: Vigoroth switched to Vigoroth, L89, M (79/100).",
1046
+ "p1a: Tangrowth used Earthquake on p2a: Vigoroth.",
1047
+ "p2a: Vigoroth dropped to 52/100 HP."
1048
+ ],
1049
+ "showdown_events": [
1050
+ "switch | p2a: Vigoroth | Vigoroth, L89, M | 79/100",
1051
+ "move | p1a: Tangrowth | Earthquake | p2a: Vigoroth",
1052
+ "-damage | p2a: Vigoroth | 52/100",
1053
+ "upkeep",
1054
+ "turn | 13"
1055
+ ],
1056
+ "valid_actions": [
1057
+ {
1058
+ "action": "move",
1059
+ "choice": "earthquake"
1060
+ },
1061
+ {
1062
+ "action": "move",
1063
+ "choice": "swordsdance"
1064
+ },
1065
+ {
1066
+ "action": "move",
1067
+ "choice": "rockslide"
1068
+ },
1069
+ {
1070
+ "action": "move",
1071
+ "choice": "powerwhip"
1072
+ },
1073
+ {
1074
+ "action": "switch",
1075
+ "choice": "ambipom"
1076
+ },
1077
+ {
1078
+ "action": "switch",
1079
+ "choice": "corsola"
1080
+ },
1081
+ {
1082
+ "action": "switch",
1083
+ "choice": "xatu"
1084
+ },
1085
+ {
1086
+ "action": "switch",
1087
+ "choice": "aggron"
1088
+ },
1089
+ {
1090
+ "action": "switch",
1091
+ "choice": "gorebyss"
1092
+ }
1093
+ ]
1094
+ },
1095
+ {
1096
+ "turn": 13,
1097
+ "battle_turn": 13,
1098
+ "player_active_before": {
1099
+ "name": "tangrowth",
1100
+ "hp": 100.0,
1101
+ "status": "Healthy"
1102
+ },
1103
+ "opponent_active_before": {
1104
+ "name": "vigoroth",
1105
+ "hp": 52.0,
1106
+ "status": "Healthy"
1107
+ },
1108
+ "player_action": {
1109
+ "action": "move",
1110
+ "choice": "earthquake"
1111
+ },
1112
+ "opponent_action": {
1113
+ "type": "move",
1114
+ "choice": "Night Slash",
1115
+ "source": "p2a: Vigoroth",
1116
+ "confidence": "logged"
1117
+ },
1118
+ "player_active_after": {
1119
+ "name": "tangrowth",
1120
+ "hp": 92.6,
1121
+ "status": "Healthy"
1122
+ },
1123
+ "opponent_active_after": {
1124
+ "name": "vigoroth",
1125
+ "hp": 15.0,
1126
+ "status": "Healthy"
1127
+ },
1128
+ "reward": 0.54,
1129
+ "cumulative_reward": 13.9308,
1130
+ "commentary": [
1131
+ "p2a: Vigoroth used Night Slash on p1a: Tangrowth.",
1132
+ "p1a: Tangrowth dropped to 279/323 HP.",
1133
+ "p2a: Vigoroth dropped to 43/100 | [from] item: Life Orb HP.",
1134
+ "p1a: Tangrowth used Earthquake on p2a: Vigoroth.",
1135
+ "p2a: Vigoroth dropped to 15/100 HP.",
1136
+ "p1a: Tangrowth recovered to 299/323 | [from] item: Leftovers HP."
1137
+ ],
1138
+ "showdown_events": [
1139
+ "move | p2a: Vigoroth | Night Slash | p1a: Tangrowth",
1140
+ "-damage | p1a: Tangrowth | 279/323",
1141
+ "-damage | p2a: Vigoroth | 43/100 | [from] item: Life Orb",
1142
+ "move | p1a: Tangrowth | Earthquake | p2a: Vigoroth",
1143
+ "-damage | p2a: Vigoroth | 15/100",
1144
+ "-heal | p1a: Tangrowth | 299/323 | [from] item: Leftovers",
1145
+ "upkeep",
1146
+ "turn | 14"
1147
+ ],
1148
+ "valid_actions": [
1149
+ {
1150
+ "action": "move",
1151
+ "choice": "earthquake"
1152
+ },
1153
+ {
1154
+ "action": "move",
1155
+ "choice": "swordsdance"
1156
+ },
1157
+ {
1158
+ "action": "move",
1159
+ "choice": "rockslide"
1160
+ },
1161
+ {
1162
+ "action": "move",
1163
+ "choice": "powerwhip"
1164
+ },
1165
+ {
1166
+ "action": "switch",
1167
+ "choice": "ambipom"
1168
+ },
1169
+ {
1170
+ "action": "switch",
1171
+ "choice": "corsola"
1172
+ },
1173
+ {
1174
+ "action": "switch",
1175
+ "choice": "xatu"
1176
+ },
1177
+ {
1178
+ "action": "switch",
1179
+ "choice": "aggron"
1180
+ },
1181
+ {
1182
+ "action": "switch",
1183
+ "choice": "gorebyss"
1184
+ }
1185
+ ]
1186
+ },
1187
+ {
1188
+ "turn": 14,
1189
+ "battle_turn": 14,
1190
+ "player_active_before": {
1191
+ "name": "tangrowth",
1192
+ "hp": 92.6,
1193
+ "status": "Healthy"
1194
+ },
1195
+ "opponent_active_before": {
1196
+ "name": "vigoroth",
1197
+ "hp": 15.0,
1198
+ "status": "Healthy"
1199
+ },
1200
+ "player_action": {
1201
+ "action": "move",
1202
+ "choice": "earthquake"
1203
+ },
1204
+ "opponent_action": {
1205
+ "type": "move",
1206
+ "choice": "Return",
1207
+ "source": "p2a: Vigoroth",
1208
+ "confidence": "logged"
1209
+ },
1210
+ "player_active_after": {
1211
+ "name": "tangrowth",
1212
+ "hp": 70.6,
1213
+ "status": "Healthy"
1214
+ },
1215
+ "opponent_active_after": {
1216
+ "name": "gorebyss",
1217
+ "hp": 26.0,
1218
+ "status": "Healthy"
1219
+ },
1220
+ "reward": 2.8063,
1221
+ "cumulative_reward": 16.7371,
1222
+ "commentary": [
1223
+ "p2a: Vigoroth used Return on p1a: Tangrowth.",
1224
+ "p1a: Tangrowth dropped to 208/323 HP.",
1225
+ "p2a: Vigoroth dropped to 5/100 | [from] item: Life Orb HP.",
1226
+ "p1a: Tangrowth used Earthquake on p2a: Vigoroth.",
1227
+ "p2a: Vigoroth dropped to 0 fnt HP.",
1228
+ "p2a: Vigoroth fainted.",
1229
+ "p1a: Tangrowth recovered to 228/323 | [from] item: Leftovers HP.",
1230
+ "p2a: Gorebyss switched to Gorebyss, L88, M (26/100)."
1231
+ ],
1232
+ "showdown_events": [
1233
+ "move | p2a: Vigoroth | Return | p1a: Tangrowth",
1234
+ "-damage | p1a: Tangrowth | 208/323",
1235
+ "-damage | p2a: Vigoroth | 5/100 | [from] item: Life Orb",
1236
+ "move | p1a: Tangrowth | Earthquake | p2a: Vigoroth",
1237
+ "-damage | p2a: Vigoroth | 0 fnt",
1238
+ "faint | p2a: Vigoroth",
1239
+ "-heal | p1a: Tangrowth | 228/323 | [from] item: Leftovers",
1240
+ "upkeep",
1241
+ "switch | p2a: Gorebyss | Gorebyss, L88, M | 26/100",
1242
+ "turn | 15"
1243
+ ],
1244
+ "valid_actions": [
1245
+ {
1246
+ "action": "move",
1247
+ "choice": "earthquake"
1248
+ },
1249
+ {
1250
+ "action": "move",
1251
+ "choice": "swordsdance"
1252
+ },
1253
+ {
1254
+ "action": "move",
1255
+ "choice": "rockslide"
1256
+ },
1257
+ {
1258
+ "action": "move",
1259
+ "choice": "powerwhip"
1260
+ },
1261
+ {
1262
+ "action": "switch",
1263
+ "choice": "ambipom"
1264
+ },
1265
+ {
1266
+ "action": "switch",
1267
+ "choice": "corsola"
1268
+ },
1269
+ {
1270
+ "action": "switch",
1271
+ "choice": "xatu"
1272
+ },
1273
+ {
1274
+ "action": "switch",
1275
+ "choice": "aggron"
1276
+ },
1277
+ {
1278
+ "action": "switch",
1279
+ "choice": "gorebyss"
1280
+ }
1281
+ ]
1282
+ },
1283
+ {
1284
+ "turn": 15,
1285
+ "battle_turn": 15,
1286
+ "player_active_before": {
1287
+ "name": "tangrowth",
1288
+ "hp": 70.6,
1289
+ "status": "Healthy"
1290
+ },
1291
+ "opponent_active_before": {
1292
+ "name": "gorebyss",
1293
+ "hp": 26.0,
1294
+ "status": "Healthy"
1295
+ },
1296
+ "player_action": {
1297
+ "action": "move",
1298
+ "choice": "earthquake"
1299
+ },
1300
+ "opponent_action": {
1301
+ "type": "switch",
1302
+ "choice": "Wailord",
1303
+ "source": "p2a: Wailord",
1304
+ "confidence": "logged"
1305
+ },
1306
+ "player_active_after": {
1307
+ "name": "tangrowth",
1308
+ "hp": 76.8,
1309
+ "status": "Healthy"
1310
+ },
1311
+ "opponent_active_after": {
1312
+ "name": "wailord",
1313
+ "hp": 19.0,
1314
+ "status": "Healthy"
1315
+ },
1316
+ "reward": 0.615,
1317
+ "cumulative_reward": 17.3521,
1318
+ "commentary": [
1319
+ "p2a: Wailord switched to Wailord, L88, M (46/100).",
1320
+ "p1a: Tangrowth used Earthquake on p2a: Wailord.",
1321
+ "p2a: Wailord dropped to 19/100 HP.",
1322
+ "p1a: Tangrowth recovered to 248/323 | [from] item: Leftovers HP."
1323
+ ],
1324
+ "showdown_events": [
1325
+ "switch | p2a: Wailord | Wailord, L88, M | 46/100",
1326
+ "move | p1a: Tangrowth | Earthquake | p2a: Wailord",
1327
+ "-damage | p2a: Wailord | 19/100",
1328
+ "-heal | p1a: Tangrowth | 248/323 | [from] item: Leftovers",
1329
+ "upkeep",
1330
+ "turn | 16"
1331
+ ],
1332
+ "valid_actions": [
1333
+ {
1334
+ "action": "move",
1335
+ "choice": "earthquake"
1336
+ },
1337
+ {
1338
+ "action": "move",
1339
+ "choice": "swordsdance"
1340
+ },
1341
+ {
1342
+ "action": "move",
1343
+ "choice": "rockslide"
1344
+ },
1345
+ {
1346
+ "action": "move",
1347
+ "choice": "powerwhip"
1348
+ },
1349
+ {
1350
+ "action": "switch",
1351
+ "choice": "ambipom"
1352
+ },
1353
+ {
1354
+ "action": "switch",
1355
+ "choice": "corsola"
1356
+ },
1357
+ {
1358
+ "action": "switch",
1359
+ "choice": "xatu"
1360
+ },
1361
+ {
1362
+ "action": "switch",
1363
+ "choice": "aggron"
1364
+ },
1365
+ {
1366
+ "action": "switch",
1367
+ "choice": "gorebyss"
1368
+ }
1369
+ ]
1370
+ },
1371
+ {
1372
+ "turn": 16,
1373
+ "battle_turn": 16,
1374
+ "player_active_before": {
1375
+ "name": "tangrowth",
1376
+ "hp": 76.8,
1377
+ "status": "Healthy"
1378
+ },
1379
+ "opponent_active_before": {
1380
+ "name": "wailord",
1381
+ "hp": 19.0,
1382
+ "status": "Healthy"
1383
+ },
1384
+ "player_action": {
1385
+ "action": "move",
1386
+ "choice": "earthquake"
1387
+ },
1388
+ "opponent_action": null,
1389
+ "player_active_after": {
1390
+ "name": "tangrowth",
1391
+ "hp": 0.0,
1392
+ "status": "FNT (status) object"
1393
+ },
1394
+ "opponent_active_after": {
1395
+ "name": "wailord",
1396
+ "hp": 0.0,
1397
+ "status": "FNT (status) object"
1398
+ },
1399
+ "reward": -1.22,
1400
+ "cumulative_reward": 16.1321,
1401
+ "commentary": [],
1402
+ "showdown_events": [],
1403
+ "valid_actions": [
1404
+ {
1405
+ "action": "move",
1406
+ "choice": "earthquake"
1407
+ },
1408
+ {
1409
+ "action": "move",
1410
+ "choice": "swordsdance"
1411
+ },
1412
+ {
1413
+ "action": "move",
1414
+ "choice": "rockslide"
1415
+ },
1416
+ {
1417
+ "action": "move",
1418
+ "choice": "powerwhip"
1419
+ },
1420
+ {
1421
+ "action": "switch",
1422
+ "choice": "ambipom"
1423
+ },
1424
+ {
1425
+ "action": "switch",
1426
+ "choice": "corsola"
1427
+ },
1428
+ {
1429
+ "action": "switch",
1430
+ "choice": "xatu"
1431
+ },
1432
+ {
1433
+ "action": "switch",
1434
+ "choice": "aggron"
1435
+ },
1436
+ {
1437
+ "action": "switch",
1438
+ "choice": "gorebyss"
1439
+ }
1440
+ ]
1441
+ },
1442
+ {
1443
+ "turn": 17,
1444
+ "battle_turn": 16,
1445
+ "player_active_before": {
1446
+ "name": "tangrowth",
1447
+ "hp": 0.0,
1448
+ "status": "FNT (status) object"
1449
+ },
1450
+ "opponent_active_before": {
1451
+ "name": "wailord",
1452
+ "hp": 0.0,
1453
+ "status": "FNT (status) object"
1454
+ },
1455
+ "player_action": {
1456
+ "action": "switch",
1457
+ "choice": "corsola"
1458
+ },
1459
+ "opponent_action": {
1460
+ "type": "move",
1461
+ "choice": "Self-Destruct",
1462
+ "source": "p2a: Wailord",
1463
+ "confidence": "logged"
1464
+ },
1465
+ "player_active_after": {
1466
+ "name": "corsola",
1467
+ "hp": 87.2,
1468
+ "status": "Healthy"
1469
+ },
1470
+ "opponent_active_after": {
1471
+ "name": "salamence",
1472
+ "hp": 100.0,
1473
+ "status": "Healthy"
1474
+ },
1475
+ "reward": -0.05,
1476
+ "cumulative_reward": 16.0821,
1477
+ "commentary": [
1478
+ "p2a: Wailord used Self-Destruct on p1a: Tangrowth.",
1479
+ "p1a: Tangrowth dropped to 0 fnt HP.",
1480
+ "p2a: Wailord fainted.",
1481
+ "p1a: Tangrowth fainted.",
1482
+ "p2a: Salamence switched to Salamence, L74, M (100/100).",
1483
+ "p1a: Corsola switched to Corsola, L98, M (232/266)."
1484
+ ],
1485
+ "showdown_events": [
1486
+ "move | p2a: Wailord | Self-Destruct | p1a: Tangrowth",
1487
+ "-crit | p1a: Tangrowth",
1488
+ "-damage | p1a: Tangrowth | 0 fnt",
1489
+ "faint | p2a: Wailord",
1490
+ "faint | p1a: Tangrowth",
1491
+ "upkeep",
1492
+ "switch | p2a: Salamence | Salamence, L74, M | 100/100",
1493
+ "switch | p1a: Corsola | Corsola, L98, M | 232/266",
1494
+ "-ability | p2a: Salamence | Intimidate | boost",
1495
+ "-unboost | p1a: Corsola | atk | 1",
1496
+ "turn | 17"
1497
+ ],
1498
+ "valid_actions": [
1499
+ {
1500
+ "action": "switch",
1501
+ "choice": "ambipom"
1502
+ },
1503
+ {
1504
+ "action": "switch",
1505
+ "choice": "corsola"
1506
+ },
1507
+ {
1508
+ "action": "switch",
1509
+ "choice": "xatu"
1510
+ },
1511
+ {
1512
+ "action": "switch",
1513
+ "choice": "aggron"
1514
+ },
1515
+ {
1516
+ "action": "switch",
1517
+ "choice": "gorebyss"
1518
+ }
1519
+ ]
1520
+ },
1521
+ {
1522
+ "turn": 18,
1523
+ "battle_turn": 17,
1524
+ "player_active_before": {
1525
+ "name": "corsola",
1526
+ "hp": 87.2,
1527
+ "status": "Healthy"
1528
+ },
1529
+ "opponent_active_before": {
1530
+ "name": "salamence",
1531
+ "hp": 100.0,
1532
+ "status": "Healthy"
1533
+ },
1534
+ "player_action": {
1535
+ "action": "move",
1536
+ "choice": "recover"
1537
+ },
1538
+ "opponent_action": {
1539
+ "type": "switch",
1540
+ "choice": "Gorebyss",
1541
+ "source": "p2a: Gorebyss",
1542
+ "confidence": "logged"
1543
+ },
1544
+ "player_active_after": {
1545
+ "name": "corsola",
1546
+ "hp": 100.0,
1547
+ "status": "Healthy"
1548
+ },
1549
+ "opponent_active_after": {
1550
+ "name": "gorebyss",
1551
+ "hp": 26.0,
1552
+ "status": "Healthy"
1553
+ },
1554
+ "reward": 0.1625,
1555
+ "cumulative_reward": 16.2446,
1556
+ "commentary": [
1557
+ "p2a: Gorebyss switched to Gorebyss, L88, M (26/100).",
1558
+ "p1a: Corsola used Recover on p1a: Corsola.",
1559
+ "p1a: Corsola recovered to 266/266 HP."
1560
+ ],
1561
+ "showdown_events": [
1562
+ "switch | p2a: Gorebyss | Gorebyss, L88, M | 26/100",
1563
+ "move | p1a: Corsola | Recover | p1a: Corsola",
1564
+ "-heal | p1a: Corsola | 266/266",
1565
+ "upkeep",
1566
+ "turn | 18"
1567
+ ],
1568
+ "valid_actions": [
1569
+ {
1570
+ "action": "move",
1571
+ "choice": "explosion"
1572
+ },
1573
+ {
1574
+ "action": "move",
1575
+ "choice": "recover"
1576
+ },
1577
+ {
1578
+ "action": "move",
1579
+ "choice": "surf"
1580
+ },
1581
+ {
1582
+ "action": "move",
1583
+ "choice": "stealthrock"
1584
+ },
1585
+ {
1586
+ "action": "switch",
1587
+ "choice": "ambipom"
1588
+ },
1589
+ {
1590
+ "action": "switch",
1591
+ "choice": "xatu"
1592
+ },
1593
+ {
1594
+ "action": "switch",
1595
+ "choice": "aggron"
1596
+ },
1597
+ {
1598
+ "action": "switch",
1599
+ "choice": "gorebyss"
1600
+ }
1601
+ ]
1602
+ },
1603
+ {
1604
+ "turn": 19,
1605
+ "battle_turn": 18,
1606
+ "player_active_before": {
1607
+ "name": "corsola",
1608
+ "hp": 100.0,
1609
+ "status": "Healthy"
1610
+ },
1611
+ "opponent_active_before": {
1612
+ "name": "gorebyss",
1613
+ "hp": 26.0,
1614
+ "status": "Healthy"
1615
+ },
1616
+ "player_action": {
1617
+ "action": "switch",
1618
+ "choice": "gorebyss"
1619
+ },
1620
+ "opponent_action": null,
1621
+ "player_active_after": {
1622
+ "name": "gorebyss",
1623
+ "hp": 0.0,
1624
+ "status": "FNT (status) object"
1625
+ },
1626
+ "opponent_active_after": {
1627
+ "name": "gorebyss",
1628
+ "hp": 16.0,
1629
+ "status": "Healthy"
1630
+ },
1631
+ "reward": -3.2333,
1632
+ "cumulative_reward": 13.0113,
1633
+ "commentary": [],
1634
+ "showdown_events": [],
1635
+ "valid_actions": [
1636
+ {
1637
+ "action": "move",
1638
+ "choice": "explosion"
1639
+ },
1640
+ {
1641
+ "action": "move",
1642
+ "choice": "recover"
1643
+ },
1644
+ {
1645
+ "action": "move",
1646
+ "choice": "surf"
1647
+ },
1648
+ {
1649
+ "action": "move",
1650
+ "choice": "stealthrock"
1651
+ },
1652
+ {
1653
+ "action": "switch",
1654
+ "choice": "ambipom"
1655
+ },
1656
+ {
1657
+ "action": "switch",
1658
+ "choice": "xatu"
1659
+ },
1660
+ {
1661
+ "action": "switch",
1662
+ "choice": "aggron"
1663
+ },
1664
+ {
1665
+ "action": "switch",
1666
+ "choice": "gorebyss"
1667
+ }
1668
+ ]
1669
+ },
1670
+ {
1671
+ "turn": 20,
1672
+ "battle_turn": 18,
1673
+ "player_active_before": {
1674
+ "name": "gorebyss",
1675
+ "hp": 0.0,
1676
+ "status": "FNT (status) object"
1677
+ },
1678
+ "opponent_active_before": {
1679
+ "name": "gorebyss",
1680
+ "hp": 16.0,
1681
+ "status": "Healthy"
1682
+ },
1683
+ "player_action": {
1684
+ "action": "switch",
1685
+ "choice": "corsola"
1686
+ },
1687
+ "opponent_action": {
1688
+ "type": "move",
1689
+ "choice": "Hidden Power",
1690
+ "source": "p2a: Gorebyss",
1691
+ "confidence": "logged"
1692
+ },
1693
+ "player_active_after": {
1694
+ "name": "corsola",
1695
+ "hp": 100.0,
1696
+ "status": "Healthy"
1697
+ },
1698
+ "opponent_active_after": {
1699
+ "name": "gorebyss",
1700
+ "hp": 16.0,
1701
+ "status": "Healthy"
1702
+ },
1703
+ "reward": -0.05,
1704
+ "cumulative_reward": 12.9613,
1705
+ "commentary": [
1706
+ "p1a: Gorebyss switched to Gorebyss, L88, M (56/240).",
1707
+ "p2a: Gorebyss used Hidden Power on p1a: Gorebyss.",
1708
+ "It was super effective.",
1709
+ "p1a: Gorebyss dropped to 0 fnt HP.",
1710
+ "p1a: Gorebyss fainted.",
1711
+ "p2a: Gorebyss dropped to 16/100 | [from] item: Life Orb HP.",
1712
+ "p1a: Corsola switched to Corsola, L98, M (266/266)."
1713
+ ],
1714
+ "showdown_events": [
1715
+ "switch | p1a: Gorebyss | Gorebyss, L88, M | 56/240",
1716
+ "move | p2a: Gorebyss | Hidden Power | p1a: Gorebyss",
1717
+ "-supereffective | p1a: Gorebyss",
1718
+ "-damage | p1a: Gorebyss | 0 fnt",
1719
+ "faint | p1a: Gorebyss",
1720
+ "-damage | p2a: Gorebyss | 16/100 | [from] item: Life Orb",
1721
+ "upkeep",
1722
+ "switch | p1a: Corsola | Corsola, L98, M | 266/266",
1723
+ "turn | 19"
1724
+ ],
1725
+ "valid_actions": [
1726
+ {
1727
+ "action": "switch",
1728
+ "choice": "ambipom"
1729
+ },
1730
+ {
1731
+ "action": "switch",
1732
+ "choice": "xatu"
1733
+ },
1734
+ {
1735
+ "action": "switch",
1736
+ "choice": "aggron"
1737
+ },
1738
+ {
1739
+ "action": "switch",
1740
+ "choice": "corsola"
1741
+ }
1742
+ ]
1743
+ },
1744
+ {
1745
+ "turn": 21,
1746
+ "battle_turn": 19,
1747
+ "player_active_before": {
1748
+ "name": "corsola",
1749
+ "hp": 100.0,
1750
+ "status": "Healthy"
1751
+ },
1752
+ "opponent_active_before": {
1753
+ "name": "gorebyss",
1754
+ "hp": 16.0,
1755
+ "status": "Healthy"
1756
+ },
1757
+ "player_action": {
1758
+ "action": "move",
1759
+ "choice": "recover"
1760
+ },
1761
+ "opponent_action": {
1762
+ "type": "switch",
1763
+ "choice": "Salamence",
1764
+ "source": "p2a: Salamence",
1765
+ "confidence": "logged"
1766
+ },
1767
+ "player_active_after": {
1768
+ "name": "corsola",
1769
+ "hp": 100.0,
1770
+ "status": "Healthy"
1771
+ },
1772
+ "opponent_active_after": {
1773
+ "name": "salamence",
1774
+ "hp": 100.0,
1775
+ "status": "Healthy"
1776
+ },
1777
+ "reward": -0.05,
1778
+ "cumulative_reward": 12.9113,
1779
+ "commentary": [
1780
+ "p2a: Salamence switched to Salamence, L74, M (100/100).",
1781
+ "p1a: Corsola used Recover on [still]."
1782
+ ],
1783
+ "showdown_events": [
1784
+ "switch | p2a: Salamence | Salamence, L74, M | 100/100",
1785
+ "-ability | p2a: Salamence | Intimidate | boost",
1786
+ "-unboost | p1a: Corsola | atk | 1",
1787
+ "move | p1a: Corsola | Recover | [still]",
1788
+ "-fail | p1a: Corsola | heal",
1789
+ "upkeep",
1790
+ "turn | 20"
1791
+ ],
1792
+ "valid_actions": [
1793
+ {
1794
+ "action": "move",
1795
+ "choice": "explosion"
1796
+ },
1797
+ {
1798
+ "action": "move",
1799
+ "choice": "recover"
1800
+ },
1801
+ {
1802
+ "action": "move",
1803
+ "choice": "surf"
1804
+ },
1805
+ {
1806
+ "action": "move",
1807
+ "choice": "stealthrock"
1808
+ },
1809
+ {
1810
+ "action": "switch",
1811
+ "choice": "ambipom"
1812
+ },
1813
+ {
1814
+ "action": "switch",
1815
+ "choice": "xatu"
1816
+ },
1817
+ {
1818
+ "action": "switch",
1819
+ "choice": "aggron"
1820
+ }
1821
+ ]
1822
+ },
1823
+ {
1824
+ "turn": 22,
1825
+ "battle_turn": 20,
1826
+ "player_active_before": {
1827
+ "name": "corsola",
1828
+ "hp": 100.0,
1829
+ "status": "Healthy"
1830
+ },
1831
+ "opponent_active_before": {
1832
+ "name": "salamence",
1833
+ "hp": 100.0,
1834
+ "status": "Healthy"
1835
+ },
1836
+ "player_action": {
1837
+ "action": "switch",
1838
+ "choice": "xatu"
1839
+ },
1840
+ "opponent_action": {
1841
+ "type": "move",
1842
+ "choice": "Dragon Dance",
1843
+ "source": "p2a: Salamence",
1844
+ "confidence": "logged"
1845
+ },
1846
+ "player_active_after": {
1847
+ "name": "xatu",
1848
+ "hp": 100.0,
1849
+ "status": "Healthy"
1850
+ },
1851
+ "opponent_active_after": {
1852
+ "name": "salamence",
1853
+ "hp": 100.0,
1854
+ "status": "Healthy"
1855
+ },
1856
+ "reward": -1.05,
1857
+ "cumulative_reward": 11.8613,
1858
+ "commentary": [
1859
+ "p1a: Xatu switched to Xatu, L89, M (259/259).",
1860
+ "p2a: Salamence used Dragon Dance on p2a: Salamence."
1861
+ ],
1862
+ "showdown_events": [
1863
+ "switch | p1a: Xatu | Xatu, L89, M | 259/259",
1864
+ "move | p2a: Salamence | Dragon Dance | p2a: Salamence",
1865
+ "-boost | p2a: Salamence | atk | 1",
1866
+ "-boost | p2a: Salamence | spe | 1",
1867
+ "upkeep",
1868
+ "turn | 21"
1869
+ ],
1870
+ "valid_actions": [
1871
+ {
1872
+ "action": "move",
1873
+ "choice": "explosion"
1874
+ },
1875
+ {
1876
+ "action": "move",
1877
+ "choice": "recover"
1878
+ },
1879
+ {
1880
+ "action": "move",
1881
+ "choice": "surf"
1882
+ },
1883
+ {
1884
+ "action": "move",
1885
+ "choice": "stealthrock"
1886
+ },
1887
+ {
1888
+ "action": "switch",
1889
+ "choice": "ambipom"
1890
+ },
1891
+ {
1892
+ "action": "switch",
1893
+ "choice": "xatu"
1894
+ },
1895
+ {
1896
+ "action": "switch",
1897
+ "choice": "aggron"
1898
+ }
1899
+ ]
1900
+ },
1901
+ {
1902
+ "turn": 23,
1903
+ "battle_turn": 21,
1904
+ "player_active_before": {
1905
+ "name": "xatu",
1906
+ "hp": 100.0,
1907
+ "status": "Healthy"
1908
+ },
1909
+ "opponent_active_before": {
1910
+ "name": "salamence",
1911
+ "hp": 100.0,
1912
+ "status": "Healthy"
1913
+ },
1914
+ "player_action": {
1915
+ "action": "move",
1916
+ "choice": "trick"
1917
+ },
1918
+ "opponent_action": {
1919
+ "type": "move",
1920
+ "choice": "Dragon Dance",
1921
+ "source": "p2a: Salamence",
1922
+ "confidence": "logged"
1923
+ },
1924
+ "player_active_after": {
1925
+ "name": "xatu",
1926
+ "hp": 100.0,
1927
+ "status": "Healthy"
1928
+ },
1929
+ "opponent_active_after": {
1930
+ "name": "salamence",
1931
+ "hp": 100.0,
1932
+ "status": "Healthy"
1933
+ },
1934
+ "reward": -1.05,
1935
+ "cumulative_reward": 10.8112,
1936
+ "commentary": [
1937
+ "p1a: Xatu used Trick on p2a: Salamence.",
1938
+ "p2a: Salamence used Dragon Dance on p2a: Salamence."
1939
+ ],
1940
+ "showdown_events": [
1941
+ "move | p1a: Xatu | Trick | p2a: Salamence",
1942
+ "-activate | p1a: Xatu | move: Trick | [of] p2a: Salamence",
1943
+ "-item | p2a: Salamence | Choice Scarf | [from] move: Trick",
1944
+ "-item | p1a: Xatu | Lum Berry | [from] move: Trick",
1945
+ "move | p2a: Salamence | Dragon Dance | p2a: Salamence",
1946
+ "-boost | p2a: Salamence | atk | 1",
1947
+ "-boost | p2a: Salamence | spe | 1",
1948
+ "upkeep",
1949
+ "turn | 22"
1950
+ ],
1951
+ "valid_actions": [
1952
+ {
1953
+ "action": "move",
1954
+ "choice": "grassknot"
1955
+ },
1956
+ {
1957
+ "action": "move",
1958
+ "choice": "psychic"
1959
+ },
1960
+ {
1961
+ "action": "move",
1962
+ "choice": "trick"
1963
+ },
1964
+ {
1965
+ "action": "move",
1966
+ "choice": "heatwave"
1967
+ },
1968
+ {
1969
+ "action": "switch",
1970
+ "choice": "ambipom"
1971
+ },
1972
+ {
1973
+ "action": "switch",
1974
+ "choice": "corsola"
1975
+ },
1976
+ {
1977
+ "action": "switch",
1978
+ "choice": "aggron"
1979
+ }
1980
+ ]
1981
+ },
1982
+ {
1983
+ "turn": 24,
1984
+ "battle_turn": 22,
1985
+ "player_active_before": {
1986
+ "name": "xatu",
1987
+ "hp": 100.0,
1988
+ "status": "Healthy"
1989
+ },
1990
+ "opponent_active_before": {
1991
+ "name": "salamence",
1992
+ "hp": 100.0,
1993
+ "status": "Healthy"
1994
+ },
1995
+ "player_action": {
1996
+ "action": "move",
1997
+ "choice": "psychic"
1998
+ },
1999
+ "opponent_action": {
2000
+ "type": "switch",
2001
+ "choice": "Gorebyss",
2002
+ "source": "p2a: Gorebyss",
2003
+ "confidence": "logged"
2004
+ },
2005
+ "player_active_after": {
2006
+ "name": "xatu",
2007
+ "hp": 100.0,
2008
+ "status": "Healthy"
2009
+ },
2010
+ "opponent_active_after": {
2011
+ "name": "salamence",
2012
+ "hp": 100.0,
2013
+ "status": "Healthy"
2014
+ },
2015
+ "reward": 3.2167,
2016
+ "cumulative_reward": 14.0279,
2017
+ "commentary": [
2018
+ "p2a: Gorebyss switched to Gorebyss, L88, M (16/100).",
2019
+ "p1a: Xatu used Psychic on p2a: Gorebyss.",
2020
+ "p2a: Gorebyss dropped to 0 fnt HP.",
2021
+ "p2a: Gorebyss fainted.",
2022
+ "p2a: Salamence switched to Salamence, L74, M (100/100)."
2023
+ ],
2024
+ "showdown_events": [
2025
+ "switch | p2a: Gorebyss | Gorebyss, L88, M | 16/100",
2026
+ "move | p1a: Xatu | Psychic | p2a: Gorebyss",
2027
+ "-damage | p2a: Gorebyss | 0 fnt",
2028
+ "faint | p2a: Gorebyss",
2029
+ "upkeep",
2030
+ "switch | p2a: Salamence | Salamence, L74, M | 100/100",
2031
+ "-ability | p2a: Salamence | Intimidate | boost",
2032
+ "-unboost | p1a: Xatu | atk | 1",
2033
+ "turn | 23"
2034
+ ],
2035
+ "valid_actions": [
2036
+ {
2037
+ "action": "move",
2038
+ "choice": "grassknot"
2039
+ },
2040
+ {
2041
+ "action": "move",
2042
+ "choice": "psychic"
2043
+ },
2044
+ {
2045
+ "action": "move",
2046
+ "choice": "trick"
2047
+ },
2048
+ {
2049
+ "action": "move",
2050
+ "choice": "heatwave"
2051
+ },
2052
+ {
2053
+ "action": "switch",
2054
+ "choice": "ambipom"
2055
+ },
2056
+ {
2057
+ "action": "switch",
2058
+ "choice": "corsola"
2059
+ },
2060
+ {
2061
+ "action": "switch",
2062
+ "choice": "aggron"
2063
+ }
2064
+ ]
2065
+ },
2066
+ {
2067
+ "turn": 25,
2068
+ "battle_turn": 23,
2069
+ "player_active_before": {
2070
+ "name": "xatu",
2071
+ "hp": 100.0,
2072
+ "status": "Healthy"
2073
+ },
2074
+ "opponent_active_before": {
2075
+ "name": "salamence",
2076
+ "hp": 100.0,
2077
+ "status": "Healthy"
2078
+ },
2079
+ "player_action": {
2080
+ "action": "move",
2081
+ "choice": "grassknot"
2082
+ },
2083
+ "opponent_action": {
2084
+ "type": "move",
2085
+ "choice": "Outrage",
2086
+ "source": "p2a: Salamence",
2087
+ "confidence": "logged"
2088
+ },
2089
+ "player_active_after": {
2090
+ "name": "xatu",
2091
+ "hp": 45.6,
2092
+ "status": "Healthy"
2093
+ },
2094
+ "opponent_active_after": {
2095
+ "name": "salamence",
2096
+ "hp": 92.0,
2097
+ "status": "Healthy"
2098
+ },
2099
+ "reward": -0.7979,
2100
+ "cumulative_reward": 13.23,
2101
+ "commentary": [
2102
+ "p2a: Salamence used Outrage on p1a: Xatu.",
2103
+ "p1a: Xatu dropped to 118/259 HP.",
2104
+ "p1a: Xatu used Grass Knot on p2a: Salamence.",
2105
+ "It was resisted.",
2106
+ "p2a: Salamence dropped to 92/100 HP."
2107
+ ],
2108
+ "showdown_events": [
2109
+ "move | p2a: Salamence | Outrage | p1a: Xatu",
2110
+ "-damage | p1a: Xatu | 118/259",
2111
+ "move | p1a: Xatu | Grass Knot | p2a: Salamence",
2112
+ "-resisted | p2a: Salamence",
2113
+ "-damage | p2a: Salamence | 92/100",
2114
+ "upkeep",
2115
+ "turn | 24"
2116
+ ],
2117
+ "valid_actions": [
2118
+ {
2119
+ "action": "move",
2120
+ "choice": "grassknot"
2121
+ },
2122
+ {
2123
+ "action": "move",
2124
+ "choice": "psychic"
2125
+ },
2126
+ {
2127
+ "action": "move",
2128
+ "choice": "trick"
2129
+ },
2130
+ {
2131
+ "action": "move",
2132
+ "choice": "heatwave"
2133
+ },
2134
+ {
2135
+ "action": "switch",
2136
+ "choice": "ambipom"
2137
+ },
2138
+ {
2139
+ "action": "switch",
2140
+ "choice": "corsola"
2141
+ },
2142
+ {
2143
+ "action": "switch",
2144
+ "choice": "aggron"
2145
+ }
2146
+ ]
2147
+ },
2148
+ {
2149
+ "turn": 26,
2150
+ "battle_turn": 24,
2151
+ "player_active_before": {
2152
+ "name": "xatu",
2153
+ "hp": 45.6,
2154
+ "status": "Healthy"
2155
+ },
2156
+ "opponent_active_before": {
2157
+ "name": "salamence",
2158
+ "hp": 92.0,
2159
+ "status": "Healthy"
2160
+ },
2161
+ "player_action": {
2162
+ "action": "move",
2163
+ "choice": "grassknot"
2164
+ },
2165
+ "opponent_action": null,
2166
+ "player_active_after": {
2167
+ "name": "xatu",
2168
+ "hp": 0.0,
2169
+ "status": "FNT (status) object"
2170
+ },
2171
+ "opponent_active_after": {
2172
+ "name": "salamence",
2173
+ "hp": 92.0,
2174
+ "status": "Healthy"
2175
+ },
2176
+ "reward": -3.7875,
2177
+ "cumulative_reward": 9.4425,
2178
+ "commentary": [],
2179
+ "showdown_events": [],
2180
+ "valid_actions": [
2181
+ {
2182
+ "action": "move",
2183
+ "choice": "grassknot"
2184
+ },
2185
+ {
2186
+ "action": "move",
2187
+ "choice": "psychic"
2188
+ },
2189
+ {
2190
+ "action": "move",
2191
+ "choice": "trick"
2192
+ },
2193
+ {
2194
+ "action": "move",
2195
+ "choice": "heatwave"
2196
+ },
2197
+ {
2198
+ "action": "switch",
2199
+ "choice": "ambipom"
2200
+ },
2201
+ {
2202
+ "action": "switch",
2203
+ "choice": "corsola"
2204
+ },
2205
+ {
2206
+ "action": "switch",
2207
+ "choice": "aggron"
2208
+ }
2209
+ ]
2210
+ },
2211
+ {
2212
+ "turn": 27,
2213
+ "battle_turn": 24,
2214
+ "player_active_before": {
2215
+ "name": "xatu",
2216
+ "hp": 0.0,
2217
+ "status": "FNT (status) object"
2218
+ },
2219
+ "opponent_active_before": {
2220
+ "name": "salamence",
2221
+ "hp": 92.0,
2222
+ "status": "Healthy"
2223
+ },
2224
+ "player_action": {
2225
+ "action": "switch",
2226
+ "choice": "corsola"
2227
+ },
2228
+ "opponent_action": {
2229
+ "type": "move",
2230
+ "choice": "Outrage",
2231
+ "source": "p2a: Salamence",
2232
+ "confidence": "logged"
2233
+ },
2234
+ "player_active_after": {
2235
+ "name": "corsola",
2236
+ "hp": 100.0,
2237
+ "status": "Healthy"
2238
+ },
2239
+ "opponent_active_after": {
2240
+ "name": "salamence",
2241
+ "hp": 92.0,
2242
+ "status": "Healthy"
2243
+ },
2244
+ "reward": -0.05,
2245
+ "cumulative_reward": 9.3925,
2246
+ "commentary": [
2247
+ "p2a: Salamence used Outrage on p1a: Xatu | [from] lockedmove.",
2248
+ "p1a: Xatu dropped to 0 fnt HP.",
2249
+ "p1a: Xatu fainted.",
2250
+ "p1a: Corsola switched to Corsola, L98, M (266/266)."
2251
+ ],
2252
+ "showdown_events": [
2253
+ "move | p2a: Salamence | Outrage | p1a: Xatu | [from] lockedmove",
2254
+ "-damage | p1a: Xatu | 0 fnt",
2255
+ "faint | p1a: Xatu",
2256
+ "upkeep",
2257
+ "switch | p1a: Corsola | Corsola, L98, M | 266/266",
2258
+ "turn | 25"
2259
+ ],
2260
+ "valid_actions": [
2261
+ {
2262
+ "action": "switch",
2263
+ "choice": "ambipom"
2264
+ },
2265
+ {
2266
+ "action": "switch",
2267
+ "choice": "corsola"
2268
+ },
2269
+ {
2270
+ "action": "switch",
2271
+ "choice": "aggron"
2272
+ }
2273
+ ]
2274
+ },
2275
+ {
2276
+ "turn": 28,
2277
+ "battle_turn": 25,
2278
+ "player_active_before": {
2279
+ "name": "corsola",
2280
+ "hp": 100.0,
2281
+ "status": "Healthy"
2282
+ },
2283
+ "opponent_active_before": {
2284
+ "name": "salamence",
2285
+ "hp": 92.0,
2286
+ "status": "Healthy"
2287
+ },
2288
+ "player_action": {
2289
+ "action": "move",
2290
+ "choice": "stealthrock"
2291
+ },
2292
+ "opponent_action": {
2293
+ "type": "move",
2294
+ "choice": "Outrage",
2295
+ "source": "p2a: Salamence",
2296
+ "confidence": "logged"
2297
+ },
2298
+ "player_active_after": {
2299
+ "name": "corsola",
2300
+ "hp": 60.5,
2301
+ "status": "Healthy"
2302
+ },
2303
+ "opponent_active_after": {
2304
+ "name": "salamence",
2305
+ "hp": 92.0,
2306
+ "status": "Healthy"
2307
+ },
2308
+ "reward": -0.7063,
2309
+ "cumulative_reward": 8.6862,
2310
+ "commentary": [
2311
+ "p2a: Salamence used Outrage on p1a: Corsola | [from] lockedmove.",
2312
+ "p1a: Corsola dropped to 145/266 HP.",
2313
+ "p1a: Corsola used Stealth Rock on p2a: Salamence.",
2314
+ "p1a: Corsola recovered to 161/266 | [from] item: Leftovers HP."
2315
+ ],
2316
+ "showdown_events": [
2317
+ "move | p2a: Salamence | Outrage | p1a: Corsola | [from] lockedmove",
2318
+ "-damage | p1a: Corsola | 145/266",
2319
+ "move | p1a: Corsola | Stealth Rock | p2a: Salamence",
2320
+ "-sidestart | p2: RandomPlayer 1 | move: Stealth Rock",
2321
+ "-heal | p1a: Corsola | 161/266 | [from] item: Leftovers",
2322
+ "-start | p2a: Salamence | confusion | [fatigue]",
2323
+ "upkeep",
2324
+ "turn | 26"
2325
+ ],
2326
+ "valid_actions": [
2327
+ {
2328
+ "action": "move",
2329
+ "choice": "explosion"
2330
+ },
2331
+ {
2332
+ "action": "move",
2333
+ "choice": "recover"
2334
+ },
2335
+ {
2336
+ "action": "move",
2337
+ "choice": "surf"
2338
+ },
2339
+ {
2340
+ "action": "move",
2341
+ "choice": "stealthrock"
2342
+ },
2343
+ {
2344
+ "action": "switch",
2345
+ "choice": "ambipom"
2346
+ },
2347
+ {
2348
+ "action": "switch",
2349
+ "choice": "aggron"
2350
+ }
2351
+ ]
2352
+ },
2353
+ {
2354
+ "turn": 29,
2355
+ "battle_turn": 26,
2356
+ "player_active_before": {
2357
+ "name": "corsola",
2358
+ "hp": 60.5,
2359
+ "status": "Healthy"
2360
+ },
2361
+ "opponent_active_before": {
2362
+ "name": "salamence",
2363
+ "hp": 92.0,
2364
+ "status": "Healthy"
2365
+ },
2366
+ "player_action": {
2367
+ "action": "move",
2368
+ "choice": "recover"
2369
+ },
2370
+ "opponent_action": null,
2371
+ "player_active_after": {
2372
+ "name": "corsola",
2373
+ "hp": 100.0,
2374
+ "status": "Healthy"
2375
+ },
2376
+ "opponent_active_after": {
2377
+ "name": "salamence",
2378
+ "hp": 77.0,
2379
+ "status": "Healthy"
2380
+ },
2381
+ "reward": 0.8562,
2382
+ "cumulative_reward": 9.5425,
2383
+ "commentary": [
2384
+ "p2a: Salamence dropped to 77/100 | [from] confusion HP.",
2385
+ "p1a: Corsola used Recover on p1a: Corsola.",
2386
+ "p1a: Corsola recovered to 266/266 HP."
2387
+ ],
2388
+ "showdown_events": [
2389
+ "-activate | p2a: Salamence | confusion",
2390
+ "-damage | p2a: Salamence | 77/100 | [from] confusion",
2391
+ "move | p1a: Corsola | Recover | p1a: Corsola",
2392
+ "-heal | p1a: Corsola | 266/266",
2393
+ "upkeep",
2394
+ "turn | 27"
2395
+ ],
2396
+ "valid_actions": [
2397
+ {
2398
+ "action": "move",
2399
+ "choice": "explosion"
2400
+ },
2401
+ {
2402
+ "action": "move",
2403
+ "choice": "recover"
2404
+ },
2405
+ {
2406
+ "action": "move",
2407
+ "choice": "surf"
2408
+ },
2409
+ {
2410
+ "action": "move",
2411
+ "choice": "stealthrock"
2412
+ },
2413
+ {
2414
+ "action": "switch",
2415
+ "choice": "ambipom"
2416
+ },
2417
+ {
2418
+ "action": "switch",
2419
+ "choice": "aggron"
2420
+ }
2421
+ ]
2422
+ },
2423
+ {
2424
+ "turn": 30,
2425
+ "battle_turn": 27,
2426
+ "player_active_before": {
2427
+ "name": "corsola",
2428
+ "hp": 100.0,
2429
+ "status": "Healthy"
2430
+ },
2431
+ "opponent_active_before": {
2432
+ "name": "salamence",
2433
+ "hp": 77.0,
2434
+ "status": "Healthy"
2435
+ },
2436
+ "player_action": {
2437
+ "action": "switch",
2438
+ "choice": "aggron"
2439
+ },
2440
+ "opponent_action": null,
2441
+ "player_active_after": {
2442
+ "name": "aggron",
2443
+ "hp": 100.0,
2444
+ "status": "Healthy"
2445
+ },
2446
+ "opponent_active_after": {
2447
+ "name": "salamence",
2448
+ "hp": 63.0,
2449
+ "status": "Healthy"
2450
+ },
2451
+ "reward": 0.1833,
2452
+ "cumulative_reward": 9.7258,
2453
+ "commentary": [
2454
+ "p1a: Aggron switched to Aggron, L86, F (261/261).",
2455
+ "p2a: Salamence dropped to 63/100 | [from] confusion HP."
2456
+ ],
2457
+ "showdown_events": [
2458
+ "switch | p1a: Aggron | Aggron, L86, F | 261/261",
2459
+ "-activate | p2a: Salamence | confusion",
2460
+ "-damage | p2a: Salamence | 63/100 | [from] confusion",
2461
+ "upkeep",
2462
+ "turn | 28"
2463
+ ],
2464
+ "valid_actions": [
2465
+ {
2466
+ "action": "move",
2467
+ "choice": "explosion"
2468
+ },
2469
+ {
2470
+ "action": "move",
2471
+ "choice": "recover"
2472
+ },
2473
+ {
2474
+ "action": "move",
2475
+ "choice": "surf"
2476
+ },
2477
+ {
2478
+ "action": "move",
2479
+ "choice": "stealthrock"
2480
+ },
2481
+ {
2482
+ "action": "switch",
2483
+ "choice": "ambipom"
2484
+ },
2485
+ {
2486
+ "action": "switch",
2487
+ "choice": "aggron"
2488
+ }
2489
+ ]
2490
+ },
2491
+ {
2492
+ "turn": 31,
2493
+ "battle_turn": 28,
2494
+ "player_active_before": {
2495
+ "name": "aggron",
2496
+ "hp": 100.0,
2497
+ "status": "Healthy"
2498
+ },
2499
+ "opponent_active_before": {
2500
+ "name": "salamence",
2501
+ "hp": 63.0,
2502
+ "status": "Healthy"
2503
+ },
2504
+ "player_action": {
2505
+ "action": "switch",
2506
+ "choice": "corsola"
2507
+ },
2508
+ "opponent_action": {
2509
+ "type": "move",
2510
+ "choice": "Outrage",
2511
+ "source": "p2a: Salamence",
2512
+ "confidence": "logged"
2513
+ },
2514
+ "player_active_after": {
2515
+ "name": "corsola",
2516
+ "hp": 60.5,
2517
+ "status": "Healthy"
2518
+ },
2519
+ "opponent_active_after": {
2520
+ "name": "salamence",
2521
+ "hp": 63.0,
2522
+ "status": "Healthy"
2523
+ },
2524
+ "reward": -0.7063,
2525
+ "cumulative_reward": 9.0196,
2526
+ "commentary": [
2527
+ "p1a: Corsola switched to Corsola, L98, M (266/266).",
2528
+ "p2a: Salamence used Outrage on p1a: Corsola.",
2529
+ "p1a: Corsola dropped to 145/266 HP.",
2530
+ "p1a: Corsola recovered to 161/266 | [from] item: Leftovers HP."
2531
+ ],
2532
+ "showdown_events": [
2533
+ "switch | p1a: Corsola | Corsola, L98, M | 266/266",
2534
+ "-activate | p2a: Salamence | confusion",
2535
+ "move | p2a: Salamence | Outrage | p1a: Corsola",
2536
+ "-damage | p1a: Corsola | 145/266",
2537
+ "-heal | p1a: Corsola | 161/266 | [from] item: Leftovers",
2538
+ "upkeep",
2539
+ "turn | 29"
2540
+ ],
2541
+ "valid_actions": [
2542
+ {
2543
+ "action": "move",
2544
+ "choice": "rockpolish"
2545
+ },
2546
+ {
2547
+ "action": "move",
2548
+ "choice": "headsmash"
2549
+ },
2550
+ {
2551
+ "action": "move",
2552
+ "choice": "earthquake"
2553
+ },
2554
+ {
2555
+ "action": "move",
2556
+ "choice": "icepunch"
2557
+ },
2558
+ {
2559
+ "action": "switch",
2560
+ "choice": "ambipom"
2561
+ },
2562
+ {
2563
+ "action": "switch",
2564
+ "choice": "corsola"
2565
+ }
2566
+ ]
2567
+ },
2568
+ {
2569
+ "turn": 32,
2570
+ "battle_turn": 29,
2571
+ "player_active_before": {
2572
+ "name": "corsola",
2573
+ "hp": 60.5,
2574
+ "status": "Healthy"
2575
+ },
2576
+ "opponent_active_before": {
2577
+ "name": "salamence",
2578
+ "hp": 63.0,
2579
+ "status": "Healthy"
2580
+ },
2581
+ "player_action": {
2582
+ "action": "move",
2583
+ "choice": "stealthrock"
2584
+ },
2585
+ "opponent_action": {
2586
+ "type": "move",
2587
+ "choice": "Outrage",
2588
+ "source": "p2a: Salamence",
2589
+ "confidence": "logged"
2590
+ },
2591
+ "player_active_after": {
2592
+ "name": "corsola",
2593
+ "hp": 22.6,
2594
+ "status": "Healthy"
2595
+ },
2596
+ "opponent_active_after": {
2597
+ "name": "salamence",
2598
+ "hp": 63.0,
2599
+ "status": "Healthy"
2600
+ },
2601
+ "reward": -0.6813,
2602
+ "cumulative_reward": 8.3383,
2603
+ "commentary": [
2604
+ "p2a: Salamence used Outrage on p1a: Corsola | [from] lockedmove.",
2605
+ "p1a: Corsola dropped to 44/266 HP.",
2606
+ "p1a: Corsola used Stealth Rock on [still].",
2607
+ "p1a: Corsola recovered to 60/266 | [from] item: Leftovers HP."
2608
+ ],
2609
+ "showdown_events": [
2610
+ "-activate | p2a: Salamence | confusion",
2611
+ "move | p2a: Salamence | Outrage | p1a: Corsola | [from] lockedmove",
2612
+ "-damage | p1a: Corsola | 44/266",
2613
+ "move | p1a: Corsola | Stealth Rock | [still]",
2614
+ "-fail | p1a: Corsola",
2615
+ "-heal | p1a: Corsola | 60/266 | [from] item: Leftovers",
2616
+ "upkeep",
2617
+ "turn | 30"
2618
+ ],
2619
+ "valid_actions": [
2620
+ {
2621
+ "action": "move",
2622
+ "choice": "explosion"
2623
+ },
2624
+ {
2625
+ "action": "move",
2626
+ "choice": "recover"
2627
+ },
2628
+ {
2629
+ "action": "move",
2630
+ "choice": "surf"
2631
+ },
2632
+ {
2633
+ "action": "move",
2634
+ "choice": "stealthrock"
2635
+ },
2636
+ {
2637
+ "action": "switch",
2638
+ "choice": "ambipom"
2639
+ },
2640
+ {
2641
+ "action": "switch",
2642
+ "choice": "aggron"
2643
+ }
2644
+ ]
2645
+ },
2646
+ {
2647
+ "turn": 33,
2648
+ "battle_turn": 30,
2649
+ "player_active_before": {
2650
+ "name": "corsola",
2651
+ "hp": 22.6,
2652
+ "status": "Healthy"
2653
+ },
2654
+ "opponent_active_before": {
2655
+ "name": "salamence",
2656
+ "hp": 63.0,
2657
+ "status": "Healthy"
2658
+ },
2659
+ "player_action": {
2660
+ "action": "move",
2661
+ "choice": "surf"
2662
+ },
2663
+ "opponent_action": null,
2664
+ "player_active_after": {
2665
+ "name": "corsola",
2666
+ "hp": 0.0,
2667
+ "status": "FNT (status) object"
2668
+ },
2669
+ "opponent_active_after": {
2670
+ "name": "salamence",
2671
+ "hp": 63.0,
2672
+ "status": "Healthy"
2673
+ },
2674
+ "reward": -3.425,
2675
+ "cumulative_reward": 4.9133,
2676
+ "commentary": [],
2677
+ "showdown_events": [],
2678
+ "valid_actions": [
2679
+ {
2680
+ "action": "move",
2681
+ "choice": "explosion"
2682
+ },
2683
+ {
2684
+ "action": "move",
2685
+ "choice": "recover"
2686
+ },
2687
+ {
2688
+ "action": "move",
2689
+ "choice": "surf"
2690
+ },
2691
+ {
2692
+ "action": "move",
2693
+ "choice": "stealthrock"
2694
+ },
2695
+ {
2696
+ "action": "switch",
2697
+ "choice": "ambipom"
2698
+ },
2699
+ {
2700
+ "action": "switch",
2701
+ "choice": "aggron"
2702
+ }
2703
+ ]
2704
+ },
2705
+ {
2706
+ "turn": 34,
2707
+ "battle_turn": 30,
2708
+ "player_active_before": {
2709
+ "name": "corsola",
2710
+ "hp": 0.0,
2711
+ "status": "FNT (status) object"
2712
+ },
2713
+ "opponent_active_before": {
2714
+ "name": "salamence",
2715
+ "hp": 63.0,
2716
+ "status": "Healthy"
2717
+ },
2718
+ "player_action": {
2719
+ "action": "switch",
2720
+ "choice": "aggron"
2721
+ },
2722
+ "opponent_action": {
2723
+ "type": "move",
2724
+ "choice": "Outrage",
2725
+ "source": "p2a: Salamence",
2726
+ "confidence": "logged"
2727
+ },
2728
+ "player_active_after": {
2729
+ "name": "aggron",
2730
+ "hp": 100.0,
2731
+ "status": "Healthy"
2732
+ },
2733
+ "opponent_active_after": {
2734
+ "name": "salamence",
2735
+ "hp": 63.0,
2736
+ "status": "Healthy"
2737
+ },
2738
+ "reward": -0.05,
2739
+ "cumulative_reward": 4.8633,
2740
+ "commentary": [
2741
+ "p2a: Salamence used Outrage on p1a: Corsola.",
2742
+ "p1a: Corsola dropped to 0 fnt HP.",
2743
+ "p1a: Corsola fainted.",
2744
+ "p1a: Aggron switched to Aggron, L86, F (261/261)."
2745
+ ],
2746
+ "showdown_events": [
2747
+ "-end | p2a: Salamence | confusion",
2748
+ "move | p2a: Salamence | Outrage | p1a: Corsola",
2749
+ "-damage | p1a: Corsola | 0 fnt",
2750
+ "faint | p1a: Corsola",
2751
+ "upkeep",
2752
+ "switch | p1a: Aggron | Aggron, L86, F | 261/261",
2753
+ "turn | 31"
2754
+ ],
2755
+ "valid_actions": [
2756
+ {
2757
+ "action": "switch",
2758
+ "choice": "ambipom"
2759
+ },
2760
+ {
2761
+ "action": "switch",
2762
+ "choice": "aggron"
2763
+ }
2764
+ ]
2765
+ },
2766
+ {
2767
+ "turn": 35,
2768
+ "battle_turn": 31,
2769
+ "player_active_before": {
2770
+ "name": "aggron",
2771
+ "hp": 100.0,
2772
+ "status": "Healthy"
2773
+ },
2774
+ "opponent_active_before": {
2775
+ "name": "salamence",
2776
+ "hp": 63.0,
2777
+ "status": "Healthy"
2778
+ },
2779
+ "player_action": {
2780
+ "action": "move",
2781
+ "choice": "rockpolish"
2782
+ },
2783
+ "opponent_action": {
2784
+ "type": "move",
2785
+ "choice": "Outrage",
2786
+ "source": "p2a: Salamence",
2787
+ "confidence": "logged"
2788
+ },
2789
+ "player_active_after": {
2790
+ "name": "aggron",
2791
+ "hp": 85.4,
2792
+ "status": "Healthy"
2793
+ },
2794
+ "opponent_active_after": {
2795
+ "name": "salamence",
2796
+ "hp": 63.0,
2797
+ "status": "Healthy"
2798
+ },
2799
+ "reward": 0.7125,
2800
+ "cumulative_reward": 5.5758,
2801
+ "commentary": [
2802
+ "p2a: Salamence used Outrage on p1a: Aggron | [from] lockedmove.",
2803
+ "It was resisted.",
2804
+ "p1a: Aggron dropped to 223/261 HP.",
2805
+ "p1a: Aggron used Rock Polish on p1a: Aggron."
2806
+ ],
2807
+ "showdown_events": [
2808
+ "move | p2a: Salamence | Outrage | p1a: Aggron | [from] lockedmove",
2809
+ "-resisted | p1a: Aggron",
2810
+ "-damage | p1a: Aggron | 223/261",
2811
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
2812
+ "-boost | p1a: Aggron | spe | 2",
2813
+ "upkeep",
2814
+ "turn | 32"
2815
+ ],
2816
+ "valid_actions": [
2817
+ {
2818
+ "action": "move",
2819
+ "choice": "rockpolish"
2820
+ },
2821
+ {
2822
+ "action": "move",
2823
+ "choice": "headsmash"
2824
+ },
2825
+ {
2826
+ "action": "move",
2827
+ "choice": "earthquake"
2828
+ },
2829
+ {
2830
+ "action": "move",
2831
+ "choice": "icepunch"
2832
+ },
2833
+ {
2834
+ "action": "switch",
2835
+ "choice": "ambipom"
2836
+ }
2837
+ ]
2838
+ },
2839
+ {
2840
+ "turn": 36,
2841
+ "battle_turn": 32,
2842
+ "player_active_before": {
2843
+ "name": "aggron",
2844
+ "hp": 85.4,
2845
+ "status": "Healthy"
2846
+ },
2847
+ "opponent_active_before": {
2848
+ "name": "salamence",
2849
+ "hp": 63.0,
2850
+ "status": "Healthy"
2851
+ },
2852
+ "player_action": {
2853
+ "action": "move",
2854
+ "choice": "rockpolish"
2855
+ },
2856
+ "opponent_action": {
2857
+ "type": "move",
2858
+ "choice": "Outrage",
2859
+ "source": "p2a: Salamence",
2860
+ "confidence": "logged"
2861
+ },
2862
+ "player_active_after": {
2863
+ "name": "aggron",
2864
+ "hp": 71.3,
2865
+ "status": "Healthy"
2866
+ },
2867
+ "opponent_active_after": {
2868
+ "name": "salamence",
2869
+ "hp": 63.0,
2870
+ "status": "Healthy"
2871
+ },
2872
+ "reward": 0.7188,
2873
+ "cumulative_reward": 6.2946,
2874
+ "commentary": [
2875
+ "p2a: Salamence used Outrage on p1a: Aggron | [from] lockedmove.",
2876
+ "It was resisted.",
2877
+ "p1a: Aggron dropped to 186/261 HP.",
2878
+ "p1a: Aggron used Rock Polish on p1a: Aggron."
2879
+ ],
2880
+ "showdown_events": [
2881
+ "move | p2a: Salamence | Outrage | p1a: Aggron | [from] lockedmove",
2882
+ "-resisted | p1a: Aggron",
2883
+ "-damage | p1a: Aggron | 186/261",
2884
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
2885
+ "-boost | p1a: Aggron | spe | 2",
2886
+ "-start | p2a: Salamence | confusion | [fatigue]",
2887
+ "upkeep",
2888
+ "turn | 33"
2889
+ ],
2890
+ "valid_actions": [
2891
+ {
2892
+ "action": "move",
2893
+ "choice": "rockpolish"
2894
+ },
2895
+ {
2896
+ "action": "move",
2897
+ "choice": "headsmash"
2898
+ },
2899
+ {
2900
+ "action": "move",
2901
+ "choice": "earthquake"
2902
+ },
2903
+ {
2904
+ "action": "move",
2905
+ "choice": "icepunch"
2906
+ },
2907
+ {
2908
+ "action": "switch",
2909
+ "choice": "ambipom"
2910
+ }
2911
+ ]
2912
+ },
2913
+ {
2914
+ "turn": 37,
2915
+ "battle_turn": 33,
2916
+ "player_active_before": {
2917
+ "name": "aggron",
2918
+ "hp": 71.3,
2919
+ "status": "Healthy"
2920
+ },
2921
+ "opponent_active_before": {
2922
+ "name": "salamence",
2923
+ "hp": 63.0,
2924
+ "status": "Healthy"
2925
+ },
2926
+ "player_action": {
2927
+ "action": "move",
2928
+ "choice": "rockpolish"
2929
+ },
2930
+ "opponent_action": null,
2931
+ "player_active_after": {
2932
+ "name": "aggron",
2933
+ "hp": 71.3,
2934
+ "status": "Healthy"
2935
+ },
2936
+ "opponent_active_after": {
2937
+ "name": "salamence",
2938
+ "hp": 49.0,
2939
+ "status": "Healthy"
2940
+ },
2941
+ "reward": 0.1833,
2942
+ "cumulative_reward": 6.4779,
2943
+ "commentary": [
2944
+ "p1a: Aggron used Rock Polish on p1a: Aggron.",
2945
+ "p2a: Salamence dropped to 49/100 | [from] confusion HP."
2946
+ ],
2947
+ "showdown_events": [
2948
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
2949
+ "-boost | p1a: Aggron | spe | 2",
2950
+ "-activate | p2a: Salamence | confusion",
2951
+ "-damage | p2a: Salamence | 49/100 | [from] confusion",
2952
+ "upkeep",
2953
+ "turn | 34"
2954
+ ],
2955
+ "valid_actions": [
2956
+ {
2957
+ "action": "move",
2958
+ "choice": "rockpolish"
2959
+ },
2960
+ {
2961
+ "action": "move",
2962
+ "choice": "headsmash"
2963
+ },
2964
+ {
2965
+ "action": "move",
2966
+ "choice": "earthquake"
2967
+ },
2968
+ {
2969
+ "action": "move",
2970
+ "choice": "icepunch"
2971
+ },
2972
+ {
2973
+ "action": "switch",
2974
+ "choice": "ambipom"
2975
+ }
2976
+ ]
2977
+ },
2978
+ {
2979
+ "turn": 38,
2980
+ "battle_turn": 34,
2981
+ "player_active_before": {
2982
+ "name": "aggron",
2983
+ "hp": 71.3,
2984
+ "status": "Healthy"
2985
+ },
2986
+ "opponent_active_before": {
2987
+ "name": "salamence",
2988
+ "hp": 49.0,
2989
+ "status": "Healthy"
2990
+ },
2991
+ "player_action": {
2992
+ "action": "move",
2993
+ "choice": "rockpolish"
2994
+ },
2995
+ "opponent_action": {
2996
+ "type": "move",
2997
+ "choice": "Outrage",
2998
+ "source": "p2a: Salamence",
2999
+ "confidence": "logged"
3000
+ },
3001
+ "player_active_after": {
3002
+ "name": "aggron",
3003
+ "hp": 58.6,
3004
+ "status": "Healthy"
3005
+ },
3006
+ "opponent_active_after": {
3007
+ "name": "salamence",
3008
+ "hp": 49.0,
3009
+ "status": "Healthy"
3010
+ },
3011
+ "reward": -0.2562,
3012
+ "cumulative_reward": 6.2217,
3013
+ "commentary": [
3014
+ "p1a: Aggron used Rock Polish on p1a: Aggron.",
3015
+ "p2a: Salamence used Outrage on p1a: Aggron.",
3016
+ "It was resisted.",
3017
+ "p1a: Aggron dropped to 153/261 HP."
3018
+ ],
3019
+ "showdown_events": [
3020
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
3021
+ "-boost | p1a: Aggron | spe | 0",
3022
+ "-activate | p2a: Salamence | confusion",
3023
+ "move | p2a: Salamence | Outrage | p1a: Aggron",
3024
+ "-resisted | p1a: Aggron",
3025
+ "-damage | p1a: Aggron | 153/261",
3026
+ "upkeep",
3027
+ "turn | 35"
3028
+ ],
3029
+ "valid_actions": [
3030
+ {
3031
+ "action": "move",
3032
+ "choice": "rockpolish"
3033
+ },
3034
+ {
3035
+ "action": "move",
3036
+ "choice": "headsmash"
3037
+ },
3038
+ {
3039
+ "action": "move",
3040
+ "choice": "earthquake"
3041
+ },
3042
+ {
3043
+ "action": "move",
3044
+ "choice": "icepunch"
3045
+ },
3046
+ {
3047
+ "action": "switch",
3048
+ "choice": "ambipom"
3049
+ }
3050
+ ]
3051
+ },
3052
+ {
3053
+ "turn": 39,
3054
+ "battle_turn": 35,
3055
+ "player_active_before": {
3056
+ "name": "aggron",
3057
+ "hp": 58.6,
3058
+ "status": "Healthy"
3059
+ },
3060
+ "opponent_active_before": {
3061
+ "name": "salamence",
3062
+ "hp": 49.0,
3063
+ "status": "Healthy"
3064
+ },
3065
+ "player_action": {
3066
+ "action": "move",
3067
+ "choice": "rockpolish"
3068
+ },
3069
+ "opponent_action": {
3070
+ "type": "move",
3071
+ "choice": "Outrage",
3072
+ "source": "p2a: Salamence",
3073
+ "confidence": "logged"
3074
+ },
3075
+ "player_active_after": {
3076
+ "name": "aggron",
3077
+ "hp": 45.6,
3078
+ "status": "Healthy"
3079
+ },
3080
+ "opponent_active_after": {
3081
+ "name": "salamence",
3082
+ "hp": 49.0,
3083
+ "status": "Healthy"
3084
+ },
3085
+ "reward": -0.2625,
3086
+ "cumulative_reward": 5.9592,
3087
+ "commentary": [
3088
+ "p1a: Aggron used Rock Polish on p1a: Aggron.",
3089
+ "p2a: Salamence used Outrage on p1a: Aggron | [from] lockedmove.",
3090
+ "It was resisted.",
3091
+ "p1a: Aggron dropped to 119/261 HP."
3092
+ ],
3093
+ "showdown_events": [
3094
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
3095
+ "-boost | p1a: Aggron | spe | 0",
3096
+ "-activate | p2a: Salamence | confusion",
3097
+ "move | p2a: Salamence | Outrage | p1a: Aggron | [from] lockedmove",
3098
+ "-resisted | p1a: Aggron",
3099
+ "-damage | p1a: Aggron | 119/261",
3100
+ "upkeep",
3101
+ "turn | 36"
3102
+ ],
3103
+ "valid_actions": [
3104
+ {
3105
+ "action": "move",
3106
+ "choice": "rockpolish"
3107
+ },
3108
+ {
3109
+ "action": "move",
3110
+ "choice": "headsmash"
3111
+ },
3112
+ {
3113
+ "action": "move",
3114
+ "choice": "earthquake"
3115
+ },
3116
+ {
3117
+ "action": "move",
3118
+ "choice": "icepunch"
3119
+ },
3120
+ {
3121
+ "action": "switch",
3122
+ "choice": "ambipom"
3123
+ }
3124
+ ]
3125
+ },
3126
+ {
3127
+ "turn": 40,
3128
+ "battle_turn": 36,
3129
+ "player_active_before": {
3130
+ "name": "aggron",
3131
+ "hp": 45.6,
3132
+ "status": "Healthy"
3133
+ },
3134
+ "opponent_active_before": {
3135
+ "name": "salamence",
3136
+ "hp": 49.0,
3137
+ "status": "Healthy"
3138
+ },
3139
+ "player_action": {
3140
+ "action": "move",
3141
+ "choice": "rockpolish"
3142
+ },
3143
+ "opponent_action": {
3144
+ "type": "move",
3145
+ "choice": "Outrage",
3146
+ "source": "p2a: Salamence",
3147
+ "confidence": "logged"
3148
+ },
3149
+ "player_active_after": {
3150
+ "name": "aggron",
3151
+ "hp": 31.8,
3152
+ "status": "Healthy"
3153
+ },
3154
+ "opponent_active_after": {
3155
+ "name": "salamence",
3156
+ "hp": 49.0,
3157
+ "status": "Healthy"
3158
+ },
3159
+ "reward": -0.275,
3160
+ "cumulative_reward": 5.6842,
3161
+ "commentary": [
3162
+ "p1a: Aggron used Rock Polish on p1a: Aggron.",
3163
+ "p2a: Salamence used Outrage on p1a: Aggron.",
3164
+ "It was resisted.",
3165
+ "p1a: Aggron dropped to 83/261 HP."
3166
+ ],
3167
+ "showdown_events": [
3168
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
3169
+ "-boost | p1a: Aggron | spe | 0",
3170
+ "-end | p2a: Salamence | confusion",
3171
+ "move | p2a: Salamence | Outrage | p1a: Aggron",
3172
+ "-resisted | p1a: Aggron",
3173
+ "-damage | p1a: Aggron | 83/261",
3174
+ "upkeep",
3175
+ "turn | 37"
3176
+ ],
3177
+ "valid_actions": [
3178
+ {
3179
+ "action": "move",
3180
+ "choice": "rockpolish"
3181
+ },
3182
+ {
3183
+ "action": "move",
3184
+ "choice": "headsmash"
3185
+ },
3186
+ {
3187
+ "action": "move",
3188
+ "choice": "earthquake"
3189
+ },
3190
+ {
3191
+ "action": "move",
3192
+ "choice": "icepunch"
3193
+ },
3194
+ {
3195
+ "action": "switch",
3196
+ "choice": "ambipom"
3197
+ }
3198
+ ]
3199
+ },
3200
+ {
3201
+ "turn": 41,
3202
+ "battle_turn": 37,
3203
+ "player_active_before": {
3204
+ "name": "aggron",
3205
+ "hp": 31.8,
3206
+ "status": "Healthy"
3207
+ },
3208
+ "opponent_active_before": {
3209
+ "name": "salamence",
3210
+ "hp": 49.0,
3211
+ "status": "Healthy"
3212
+ },
3213
+ "player_action": {
3214
+ "action": "switch",
3215
+ "choice": "ambipom"
3216
+ },
3217
+ "opponent_action": {
3218
+ "type": "move",
3219
+ "choice": "Outrage",
3220
+ "source": "p2a: Salamence",
3221
+ "confidence": "logged"
3222
+ },
3223
+ "player_active_after": {
3224
+ "name": "ambipom",
3225
+ "hp": 28.3,
3226
+ "status": "Healthy"
3227
+ },
3228
+ "opponent_active_after": {
3229
+ "name": "salamence",
3230
+ "hp": 49.0,
3231
+ "status": "Healthy"
3232
+ },
3233
+ "reward": -1.175,
3234
+ "cumulative_reward": 4.5092,
3235
+ "commentary": [
3236
+ "p1a: Ambipom switched to Ambipom, L80, F (251/251).",
3237
+ "p2a: Salamence used Outrage on p1a: Ambipom | [from] lockedmove.",
3238
+ "p1a: Ambipom dropped to 71/251 HP."
3239
+ ],
3240
+ "showdown_events": [
3241
+ "switch | p1a: Ambipom | Ambipom, L80, F | 251/251",
3242
+ "move | p2a: Salamence | Outrage | p1a: Ambipom | [from] lockedmove",
3243
+ "-damage | p1a: Ambipom | 71/251",
3244
+ "-start | p2a: Salamence | confusion | [fatigue]",
3245
+ "upkeep",
3246
+ "turn | 38"
3247
+ ],
3248
+ "valid_actions": [
3249
+ {
3250
+ "action": "move",
3251
+ "choice": "rockpolish"
3252
+ },
3253
+ {
3254
+ "action": "move",
3255
+ "choice": "headsmash"
3256
+ },
3257
+ {
3258
+ "action": "move",
3259
+ "choice": "earthquake"
3260
+ },
3261
+ {
3262
+ "action": "move",
3263
+ "choice": "icepunch"
3264
+ },
3265
+ {
3266
+ "action": "switch",
3267
+ "choice": "ambipom"
3268
+ }
3269
+ ]
3270
+ },
3271
+ {
3272
+ "turn": 42,
3273
+ "battle_turn": 38,
3274
+ "player_active_before": {
3275
+ "name": "ambipom",
3276
+ "hp": 28.3,
3277
+ "status": "Healthy"
3278
+ },
3279
+ "opponent_active_before": {
3280
+ "name": "salamence",
3281
+ "hp": 49.0,
3282
+ "status": "Healthy"
3283
+ },
3284
+ "player_action": {
3285
+ "action": "move",
3286
+ "choice": "pursuit"
3287
+ },
3288
+ "opponent_action": null,
3289
+ "player_active_after": {
3290
+ "name": "ambipom",
3291
+ "hp": 28.3,
3292
+ "status": "Healthy"
3293
+ },
3294
+ "opponent_active_after": {
3295
+ "name": "salamence",
3296
+ "hp": 8.0,
3297
+ "status": "Healthy"
3298
+ },
3299
+ "reward": 0.6333,
3300
+ "cumulative_reward": 5.1425,
3301
+ "commentary": [
3302
+ "p2a: Salamence dropped to 36/100 | [from] confusion HP.",
3303
+ "p1a: Ambipom used Pursuit on p2a: Salamence.",
3304
+ "p2a: Salamence dropped to 8/100 HP."
3305
+ ],
3306
+ "showdown_events": [
3307
+ "-activate | p2a: Salamence | confusion",
3308
+ "-damage | p2a: Salamence | 36/100 | [from] confusion",
3309
+ "move | p1a: Ambipom | Pursuit | p2a: Salamence",
3310
+ "-damage | p2a: Salamence | 8/100",
3311
+ "upkeep",
3312
+ "turn | 39"
3313
+ ],
3314
+ "valid_actions": [
3315
+ {
3316
+ "action": "move",
3317
+ "choice": "pursuit"
3318
+ },
3319
+ {
3320
+ "action": "move",
3321
+ "choice": "uturn"
3322
+ },
3323
+ {
3324
+ "action": "move",
3325
+ "choice": "return"
3326
+ },
3327
+ {
3328
+ "action": "move",
3329
+ "choice": "lowkick"
3330
+ },
3331
+ {
3332
+ "action": "switch",
3333
+ "choice": "aggron"
3334
+ }
3335
+ ]
3336
+ },
3337
+ {
3338
+ "turn": 43,
3339
+ "battle_turn": 39,
3340
+ "player_active_before": {
3341
+ "name": "ambipom",
3342
+ "hp": 28.3,
3343
+ "status": "Healthy"
3344
+ },
3345
+ "opponent_active_before": {
3346
+ "name": "salamence",
3347
+ "hp": 8.0,
3348
+ "status": "Healthy"
3349
+ },
3350
+ "player_action": {
3351
+ "action": "move",
3352
+ "choice": "pursuit"
3353
+ },
3354
+ "opponent_action": null,
3355
+ "player_active_after": {
3356
+ "name": "ambipom",
3357
+ "hp": 0.0,
3358
+ "status": "FNT (status) object"
3359
+ },
3360
+ "opponent_active_after": {
3361
+ "name": "salamence",
3362
+ "hp": 8.0,
3363
+ "status": "Healthy"
3364
+ },
3365
+ "reward": -3.4937,
3366
+ "cumulative_reward": 1.6487,
3367
+ "commentary": [],
3368
+ "showdown_events": [],
3369
+ "valid_actions": [
3370
+ {
3371
+ "action": "move",
3372
+ "choice": "pursuit"
3373
+ },
3374
+ {
3375
+ "action": "switch",
3376
+ "choice": "aggron"
3377
+ }
3378
+ ]
3379
+ },
3380
+ {
3381
+ "turn": 44,
3382
+ "battle_turn": 39,
3383
+ "player_active_before": {
3384
+ "name": "ambipom",
3385
+ "hp": 0.0,
3386
+ "status": "FNT (status) object"
3387
+ },
3388
+ "opponent_active_before": {
3389
+ "name": "salamence",
3390
+ "hp": 8.0,
3391
+ "status": "Healthy"
3392
+ },
3393
+ "player_action": {
3394
+ "action": "switch",
3395
+ "choice": "aggron"
3396
+ },
3397
+ "opponent_action": {
3398
+ "type": "move",
3399
+ "choice": "Outrage",
3400
+ "source": "p2a: Salamence",
3401
+ "confidence": "logged"
3402
+ },
3403
+ "player_active_after": {
3404
+ "name": "aggron",
3405
+ "hp": 31.8,
3406
+ "status": "Healthy"
3407
+ },
3408
+ "opponent_active_after": {
3409
+ "name": "salamence",
3410
+ "hp": 8.0,
3411
+ "status": "Healthy"
3412
+ },
3413
+ "reward": -0.05,
3414
+ "cumulative_reward": 1.5987,
3415
+ "commentary": [
3416
+ "p2a: Salamence used Outrage on p1a: Ambipom.",
3417
+ "p1a: Ambipom dropped to 0 fnt HP.",
3418
+ "p1a: Ambipom fainted.",
3419
+ "p1a: Aggron switched to Aggron, L86, F (83/261)."
3420
+ ],
3421
+ "showdown_events": [
3422
+ "-activate | p2a: Salamence | confusion",
3423
+ "move | p2a: Salamence | Outrage | p1a: Ambipom",
3424
+ "-damage | p1a: Ambipom | 0 fnt",
3425
+ "faint | p1a: Ambipom",
3426
+ "upkeep",
3427
+ "switch | p1a: Aggron | Aggron, L86, F | 83/261",
3428
+ "turn | 40"
3429
+ ],
3430
+ "valid_actions": [
3431
+ {
3432
+ "action": "switch",
3433
+ "choice": "aggron"
3434
+ }
3435
+ ]
3436
+ },
3437
+ {
3438
+ "turn": 45,
3439
+ "battle_turn": 40,
3440
+ "player_active_before": {
3441
+ "name": "aggron",
3442
+ "hp": 31.8,
3443
+ "status": "Healthy"
3444
+ },
3445
+ "opponent_active_before": {
3446
+ "name": "salamence",
3447
+ "hp": 8.0,
3448
+ "status": "Healthy"
3449
+ },
3450
+ "player_action": {
3451
+ "action": "move",
3452
+ "choice": "rockpolish"
3453
+ },
3454
+ "opponent_action": {
3455
+ "type": "move",
3456
+ "choice": "Outrage",
3457
+ "source": "p2a: Salamence",
3458
+ "confidence": "logged"
3459
+ },
3460
+ "player_active_after": {
3461
+ "name": "aggron",
3462
+ "hp": 19.2,
3463
+ "status": "Healthy"
3464
+ },
3465
+ "opponent_active_after": {
3466
+ "name": "salamence",
3467
+ "hp": 8.0,
3468
+ "status": "Healthy"
3469
+ },
3470
+ "reward": -0.2562,
3471
+ "cumulative_reward": 1.3425,
3472
+ "commentary": [
3473
+ "p2a: Salamence used Outrage on p1a: Aggron | [from] lockedmove.",
3474
+ "It was resisted.",
3475
+ "p1a: Aggron dropped to 50/261 HP.",
3476
+ "p1a: Aggron used Rock Polish on p1a: Aggron."
3477
+ ],
3478
+ "showdown_events": [
3479
+ "-activate | p2a: Salamence | confusion",
3480
+ "move | p2a: Salamence | Outrage | p1a: Aggron | [from] lockedmove",
3481
+ "-resisted | p1a: Aggron",
3482
+ "-damage | p1a: Aggron | 50/261",
3483
+ "move | p1a: Aggron | Rock Polish | p1a: Aggron",
3484
+ "-boost | p1a: Aggron | spe | 2",
3485
+ "upkeep",
3486
+ "turn | 41"
3487
+ ],
3488
+ "valid_actions": [
3489
+ {
3490
+ "action": "move",
3491
+ "choice": "rockpolish"
3492
+ },
3493
+ {
3494
+ "action": "move",
3495
+ "choice": "headsmash"
3496
+ },
3497
+ {
3498
+ "action": "move",
3499
+ "choice": "earthquake"
3500
+ },
3501
+ {
3502
+ "action": "move",
3503
+ "choice": "icepunch"
3504
+ }
3505
+ ]
3506
+ },
3507
+ {
3508
+ "turn": 46,
3509
+ "battle_turn": 41,
3510
+ "player_active_before": {
3511
+ "name": "aggron",
3512
+ "hp": 19.2,
3513
+ "status": "Healthy"
3514
+ },
3515
+ "opponent_active_before": {
3516
+ "name": "salamence",
3517
+ "hp": 8.0,
3518
+ "status": "Healthy"
3519
+ },
3520
+ "player_action": {
3521
+ "action": "move",
3522
+ "choice": "rockpolish"
3523
+ },
3524
+ "opponent_action": null,
3525
+ "player_active_after": {
3526
+ "name": "aggron",
3527
+ "hp": 19.2,
3528
+ "status": "Healthy"
3529
+ },
3530
+ "opponent_active_after": {
3531
+ "name": "salamence",
3532
+ "hp": 0.0,
3533
+ "status": "FNT (status) object"
3534
+ },
3535
+ "reward": 3.0833,
3536
+ "cumulative_reward": 4.4258,
3537
+ "commentary": [
3538
+ "p2a: Salamence dropped to 0 fnt | [from] confusion HP.",
3539
+ "p2a: Salamence fainted."
3540
+ ],
3541
+ "showdown_events": [
3542
+ "-activate | p2a: Salamence | confusion",
3543
+ "-damage | p2a: Salamence | 0 fnt | [from] confusion",
3544
+ "faint | p2a: Salamence"
3545
+ ],
3546
+ "valid_actions": [
3547
+ {
3548
+ "action": "move",
3549
+ "choice": "rockpolish"
3550
+ },
3551
+ {
3552
+ "action": "move",
3553
+ "choice": "headsmash"
3554
+ },
3555
+ {
3556
+ "action": "move",
3557
+ "choice": "earthquake"
3558
+ },
3559
+ {
3560
+ "action": "move",
3561
+ "choice": "icepunch"
3562
+ }
3563
+ ]
3564
+ }
3565
+ ]
3566
+ }
convert_battle_log.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+
8
+
9
+ ACTIVE_SELF_RE = re.compile(
10
+ r"### Active Self\n- Name: (?P<name>.+?)\n- HP: (?P<hp>[\d.]+)%\n- Status: (?P<status>.+?)\n",
11
+ re.MULTILINE,
12
+ )
13
+ ACTIVE_OPP_RE = re.compile(
14
+ r"### Active Opponent\n- Name: (?P<name>.+?)\n- HP: (?P<hp>[\d.]+)%\n- Status: (?P<status>.+?)\n",
15
+ re.MULTILINE,
16
+ )
17
+
18
+
19
+ def parse_args() -> argparse.Namespace:
20
+ parser = argparse.ArgumentParser(description="Convert a raw recorded battle log into replay JSON.")
21
+ parser.add_argument("--input", required=True, help="Path to detailed battle JSON from record_battle.py")
22
+ parser.add_argument(
23
+ "--output",
24
+ default="battle_logs/replay_battle.json",
25
+ help="Path to replay-friendly output JSON",
26
+ )
27
+ return parser.parse_args()
28
+
29
+
30
+ def _parse_active(markdown: str, pattern: re.Pattern[str]) -> dict:
31
+ match = pattern.search(markdown)
32
+ if not match:
33
+ return {"name": "unknown", "hp": None, "status": "unknown"}
34
+ return {
35
+ "name": match.group("name").strip(),
36
+ "hp": float(match.group("hp")),
37
+ "status": match.group("status").strip(),
38
+ }
39
+
40
+
41
+ def _build_commentary(turn: dict) -> list[str]:
42
+ comments: list[str] = []
43
+ for event in turn.get("showdown_events", []):
44
+ if event.startswith("move | "):
45
+ _, actor, move, target = [part.strip() for part in event.split(" | ", 3)]
46
+ comments.append(f"{actor} used {move} on {target}.")
47
+ elif event.startswith("switch | "):
48
+ _, actor, species, hp = [part.strip() for part in event.split(" | ", 3)]
49
+ comments.append(f"{actor} switched to {species} ({hp}).")
50
+ elif event.startswith("-damage | "):
51
+ _, actor, hp = [part.strip() for part in event.split(" | ", 2)]
52
+ comments.append(f"{actor} dropped to {hp} HP.")
53
+ elif event.startswith("-heal | "):
54
+ _, actor, hp = [part.strip() for part in event.split(" | ", 2)]
55
+ comments.append(f"{actor} recovered to {hp} HP.")
56
+ elif event.startswith("faint | "):
57
+ _, actor = [part.strip() for part in event.split(" | ", 1)]
58
+ comments.append(f"{actor} fainted.")
59
+ elif event.startswith("-supereffective"):
60
+ comments.append("It was super effective.")
61
+ elif event.startswith("-resisted"):
62
+ comments.append("It was resisted.")
63
+ elif event.startswith("-immune"):
64
+ comments.append("The target was immune.")
65
+ elif event.startswith("-status | "):
66
+ parts = [part.strip() for part in event.split(" | ")]
67
+ if len(parts) >= 3:
68
+ comments.append(f"{parts[1]} was inflicted with {parts[2]}.")
69
+ return comments
70
+
71
+
72
+ def convert_battle(raw: dict) -> dict:
73
+ turns = []
74
+ player_team: dict[str, dict] = {}
75
+ opponent_team: dict[str, dict] = {}
76
+
77
+ for turn in raw.get("turns", []):
78
+ pre_state = turn.get("state_markdown", "")
79
+ post_state = turn.get("post_state_markdown", "")
80
+ player_before = _parse_active(pre_state, ACTIVE_SELF_RE)
81
+ opp_before = _parse_active(pre_state, ACTIVE_OPP_RE)
82
+ player_after = _parse_active(post_state, ACTIVE_SELF_RE)
83
+ opp_after = _parse_active(post_state, ACTIVE_OPP_RE)
84
+
85
+ player_team.setdefault(player_before["name"], {"name": player_before["name"]})
86
+ if opp_before["name"] != "unknown":
87
+ opponent_team.setdefault(opp_before["name"], {"name": opp_before["name"]})
88
+ if opp_after["name"] != "unknown":
89
+ opponent_team.setdefault(opp_after["name"], {"name": opp_after["name"]})
90
+
91
+ turns.append(
92
+ {
93
+ "turn": turn["turn"],
94
+ "battle_turn": turn.get("battle_turn"),
95
+ "player_active_before": player_before,
96
+ "opponent_active_before": opp_before,
97
+ "player_action": turn.get("chosen_action"),
98
+ "opponent_action": turn.get("opponent_action"),
99
+ "player_active_after": player_after,
100
+ "opponent_active_after": opp_after,
101
+ "reward": turn.get("reward"),
102
+ "cumulative_reward": turn.get("cumulative_reward"),
103
+ "commentary": _build_commentary(turn),
104
+ "showdown_events": turn.get("showdown_events", []),
105
+ "valid_actions": turn.get("valid_actions", []),
106
+ }
107
+ )
108
+
109
+ return {
110
+ "meta": {
111
+ "title": "OpenEnv-WolfeClick Replay",
112
+ "model": raw.get("model"),
113
+ "format": raw.get("format"),
114
+ "outcome": raw.get("outcome"),
115
+ "natural_finish": raw.get("natural_finish"),
116
+ "final_reason": raw.get("final_reason"),
117
+ "battle_tag": raw.get("battle_tag"),
118
+ "room_path": raw.get("room_path"),
119
+ "total_turns": raw.get("total_turns"),
120
+ "total_reward": raw.get("total_reward"),
121
+ },
122
+ "teams": {
123
+ "player": list(player_team.values()),
124
+ "opponent": list(opponent_team.values()),
125
+ },
126
+ "turns": turns,
127
+ }
128
+
129
+
130
+ def main() -> None:
131
+ args = parse_args()
132
+ input_path = Path(args.input)
133
+ output_path = Path(args.output)
134
+ output_path.parent.mkdir(parents=True, exist_ok=True)
135
+
136
+ raw = json.loads(input_path.read_text())
137
+ replay = convert_battle(raw)
138
+ output_path.write_text(json.dumps(replay, indent=2))
139
+ print(f"Wrote replay JSON to {output_path}")
140
+ print(json.dumps(replay["meta"], indent=2))
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
requirements_space.txt CHANGED
@@ -1 +1,2 @@
1
- gradio>=4.0.0
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn>=0.30.0
space_app.py CHANGED
@@ -1,360 +1,34 @@
1
- """OpenEnv-WolfeClick Battle Replay Viewer — Gradio HF Space app."""
2
  from __future__ import annotations
3
 
4
  import json
5
- import time
6
  from pathlib import Path
7
 
8
- import gradio as gr
 
 
9
 
10
- BATTLE_LOGS_DIR = Path(__file__).parent / "battle_logs"
11
 
12
- # ---------------------------------------------------------------------------
13
- # Helpers
14
- # ---------------------------------------------------------------------------
 
15
 
16
- def load_battle_log(path: Path) -> dict:
17
- with open(path) as f:
18
- return json.load(f)
19
 
20
 
21
- def list_battle_logs() -> list[str]:
22
- if not BATTLE_LOGS_DIR.exists():
23
- return []
24
- return sorted(p.name for p in BATTLE_LOGS_DIR.glob("*.json"))
25
 
26
 
27
- def format_action_badge(action: dict, was_illegal: bool) -> str:
28
- kind = action.get("action", "?")
29
- choice = action.get("choice", "?")
30
- icon = "[ATK]" if kind == "move" else "[SW]"
31
- badge = f"`{icon} {kind}: {choice}`"
32
- if was_illegal:
33
- badge += " **[!] illegal -- fallback used**"
34
- return badge
35
 
36
 
37
- def format_valid_actions(actions: list[dict]) -> str:
38
- parts = []
39
- for a in actions:
40
- icon = "[ATK]" if a["action"] == "move" else "[SW]"
41
- parts.append(f"{icon} {a['choice']}")
42
- return " | ".join(parts)
43
 
44
-
45
- def reward_color(r: float) -> str:
46
- if r > 0:
47
- return "green"
48
- elif r < 0:
49
- return "red"
50
- return "gray"
51
-
52
-
53
- def build_turn_block(turn: dict) -> str:
54
- t = turn["turn"]
55
- action = turn["chosen_action"]
56
- reward = turn["reward"]
57
- cumulative = turn["cumulative_reward"]
58
- illegal = turn.get("action_was_illegal", False)
59
- state_md = turn["state_markdown"]
60
- valid = turn.get("valid_actions", [])
61
-
62
- rc = reward_color(reward)
63
- sign = "+" if reward >= 0 else ""
64
-
65
- lines = [
66
- f"### Turn {t}",
67
- "",
68
- f"**Action:** {format_action_badge(action, illegal)}",
69
- "",
70
- f"**Reward:** <span style='color:{rc};font-weight:bold'>{sign}{reward:.2f}</span>"
71
- f" &nbsp;|&nbsp; **Cumulative:** {cumulative:.2f}",
72
- "",
73
- "<details><summary><b>Battle State</b> (click to expand)</summary>",
74
- "",
75
- state_md,
76
- "",
77
- "</details>",
78
- "",
79
- f"<details><summary><b>Legal Actions</b></summary>\n\n{format_valid_actions(valid)}\n\n</details>",
80
- "",
81
- "---",
82
- ]
83
- return "\n".join(lines)
84
-
85
-
86
- def build_summary(log: dict) -> str:
87
- outcome = log.get("outcome", "unknown")
88
- total_reward = log.get("total_reward", 0)
89
- total_turns = log.get("total_turns", 0)
90
- model = log.get("model", "Unknown")
91
- fmt = log.get("format", "unknown")
92
-
93
- icon = "WIN" if outcome == "won" else ("LOSS" if outcome == "lost" else "DRAW")
94
- rc = reward_color(total_reward)
95
- sign = "+" if total_reward >= 0 else ""
96
-
97
- return (
98
- f"## {icon} Battle Result: **{outcome.upper()}**\n\n"
99
- f"| Metric | Value |\n|---|---|\n"
100
- f"| Model | `{model}` |\n"
101
- f"| Format | `{fmt}` |\n"
102
- f"| Turns | {total_turns} |\n"
103
- f"| Total Reward | <span style='color:{rc};font-weight:bold'>{sign}{total_reward:.2f}</span> |\n"
104
- )
105
-
106
-
107
- # ---------------------------------------------------------------------------
108
- # Replay generator
109
- # ---------------------------------------------------------------------------
110
-
111
- def replay_battle(log_name: str, speed: float):
112
- """Yield turn-by-turn markdown replay."""
113
- if not log_name:
114
- yield "Select a battle log to replay."
115
- return
116
-
117
- log = load_battle_log(BATTLE_LOGS_DIR / log_name)
118
- turns = log.get("turns", [])
119
- if not turns:
120
- yield "No turns recorded in this battle."
121
- return
122
-
123
- output_parts: list[str] = []
124
- for turn in turns:
125
- output_parts.append(build_turn_block(turn))
126
- yield "\n".join(output_parts)
127
- time.sleep(max(0.1, speed))
128
-
129
- output_parts.append(build_summary(log))
130
- yield "\n".join(output_parts)
131
-
132
-
133
- # ---------------------------------------------------------------------------
134
- # Tab content
135
- # ---------------------------------------------------------------------------
136
-
137
- ENV_DESIGN_MD = """
138
- # Environment Design
139
-
140
- ## State Format
141
-
142
- Each turn, the model receives a structured markdown state with three sections:
143
-
144
- ### Part A: Active Field
145
- Current active Pokemon for both sides — HP, status, ability, item, stat modifiers, and estimated opponent speed range.
146
-
147
- ### Part B: Full Self Roster
148
- All 6 Pokemon on the player's team with HP, status, item, and all known moves (with type abbreviation and base power).
149
-
150
- ### Part C: Opponent History
151
- Every opponent Pokemon revealed so far — last known HP, status, revealed moves, items, and abilities. Updated incrementally each turn.
152
-
153
- ---
154
-
155
- ## Action Space
156
-
157
- The model must output exactly one JSON action:
158
-
159
- ```json
160
- {"action": "move" | "switch", "choice": "Exact Name of Move or Pokemon"}
161
- ```
162
-
163
- Up to **4 moves** and **5 switches** are available per turn. The environment validates the action against legal options and applies a **-10.0 penalty** for illegal actions (hallucinated moves/Pokemon).
164
-
165
- ---
166
-
167
- ## Reward Components
168
-
169
- | Component | Signal | Range |
170
- |---|---|---|
171
- | Damage dealt | +1.0 per 10% opponent HP reduced | Positive |
172
- | Damage taken | -1.0 per 10% self HP lost | Negative |
173
- | Knockouts | +3.0 per opponent faint, -3.0 per self faint | +/- |
174
- | Healing | +1.0 per 10% healed (capped at 3.0/battle) | Positive |
175
- | Setup | +0.5 per stat stage gained (capped at 2.0/mon) | Positive |
176
- | Opponent setup | -0.5 per opponent stat stage gained | Negative |
177
- | Type effectiveness | +0.5 super effective, -1.0 immune | +/- |
178
- | Passive damage | Incremental reward for hazards/weather | Positive |
179
- | Status inflicted | -0.5 to -1.0 for status ailments on self | Negative |
180
- | Illegal action | -10.0 for hallucinated actions | Negative |
181
- | Step penalty | -0.05 per turn (anti-stall) | Negative |
182
-
183
- ---
184
-
185
- ## Training Pipeline
186
-
187
- ```
188
- Base Model (Qwen3-4B-Instruct)
189
- |
190
- [JSON Warm-up SFT] <-- establish legal action baseline
191
- |
192
- [Rollout Collection] <-- live Pokemon Showdown battles
193
- |
194
- [GRPO Training] <-- optimize policy on real trajectories
195
- |
196
- LoRA Checkpoint --> Hugging Face Hub
197
- ```
198
-
199
- The training uses **GRPO (Group Relative Policy Optimization)** on real rollout data collected from live Pokemon Showdown battles. Each rollout records the state, chosen action, and shaped reward, which GRPO uses to improve the policy beyond simple format compliance.
200
-
201
- ---
202
-
203
- ## Architecture
204
-
205
- ```
206
- Pokemon Showdown (Node.js, port 8000)
207
- | WebSocket
208
- PokeEnvClient (async background loop)
209
- |-- RLPlayer (queue-driven, receives model actions)
210
- |-- RandomPlayer (opponent)
211
- |
212
- PokemonShowdownEnv (synchronous wrapper)
213
- |-- state_formatter (markdown state for LLM)
214
- |-- action_space (JSON validation + matching)
215
- |-- reward calculator (shaped multi-component reward)
216
- |
217
- OpenEnv Server (FastAPI, standard reset/step API)
218
- ```
219
- """
220
-
221
- GET_STARTED_MD = """
222
- # Get Started
223
-
224
- ## Installation
225
-
226
- ```bash
227
- git clone https://github.com/Atharva2099/OpenEnv-WolfeClick.git
228
- cd OpenEnv-WolfeClick
229
- pip install -e .
230
- ```
231
-
232
- ## Run a local battle (random actions)
233
-
234
- ```bash
235
- python examples/run_single_episode.py
236
- ```
237
-
238
- ## Watch a trained model battle
239
-
240
- ```bash
241
- python examples/watch_model_battle.py --revision grpo-qwen3-4b-run3
242
- ```
243
-
244
- ## Record a battle to JSON
245
-
246
- ```bash
247
- python record_battle.py --revision grpo-qwen3-4b-run3 --output battle_logs/my_battle.json
248
- ```
249
-
250
- ## Training (Colab)
251
-
252
- Open `trainer.ipynb` in Google Colab with a GPU runtime. The notebook handles:
253
- 1. Starting a local Pokemon Showdown server
254
- 2. Collecting rollout trajectories from live battles
255
- 3. Training a LoRA adapter with GRPO
256
- 4. Saving the checkpoint to Hugging Face Hub
257
-
258
- ---
259
-
260
- ## Links
261
-
262
- | Resource | Link |
263
- |---|---|
264
- | Model Weights | [Atharva2099/openenv-smogon-rl](https://huggingface.co/Atharva2099/openenv-smogon-rl) |
265
- | Training Notebook | `trainer.ipynb` |
266
- | Watch Battle Notebook | `watch_battle.ipynb` |
267
- | Benchmark Notebook | `benchmarks/benchmark.ipynb` |
268
-
269
- ## Trained Checkpoints
270
-
271
- | Checkpoint | Description |
272
- |---|---|
273
- | `grpo-qwen3-4b-run1` | First GRPO training run |
274
- | `grpo-qwen3-4b-run2` | Second run with tuned reward shaping |
275
- | `grpo-qwen3-4b-run3` | Third run, best performing |
276
-
277
- ## OpenEnv Integration
278
-
279
- This environment follows the [OpenEnv](https://github.com/openenv) standard. The server exposes `reset()` and `step()` endpoints:
280
-
281
- ```python
282
- from env.client import WolfeClickEnv
283
-
284
- env = WolfeClickEnv(server_url="http://localhost:8001")
285
- obs = env.reset()
286
- obs = env.step({"action_json": '{"action": "move", "choice": "flamethrower"}'})
287
- ```
288
- """
289
-
290
-
291
- # ---------------------------------------------------------------------------
292
- # App
293
- # ---------------------------------------------------------------------------
294
-
295
- def build_app() -> gr.Blocks:
296
- available_logs = list_battle_logs()
297
- default_log = available_logs[0] if available_logs else None
298
-
299
- with gr.Blocks(
300
- title="OpenEnv-WolfeClick",
301
- ) as app:
302
- gr.HTML(
303
- "<h1 class='main-title'>OpenEnv-WolfeClick</h1>"
304
- "<p class='subtitle'>Train LLMs to play competitive Pokemon Showdown with GRPO</p>"
305
- )
306
-
307
- with gr.Tabs():
308
- # ---- Tab 1: Battle Replay ----
309
- with gr.Tab("Battle Replay", id="replay"):
310
- gr.Markdown(
311
- "Watch a pre-recorded battle played by the GRPO-trained model. "
312
- "Each turn shows the battle state the model received, the action it chose, "
313
- "and the shaped reward signal."
314
- )
315
- with gr.Row():
316
- log_selector = gr.Dropdown(
317
- choices=available_logs,
318
- value=default_log,
319
- label="Battle Log",
320
- scale=3,
321
- )
322
- speed_slider = gr.Slider(
323
- minimum=0.1,
324
- maximum=3.0,
325
- value=0.8,
326
- step=0.1,
327
- label="Replay Speed (seconds per turn)",
328
- scale=2,
329
- )
330
- play_btn = gr.Button("Play Replay", variant="primary", scale=1)
331
-
332
- replay_output = gr.Markdown(
333
- value="Click **Play Replay** to start.",
334
- label="Battle",
335
- )
336
-
337
- play_btn.click(
338
- fn=replay_battle,
339
- inputs=[log_selector, speed_slider],
340
- outputs=replay_output,
341
- )
342
-
343
- # ---- Tab 2: Environment Design ----
344
- with gr.Tab("Environment Design", id="design"):
345
- gr.Markdown(ENV_DESIGN_MD)
346
-
347
- # ---- Tab 3: Get Started ----
348
- with gr.Tab("Get Started", id="start"):
349
- gr.Markdown(GET_STARTED_MD)
350
-
351
- return app
352
-
353
-
354
- if __name__ == "__main__":
355
- app = build_app()
356
- app.launch(
357
- server_name="0.0.0.0",
358
- server_port=7860,
359
- theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
360
- )
 
 
1
  from __future__ import annotations
2
 
3
  import json
 
4
  from pathlib import Path
5
 
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.responses import FileResponse, JSONResponse
8
+ from fastapi.staticfiles import StaticFiles
9
 
 
10
 
11
+ ROOT = Path(__file__).parent
12
+ STATIC_DIR = ROOT / "static"
13
+ BATTLE_LOGS_DIR = ROOT / "battle_logs"
14
+ REPLAY_PATH = BATTLE_LOGS_DIR / "replay_battle.json"
15
 
16
+ app = FastAPI(title="OpenEnv-WolfeClick")
17
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
 
18
 
19
 
20
+ def load_replay() -> dict:
21
+ if not REPLAY_PATH.exists():
22
+ raise HTTPException(status_code=404, detail="replay_battle.json not found")
23
+ return json.loads(REPLAY_PATH.read_text())
24
 
25
 
26
+ @app.get("/")
27
+ def index() -> FileResponse:
28
+ return FileResponse(STATIC_DIR / "index.html")
 
 
 
 
 
29
 
30
 
31
+ @app.get("/api/replay")
32
+ def replay() -> JSONResponse:
33
+ return JSONResponse(load_replay())
 
 
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/app.js ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const replayState = {
2
+ data: null,
3
+ currentTurn: 1,
4
+ autoplayTimer: null,
5
+ frameMode: false,
6
+ };
7
+
8
+ const el = {
9
+ tabs: [...document.querySelectorAll(".top-tab")],
10
+ panels: [...document.querySelectorAll(".tab-panel")],
11
+ landingCard: document.getElementById("landing-card"),
12
+ battleScreen: document.getElementById("battle-screen"),
13
+ turnTitle: document.getElementById("turn-title"),
14
+ battleSubtitle: document.getElementById("battle-subtitle"),
15
+ metaOutcome: document.getElementById("meta-outcome"),
16
+ metaTotalReward: document.getElementById("meta-total-reward"),
17
+ playerName: document.getElementById("player-name"),
18
+ playerStatus: document.getElementById("player-status"),
19
+ playerTransition: document.getElementById("player-transition"),
20
+ playerSprite: document.getElementById("player-sprite"),
21
+ playerHpBar: document.querySelector("#player-hp-bar span"),
22
+ playerHpLabel: document.getElementById("player-hp-label"),
23
+ opponentName: document.getElementById("opponent-name"),
24
+ opponentStatus: document.getElementById("opponent-status"),
25
+ opponentTransition: document.getElementById("opponent-transition"),
26
+ opponentSprite: document.getElementById("opponent-sprite"),
27
+ opponentHpBar: document.querySelector("#opponent-hp-bar span"),
28
+ opponentHpLabel: document.getElementById("opponent-hp-label"),
29
+ modelAction: document.getElementById("model-action"),
30
+ opponentAction: document.getElementById("opponent-action"),
31
+ rewardLine: document.getElementById("reward-line"),
32
+ commentaryList: document.getElementById("commentary-list"),
33
+ playerTeam: document.getElementById("player-team"),
34
+ opponentTeam: document.getElementById("opponent-team"),
35
+ validActions: document.getElementById("valid-actions"),
36
+ summaryLines: document.getElementById("summary-lines"),
37
+ speed: document.getElementById("speed"),
38
+ speedValue: document.getElementById("speed-value"),
39
+ startBtn: document.getElementById("start-btn"),
40
+ frameBtn: document.getElementById("frame-btn"),
41
+ prevBtn: document.getElementById("prev-btn"),
42
+ nextBtn: document.getElementById("next-btn"),
43
+ jumpBtn: document.getElementById("jump-btn"),
44
+ turnInput: document.getElementById("turn-input"),
45
+ };
46
+
47
+ function toShowdownName(name) {
48
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
49
+ }
50
+
51
+ function spriteCandidates(name) {
52
+ const s = toShowdownName(name);
53
+ return [
54
+ `https://play.pokemonshowdown.com/sprites/gen5/${s}.png`,
55
+ `https://play.pokemonshowdown.com/sprites/gen4/${s}.png`,
56
+ `https://play.pokemonshowdown.com/sprites/gen3/${s}.png`,
57
+ `https://play.pokemonshowdown.com/sprites/gen2/${s}.png`,
58
+ ];
59
+ }
60
+
61
+ function applySprite(img, name, mirrored) {
62
+ const sources = spriteCandidates(name);
63
+ img.classList.toggle("player-sprite", mirrored);
64
+ let index = 0;
65
+ img.onerror = () => {
66
+ index += 1;
67
+ if (index < sources.length) {
68
+ img.src = sources[index];
69
+ } else {
70
+ img.onerror = null;
71
+ img.alt = name;
72
+ img.src =
73
+ "data:image/svg+xml;utf8," +
74
+ encodeURIComponent(
75
+ `<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'>
76
+ <rect width='100%' height='100%' rx='28' fill='#f7f2ef' stroke='#ff7aa2' stroke-width='3'/>
77
+ <text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle'
78
+ font-family='Space Grotesk' font-size='18' fill='#221830'>${name}</text>
79
+ </svg>`
80
+ );
81
+ }
82
+ };
83
+ img.src = sources[0];
84
+ }
85
+
86
+ function hpColor(value) {
87
+ if (value <= 20) return "#ff6b7a";
88
+ if (value <= 50) return "#ffd166";
89
+ return "#6de29c";
90
+ }
91
+
92
+ function setHp(target, label, hp) {
93
+ const value = Number.isFinite(hp) ? Math.max(0, Math.min(100, hp)) : 0;
94
+ target.style.width = `${value}%`;
95
+ target.style.background = `linear-gradient(90deg, ${hpColor(value)} 0%, ${value <= 20 ? "#ff8793" : value <= 50 ? "#ffe29b" : "#9af0b5"} 100%)`;
96
+ label.textContent = `${Math.round(value)}%`;
97
+ }
98
+
99
+ function actionText(label, action) {
100
+ if (!action) return `${label}: unknown`;
101
+ const type = action.action || action.type || "?";
102
+ return `${label}: ${type} - ${action.choice || "?"}`;
103
+ }
104
+
105
+ function renderChips(container, items, activeName = null, hidden = 0, formatter = (x) => x) {
106
+ container.innerHTML = "";
107
+ items.forEach((item) => {
108
+ const value = formatter(item);
109
+ const chip = document.createElement("span");
110
+ chip.className = "chip";
111
+ if (typeof item === "string" && item === activeName) chip.classList.add("active");
112
+ if (item?.action) chip.classList.add(item.action);
113
+ chip.textContent = value;
114
+ container.appendChild(chip);
115
+ });
116
+ for (let i = 0; i < hidden; i += 1) {
117
+ const chip = document.createElement("span");
118
+ chip.className = "chip hidden";
119
+ chip.textContent = "Unknown";
120
+ container.appendChild(chip);
121
+ }
122
+ }
123
+
124
+ function revealedOpponent(turnNumber) {
125
+ const revealed = [];
126
+ replayState.data.turns.slice(0, turnNumber).forEach((turn) => {
127
+ [turn.opponent_active_before.name, turn.opponent_active_after.name].forEach((name) => {
128
+ if (name && name !== "unknown" && !revealed.includes(name)) revealed.push(name);
129
+ });
130
+ });
131
+ return revealed;
132
+ }
133
+
134
+ function renderSummary() {
135
+ const { meta, teams } = replayState.data;
136
+ const playerTeam = teams.player.map((m) => m.name).join(", ");
137
+ const opponentTeam = teams.opponent.map((m) => m.name).join(", ");
138
+ const lines = [
139
+ ["Outcome", String(meta.outcome || "unknown").toUpperCase()],
140
+ ["Total Turns", String(meta.total_turns || 0)],
141
+ ["Total Reward", Number(meta.total_reward || 0).toFixed(2)],
142
+ ["Player Team", playerTeam],
143
+ ["Opponent Team", opponentTeam],
144
+ ["Model", meta.model || "Unknown"],
145
+ ];
146
+ el.summaryLines.innerHTML = lines
147
+ .map(
148
+ ([label, value]) =>
149
+ `<div class="summary-line"><span class="label">${label}:</span><span class="value">${value}</span></div>`
150
+ )
151
+ .join("");
152
+ }
153
+
154
+ function renderTurn(turnIndex) {
155
+ if (!replayState.data) return;
156
+ const turns = replayState.data.turns;
157
+ const turn = turns[Math.max(0, Math.min(turns.length - 1, turnIndex - 1))];
158
+ replayState.currentTurn = turn.turn;
159
+ el.turnInput.value = String(turn.turn);
160
+
161
+ const pBefore = turn.player_active_before;
162
+ const pAfter = turn.player_active_after?.name !== "unknown" ? turn.player_active_after : pBefore;
163
+ const oBefore = turn.opponent_active_before;
164
+ const oAfter = turn.opponent_active_after?.name !== "unknown" ? turn.opponent_active_after : oBefore;
165
+
166
+ el.landingCard.classList.add("is-hidden");
167
+ el.battleScreen.classList.remove("is-hidden");
168
+
169
+ el.turnTitle.textContent = `Turn ${turn.turn} Replay`;
170
+ el.battleSubtitle.textContent = replayState.frameMode
171
+ ? "Frame-by-frame inspection mode"
172
+ : "Autoplay battle replay";
173
+ el.metaOutcome.textContent = String(replayState.data.meta.outcome || "unknown").toUpperCase();
174
+ el.metaTotalReward.textContent = Number(replayState.data.meta.total_reward || 0).toFixed(2);
175
+
176
+ el.playerName.textContent = pAfter.name;
177
+ el.playerStatus.textContent = pAfter.status;
178
+ el.playerTransition.textContent = pBefore.name !== pAfter.name ? `Started turn as ${pBefore.name}` : "";
179
+ applySprite(el.playerSprite, pAfter.name, true);
180
+ setHp(el.playerHpBar, el.playerHpLabel, pAfter.hp);
181
+
182
+ el.opponentName.textContent = oAfter.name;
183
+ el.opponentStatus.textContent = oAfter.status;
184
+ el.opponentTransition.textContent = oBefore.name !== oAfter.name ? `Started turn as ${oBefore.name}` : "";
185
+ applySprite(el.opponentSprite, oAfter.name, false);
186
+ setHp(el.opponentHpBar, el.opponentHpLabel, oAfter.hp);
187
+
188
+ el.modelAction.textContent = actionText("Model", turn.player_action);
189
+ el.opponentAction.textContent = actionText("Opponent", turn.opponent_action);
190
+ el.rewardLine.textContent = `Reward: ${Number(turn.reward || 0).toFixed(2)} | Cumulative: ${Number(turn.cumulative_reward || 0).toFixed(2)}`;
191
+
192
+ el.commentaryList.innerHTML = "";
193
+ (turn.commentary || []).slice(0, 8).forEach((line) => {
194
+ const li = document.createElement("li");
195
+ li.textContent = line;
196
+ el.commentaryList.appendChild(li);
197
+ });
198
+
199
+ renderChips(el.playerTeam, replayState.data.teams.player.map((m) => m.name), pAfter.name);
200
+ const knownOpp = revealedOpponent(turn.turn);
201
+ renderChips(el.opponentTeam, knownOpp, oAfter.name, Math.max(0, 6 - knownOpp.length));
202
+ renderChips(
203
+ el.validActions,
204
+ turn.valid_actions || [],
205
+ null,
206
+ 0,
207
+ (action) => `${action.action}: ${action.choice}`
208
+ );
209
+ }
210
+
211
+ function stopAutoplay() {
212
+ if (replayState.autoplayTimer) {
213
+ clearTimeout(replayState.autoplayTimer);
214
+ replayState.autoplayTimer = null;
215
+ }
216
+ }
217
+
218
+ function stepTo(turnNumber) {
219
+ stopAutoplay();
220
+ replayState.frameMode = true;
221
+ renderTurn(turnNumber);
222
+ }
223
+
224
+ function autoplayFrom(turnNumber = 1) {
225
+ stopAutoplay();
226
+ replayState.frameMode = false;
227
+ let index = Math.max(1, turnNumber);
228
+
229
+ const tick = () => {
230
+ renderTurn(index);
231
+ if (index >= replayState.data.turns.length) {
232
+ replayState.autoplayTimer = null;
233
+ return;
234
+ }
235
+ const delay = Number(el.speed.value || 2.5) * 1000;
236
+ index += 1;
237
+ replayState.autoplayTimer = setTimeout(tick, delay);
238
+ };
239
+
240
+ tick();
241
+ }
242
+
243
+ function activateTab(name) {
244
+ el.tabs.forEach((tab) => tab.classList.toggle("is-active", tab.dataset.tab === name));
245
+ el.panels.forEach((panel) => panel.classList.toggle("is-active", panel.id === `tab-${name}`));
246
+ }
247
+
248
+ async function boot() {
249
+ const res = await fetch("/api/replay");
250
+ replayState.data = await res.json();
251
+ renderSummary();
252
+ activateTab("replay");
253
+ }
254
+
255
+ el.tabs.forEach((tab) => {
256
+ tab.addEventListener("click", () => activateTab(tab.dataset.tab));
257
+ });
258
+
259
+ el.speed.addEventListener("input", () => {
260
+ el.speedValue.textContent = `${Number(el.speed.value).toFixed(1)}s`;
261
+ });
262
+
263
+ el.startBtn.addEventListener("click", () => autoplayFrom(1));
264
+ el.frameBtn.addEventListener("click", () => stepTo(1));
265
+ el.prevBtn.addEventListener("click", () => stepTo(replayState.currentTurn - 1));
266
+ el.nextBtn.addEventListener("click", () => stepTo(replayState.currentTurn + 1));
267
+ el.jumpBtn.addEventListener("click", () => stepTo(Number(el.turnInput.value || 1)));
268
+ el.turnInput.addEventListener("keydown", (event) => {
269
+ if (event.key === "Enter") stepTo(Number(el.turnInput.value || 1));
270
+ });
271
+
272
+ boot();
static/index.html ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenEnv-WolfeClick</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Grotesk:wght@400;500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="/static/styles.css" />
14
+ </head>
15
+ <body>
16
+ <div class="page-shell">
17
+ <header class="hero">
18
+ <div class="hero__glow"></div>
19
+ <h1>OpenEnv-WolfeClick</h1>
20
+ <p>Watch how our fine-tuned model plays Pokemon.</p>
21
+ </header>
22
+
23
+ <nav class="top-tabs" aria-label="Primary">
24
+ <button class="top-tab is-active" data-tab="replay">Battle Replay</button>
25
+ <button class="top-tab" data-tab="design">Environment Design</button>
26
+ <button class="top-tab" data-tab="start">Get Started</button>
27
+ </nav>
28
+
29
+ <main>
30
+ <section id="tab-replay" class="tab-panel is-active">
31
+ <div class="replay-layout">
32
+ <section class="battle-column">
33
+ <div id="landing-card" class="landing-card">
34
+ <div class="landing-card__inner">
35
+ <h2>Watch how our fine-tuned model plays Pokemon</h2>
36
+ <p>
37
+ Start the replay to watch the full battle automatically, or switch to
38
+ frame-by-frame mode to inspect the exact state, legal actions, and decisions
39
+ on each turn.
40
+ </p>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="battle-screen" class="battle-screen is-hidden">
45
+ <div class="battle-screen__top">
46
+ <div>
47
+ <div id="turn-title" class="battle-title">Turn 1 Replay</div>
48
+ <div id="battle-subtitle" class="battle-subtitle">Model-visible strategic replay</div>
49
+ </div>
50
+ <div class="battle-meta">
51
+ <div><span>Outcome</span><strong id="meta-outcome">WON</strong></div>
52
+ <div><span>Total Reward</span><strong id="meta-total-reward">0.00</strong></div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="arena-grid">
57
+ <article class="battle-card">
58
+ <div class="battle-card__header">
59
+ <h3 id="player-name">Player</h3>
60
+ <span id="player-status" class="status-pill">Healthy</span>
61
+ </div>
62
+ <div id="player-transition" class="transition-line"></div>
63
+ <div class="sprite-frame">
64
+ <img id="player-sprite" alt="Player Pokemon" />
65
+ </div>
66
+ <div id="player-hp-bar" class="hp-bar"><span></span></div>
67
+ <div id="player-hp-label" class="hp-label">100%</div>
68
+ </article>
69
+
70
+ <article class="battle-card">
71
+ <div class="battle-card__header">
72
+ <h3 id="opponent-name">Opponent</h3>
73
+ <span id="opponent-status" class="status-pill">Healthy</span>
74
+ </div>
75
+ <div id="opponent-transition" class="transition-line"></div>
76
+ <div class="sprite-frame">
77
+ <img id="opponent-sprite" alt="Opponent Pokemon" />
78
+ </div>
79
+ <div id="opponent-hp-bar" class="hp-bar"><span></span></div>
80
+ <div id="opponent-hp-label" class="hp-label">100%</div>
81
+ </article>
82
+ </div>
83
+
84
+ <div class="detail-grid">
85
+ <article class="panel-card">
86
+ <h4>Turn Actions</h4>
87
+ <div id="model-action" class="action-line"></div>
88
+ <div id="opponent-action" class="action-line"></div>
89
+ <div id="reward-line" class="reward-line"></div>
90
+ </article>
91
+
92
+ <article class="panel-card">
93
+ <h4>Turn Commentary</h4>
94
+ <ul id="commentary-list" class="commentary-list"></ul>
95
+ </article>
96
+ </div>
97
+
98
+ <div class="detail-grid">
99
+ <article class="panel-card">
100
+ <h4>Model Visible Team State</h4>
101
+ <div class="state-label">Your Team</div>
102
+ <div id="player-team" class="chip-row"></div>
103
+ <div class="state-label">Opponent Revealed So Far</div>
104
+ <div id="opponent-team" class="chip-row"></div>
105
+ </article>
106
+
107
+ <article class="panel-card">
108
+ <h4>Legal Actions This Turn</h4>
109
+ <div id="valid-actions" class="chip-row"></div>
110
+ </article>
111
+ </div>
112
+ </div>
113
+ </section>
114
+
115
+ <aside class="sidebar">
116
+ <section class="sidebar-card">
117
+ <h3>Replay Summary</h3>
118
+ <div id="summary-lines" class="summary-lines"></div>
119
+ </section>
120
+
121
+ <section class="sidebar-card">
122
+ <label class="control-label" for="speed">Autoplay Speed</label>
123
+ <div class="speed-row">
124
+ <input id="speed" type="range" min="1" max="10" step="0.5" value="2.5" />
125
+ <span id="speed-value" class="speed-value">2.5s</span>
126
+ </div>
127
+ </section>
128
+
129
+ <section class="sidebar-card action-stack">
130
+ <button id="start-btn" class="btn btn--primary">Start Game</button>
131
+ <button id="frame-btn" class="btn btn--secondary">View Frame by Frame</button>
132
+ </section>
133
+
134
+ <section class="sidebar-card navigator-card">
135
+ <div class="navigator-top">
136
+ <button id="prev-btn" class="btn btn--icon" aria-label="Previous turn">◀</button>
137
+ <div class="turn-box">
138
+ <label class="control-label" for="turn-input">Turn</label>
139
+ <input id="turn-input" type="number" min="1" step="1" value="1" />
140
+ </div>
141
+ <button id="next-btn" class="btn btn--icon" aria-label="Next turn">▶</button>
142
+ </div>
143
+ <button id="jump-btn" class="btn btn--secondary btn--wide">Jump To Turn</button>
144
+ </section>
145
+ </aside>
146
+ </div>
147
+ </section>
148
+
149
+ <section id="tab-design" class="tab-panel content-page">
150
+ <article class="content-card">
151
+ <h2>Environment Design</h2>
152
+ <p>
153
+ OpenEnv-WolfeClick wraps competitive Pokemon Showdown as an OpenEnv-compatible
154
+ environment.
155
+ </p>
156
+ <p>The model receives:</p>
157
+ <ul>
158
+ <li>the active field</li>
159
+ <li>the full self roster</li>
160
+ <li>revealed opponent history</li>
161
+ <li>the exact legal actions for the turn</li>
162
+ </ul>
163
+ <p>It must emit exactly one JSON action:</p>
164
+ <pre><code>{"action": "move" | "switch", "choice": "Exact Name of Move or Pokemon"}</code></pre>
165
+ <p>
166
+ The training loop then collects real trajectories from live battles and applies GRPO
167
+ to improve the action policy on those rollouts.
168
+ </p>
169
+ </article>
170
+ </section>
171
+
172
+ <section id="tab-start" class="tab-panel content-page">
173
+ <article class="content-card">
174
+ <h2>Get Started</h2>
175
+ <h3>Record a real battle</h3>
176
+ <pre><code>python record_battle.py --revision grpo-qwen3-4b-run3 --output battle_logs/raw_battle.json</code></pre>
177
+ <h3>Convert it for replay</h3>
178
+ <pre><code>python convert_battle_log.py --input battle_logs/raw_battle.json --output battle_logs/replay_battle.json</code></pre>
179
+ <h3>Launch locally</h3>
180
+ <pre><code>uv venv
181
+ source .venv/bin/activate
182
+ uv pip install -r requirements_space.txt
183
+ uvicorn space_app:app --reload --host 0.0.0.0 --port 7860</code></pre>
184
+ <h3>Included notebooks</h3>
185
+ <ul>
186
+ <li><code>trainer.ipynb</code></li>
187
+ <li><code>watch_battle.ipynb</code></li>
188
+ <li><code>benchmarks/benchmark.ipynb</code></li>
189
+ </ul>
190
+ </article>
191
+ </section>
192
+ </main>
193
+ </div>
194
+
195
+ <script src="/static/app.js" type="module"></script>
196
+ </body>
197
+ </html>
static/styles.css ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-0: #07131e;
3
+ --bg-1: #0a1c1b;
4
+ --bg-2: #081520;
5
+ --accent: #d24d73;
6
+ --accent-2: #ff7aa2;
7
+ --accent-3: #702845;
8
+ --panel: rgba(15, 20, 37, 0.82);
9
+ --panel-border: rgba(255, 122, 162, 0.24);
10
+ --panel-soft: rgba(255, 255, 255, 0.03);
11
+ --card-light: #f7f2ef;
12
+ --card-text: #182030;
13
+ --text: #f7edf2;
14
+ --text-soft: #f6cfdc;
15
+ --muted: #b48898;
16
+ --success: #6de29c;
17
+ --warn: #ffd166;
18
+ --danger: #ff6b7a;
19
+ }
20
+
21
+ * { box-sizing: border-box; }
22
+
23
+ html, body {
24
+ margin: 0;
25
+ min-height: 100%;
26
+ background:
27
+ radial-gradient(circle at top left, rgba(210, 77, 115, 0.14), transparent 20%),
28
+ radial-gradient(circle at top right, rgba(255, 122, 162, 0.11), transparent 18%),
29
+ linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 46%, #050c15 100%);
30
+ color: var(--text);
31
+ font-family: "Space Grotesk", sans-serif;
32
+ }
33
+
34
+ body {
35
+ background-attachment: fixed;
36
+ }
37
+
38
+ .page-shell {
39
+ max-width: 1320px;
40
+ margin: 0 auto;
41
+ padding: 20px 20px 28px;
42
+ }
43
+
44
+ .hero {
45
+ position: relative;
46
+ overflow: hidden;
47
+ padding: 18px 20px 14px;
48
+ border-radius: 8px;
49
+ background:
50
+ radial-gradient(circle at 15% 50%, rgba(210, 77, 115, 0.15), transparent 22%),
51
+ radial-gradient(circle at 85% 50%, rgba(255, 122, 162, 0.12), transparent 20%),
52
+ linear-gradient(90deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00));
53
+ border: 1px solid rgba(255,255,255,0.06);
54
+ margin-bottom: 16px;
55
+ }
56
+
57
+ .hero__glow {
58
+ position: absolute;
59
+ inset: 0 auto auto 0;
60
+ width: 180px;
61
+ height: 180px;
62
+ background: radial-gradient(circle, rgba(255, 122, 162, 0.18), transparent 72%);
63
+ pointer-events: none;
64
+ }
65
+
66
+ .hero h1,
67
+ .top-tab,
68
+ .battle-title,
69
+ .battle-meta strong,
70
+ .battle-card h3,
71
+ .panel-card h4,
72
+ .sidebar-card h3,
73
+ .control-label,
74
+ .content-card h2,
75
+ .content-card h3,
76
+ .btn {
77
+ font-family: "Press Start 2P", monospace;
78
+ }
79
+
80
+ .hero h1 {
81
+ margin: 0 0 12px;
82
+ font-size: 44px;
83
+ line-height: 1.1;
84
+ color: var(--text);
85
+ }
86
+
87
+ .hero p {
88
+ margin: 0;
89
+ font-size: 16px;
90
+ color: var(--text-soft);
91
+ }
92
+
93
+ .top-tabs {
94
+ display: flex;
95
+ gap: 14px;
96
+ padding: 0 6px;
97
+ margin-bottom: 16px;
98
+ border-bottom: 1px solid rgba(255,255,255,0.10);
99
+ }
100
+
101
+ .top-tab {
102
+ cursor: pointer;
103
+ background: transparent;
104
+ color: rgba(255,255,255,0.9);
105
+ border: none;
106
+ border-bottom: 4px solid transparent;
107
+ padding: 12px 14px 10px;
108
+ font-size: 14px;
109
+ }
110
+
111
+ .top-tab:hover,
112
+ .top-tab.is-active {
113
+ color: var(--accent-2);
114
+ border-bottom-color: var(--accent-2);
115
+ }
116
+
117
+ .tab-panel { display: none; }
118
+ .tab-panel.is-active { display: block; }
119
+
120
+ .replay-layout {
121
+ display: grid;
122
+ grid-template-columns: minmax(0, 1.8fr) minmax(320px, 0.78fr);
123
+ gap: 16px;
124
+ align-items: start;
125
+ }
126
+
127
+ .battle-column,
128
+ .sidebar {
129
+ display: grid;
130
+ gap: 14px;
131
+ }
132
+
133
+ .battle-screen,
134
+ .landing-card,
135
+ .sidebar-card {
136
+ position: relative;
137
+ overflow: visible;
138
+ }
139
+
140
+ .battle-screen::before,
141
+ .landing-card::before,
142
+ .sidebar-card::before {
143
+ content: none;
144
+ }
145
+
146
+ .battle-screen,
147
+ .landing-card {
148
+ padding: 14px;
149
+ }
150
+
151
+ .landing-card__inner {
152
+ position: relative;
153
+ z-index: 1;
154
+ padding: 0;
155
+ }
156
+
157
+ .landing-card h2 {
158
+ margin: 0 0 12px;
159
+ font-size: 30px;
160
+ line-height: 1.25;
161
+ max-width: 13ch;
162
+ }
163
+
164
+ .landing-card p {
165
+ font-family: "Press Start 2P", monospace;
166
+ margin: 0;
167
+ font-size: 15px;
168
+ line-height: 1.8;
169
+ color: var(--text-soft);
170
+ max-width: 28ch;
171
+ }
172
+
173
+ .battle-screen.is-hidden,
174
+ .landing-card.is-hidden {
175
+ display: none;
176
+ }
177
+
178
+ .battle-screen__top {
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: flex-start;
182
+ gap: 14px;
183
+ margin-bottom: 14px;
184
+ }
185
+
186
+ .battle-title {
187
+ font-size: 26px;
188
+ line-height: 1.2;
189
+ }
190
+
191
+ .battle-subtitle {
192
+ margin-top: 6px;
193
+ font-size: 14px;
194
+ color: var(--muted);
195
+ }
196
+
197
+ .battle-meta {
198
+ display: grid;
199
+ gap: 8px;
200
+ text-align: right;
201
+ }
202
+
203
+ .battle-meta div {
204
+ display: grid;
205
+ gap: 6px;
206
+ }
207
+
208
+ .battle-meta span {
209
+ font-size: 11px;
210
+ color: var(--text-soft);
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.12em;
213
+ }
214
+
215
+ .battle-meta strong {
216
+ font-size: 14px;
217
+ color: var(--text);
218
+ }
219
+
220
+ .arena-grid,
221
+ .detail-grid {
222
+ display: grid;
223
+ grid-template-columns: 1fr 1fr;
224
+ gap: 12px;
225
+ margin-bottom: 12px;
226
+ }
227
+
228
+ .battle-card,
229
+ .panel-card {
230
+ position: relative;
231
+ z-index: 1;
232
+ border-radius: 6px;
233
+ background: linear-gradient(180deg, var(--card-light), #efe8e4);
234
+ border: 1px solid rgba(255, 122, 162, 0.16);
235
+ color: var(--card-text);
236
+ }
237
+
238
+ .battle-card {
239
+ padding: 14px;
240
+ }
241
+
242
+ .panel-card {
243
+ padding: 14px 14px 12px;
244
+ }
245
+
246
+ .battle-card__header {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ gap: 12px;
250
+ align-items: flex-start;
251
+ margin-bottom: 8px;
252
+ }
253
+
254
+ .battle-card h3,
255
+ .panel-card h4 {
256
+ margin: 0;
257
+ color: #221830;
258
+ }
259
+
260
+ .battle-card h3 { font-size: 26px; line-height: 1.2; }
261
+ .panel-card h4 { font-size: 15px; margin-bottom: 10px; }
262
+
263
+ .battle-card h3 { font-size: 20px; line-height: 1.2; }
264
+
265
+ .status-pill {
266
+ padding: 7px 10px;
267
+ border-radius: 4px;
268
+ background: linear-gradient(180deg, #2f2336, #1a1322);
269
+ color: var(--text);
270
+ font-size: 10px;
271
+ white-space: nowrap;
272
+ }
273
+
274
+ .transition-line {
275
+ min-height: 18px;
276
+ font-size: 12px;
277
+ color: #70546b;
278
+ margin-bottom: 6px;
279
+ }
280
+
281
+ .sprite-frame {
282
+ height: 164px;
283
+ display: grid;
284
+ place-items: center;
285
+ }
286
+
287
+ .sprite-frame img {
288
+ width: 136px;
289
+ height: 136px;
290
+ object-fit: contain;
291
+ image-rendering: pixelated;
292
+ }
293
+
294
+ .sprite-frame img.player-sprite {
295
+ transform: scaleX(-1);
296
+ }
297
+
298
+ .hp-bar {
299
+ height: 16px;
300
+ border-radius: 4px;
301
+ background: #281d33;
302
+ border: 2px solid rgba(255, 122, 162, 0.18);
303
+ overflow: hidden;
304
+ }
305
+
306
+ .hp-bar span {
307
+ display: block;
308
+ height: 100%;
309
+ width: 100%;
310
+ background: linear-gradient(90deg, var(--accent) 0%, var(--accent-2) 100%);
311
+ }
312
+
313
+ .hp-label {
314
+ margin-top: 8px;
315
+ font-size: 13px;
316
+ color: #47354d;
317
+ }
318
+
319
+ .action-line,
320
+ .reward-line,
321
+ .state-label,
322
+ .summary-lines,
323
+ .commentary-list,
324
+ .chip-row,
325
+ .content-card p,
326
+ .content-card li,
327
+ .content-card code,
328
+ .content-card pre {
329
+ font-size: 15px;
330
+ }
331
+
332
+ .action-line + .action-line {
333
+ margin-top: 8px;
334
+ }
335
+
336
+ .reward-line {
337
+ margin-top: 12px;
338
+ color: #5b2238;
339
+ font-weight: 700;
340
+ }
341
+
342
+ .commentary-list {
343
+ margin: 0;
344
+ padding-left: 18px;
345
+ line-height: 1.5;
346
+ }
347
+
348
+ .state-label {
349
+ color: #6b5167;
350
+ margin-bottom: 8px;
351
+ }
352
+
353
+ .state-label + .chip-row {
354
+ margin-bottom: 10px;
355
+ }
356
+
357
+ .chip-row {
358
+ display: flex;
359
+ flex-wrap: wrap;
360
+ gap: 8px;
361
+ }
362
+
363
+ .chip {
364
+ padding: 6px 8px;
365
+ border-radius: 4px;
366
+ border: 1px solid rgba(210, 77, 115, 0.24);
367
+ background: rgba(255,255,255,0.72);
368
+ color: #281d33;
369
+ }
370
+
371
+ .chip.active {
372
+ background: linear-gradient(180deg, rgba(255, 122, 162, 0.16), rgba(210, 77, 115, 0.12));
373
+ border-color: rgba(210, 77, 115, 0.42);
374
+ font-weight: 700;
375
+ }
376
+
377
+ .chip.hidden {
378
+ color: #7b667b;
379
+ border-style: dashed;
380
+ }
381
+
382
+ .chip.move {
383
+ background: rgba(255, 122, 162, 0.10);
384
+ }
385
+
386
+ .chip.switch {
387
+ background: rgba(210, 77, 115, 0.08);
388
+ }
389
+
390
+ .sidebar-card {
391
+ padding: 0;
392
+ }
393
+
394
+ .sidebar-card h3 {
395
+ margin: 0 0 10px;
396
+ font-size: 15px;
397
+ }
398
+
399
+ .summary-lines {
400
+ display: grid;
401
+ gap: 8px;
402
+ color: var(--text);
403
+ font-family: "Press Start 2P", monospace;
404
+ }
405
+
406
+ .summary-line .label {
407
+ color: var(--text-soft);
408
+ margin-right: 8px;
409
+ font-weight: 700;
410
+ font-family: "Press Start 2P", monospace;
411
+ font-size: 10px;
412
+ }
413
+
414
+ .summary-line .value {
415
+ color: var(--text);
416
+ font-family: "Press Start 2P", monospace;
417
+ font-size: 10px;
418
+ font-weight: 600;
419
+ }
420
+
421
+ .summary-card-inner {
422
+ position: relative;
423
+ z-index: 1;
424
+ border-radius: 6px;
425
+ background: linear-gradient(180deg, var(--card-light), #efe8e4);
426
+ color: var(--card-text);
427
+ border: 1px solid rgba(255, 122, 162, 0.16);
428
+ padding: 14px;
429
+ }
430
+
431
+ .summary-card-inner h3 { color: #221830; }
432
+
433
+ .control-label {
434
+ display: block;
435
+ margin-bottom: 8px;
436
+ font-size: 11px;
437
+ color: var(--text-soft);
438
+ }
439
+
440
+ .speed-row {
441
+ display: grid;
442
+ grid-template-columns: 1fr auto;
443
+ gap: 10px;
444
+ align-items: center;
445
+ }
446
+
447
+ .speed-row input[type="range"] {
448
+ width: 100%;
449
+ accent-color: var(--accent);
450
+ }
451
+
452
+ .speed-value {
453
+ font-family: "Press Start 2P", monospace;
454
+ font-size: 10px;
455
+ color: var(--text);
456
+ }
457
+
458
+ .action-stack,
459
+ .navigator-card {
460
+ display: grid;
461
+ gap: 10px;
462
+ }
463
+
464
+ .navigator-top {
465
+ display: grid;
466
+ grid-template-columns: 52px 1fr 52px;
467
+ gap: 10px;
468
+ align-items: center;
469
+ }
470
+
471
+ .turn-box {
472
+ display: grid;
473
+ gap: 8px;
474
+ }
475
+
476
+ .turn-box input {
477
+ font-family: "Press Start 2P", monospace;
478
+ width: 100%;
479
+ border-radius: 4px;
480
+ border: 1px solid rgba(255, 122, 162, 0.18);
481
+ background: rgba(255,255,255,0.08);
482
+ color: var(--text);
483
+ padding: 12px 10px;
484
+ font-size: 14px;
485
+ }
486
+
487
+ .btn {
488
+ cursor: pointer;
489
+ border: 1px solid rgba(255, 122, 162, 0.28);
490
+ border-radius: 4px;
491
+ padding: 14px 16px;
492
+ font-size: 14px;
493
+ line-height: 1.2;
494
+ color: var(--text);
495
+ background: linear-gradient(180deg, rgba(33, 18, 41, 0.95), rgba(16, 11, 24, 0.95));
496
+ box-shadow: 0 12px 28px rgba(0,0,0,0.22);
497
+ }
498
+
499
+ .btn:hover {
500
+ transform: translateY(-1px);
501
+ box-shadow: 0 18px 34px rgba(0,0,0,0.28);
502
+ }
503
+
504
+ .btn--primary {
505
+ background: linear-gradient(180deg, var(--accent-2), var(--accent));
506
+ }
507
+
508
+ .btn--secondary {
509
+ background: linear-gradient(180deg, rgba(39, 24, 46, 0.98), rgba(20, 13, 26, 0.98));
510
+ }
511
+
512
+ .btn--icon {
513
+ padding: 14px 0;
514
+ }
515
+
516
+ .btn--wide {
517
+ width: 100%;
518
+ }
519
+
520
+ .content-page {
521
+ max-width: 1120px;
522
+ }
523
+
524
+ .content-card {
525
+ padding: 0;
526
+ background: transparent;
527
+ border: none;
528
+ box-shadow: none;
529
+ overflow: visible;
530
+ }
531
+
532
+ .content-card,
533
+ .content-card * {
534
+ color: var(--text) !important;
535
+ }
536
+
537
+ .content-card h2,
538
+ .content-card h3 {
539
+ margin-top: 0;
540
+ color: var(--text);
541
+ font-family: "Press Start 2P", monospace;
542
+ }
543
+
544
+ .content-card h2 { font-size: 22px; }
545
+ .content-card h3 { font-size: 14px; margin-top: 20px; }
546
+
547
+ .content-card p,
548
+ .content-card li,
549
+ .content-card ul,
550
+ .content-card ol {
551
+ font-family: "Press Start 2P", monospace;
552
+ color: var(--text-soft) !important;
553
+ line-height: 1.5;
554
+ }
555
+
556
+ .content-card p,
557
+ .content-card li {
558
+ font-size: 12px;
559
+ }
560
+
561
+ .content-card pre,
562
+ .content-card code {
563
+ font-family: "Press Start 2P", monospace;
564
+ color: var(--text) !important;
565
+ }
566
+
567
+ .content-card code {
568
+ font-size: 12px;
569
+ }
570
+
571
+ .content-card pre {
572
+ background: rgba(255,255,255,0.05);
573
+ border: 1px solid rgba(255, 122, 162, 0.16);
574
+ border-radius: 4px;
575
+ padding: 14px;
576
+ overflow: auto;
577
+ box-shadow: 0 10px 24px rgba(0,0,0,0.18);
578
+ }
579
+
580
+ @media (max-width: 1120px) {
581
+ .replay-layout { grid-template-columns: 1fr; }
582
+ }
583
+
584
+ @media (max-width: 760px) {
585
+ .page-shell { padding: 18px 16px 32px; }
586
+ .hero h1 { font-size: 30px; }
587
+ .landing-card h2 { font-size: 24px; }
588
+ .landing-card p { font-size: 18px; }
589
+ .arena-grid,
590
+ .detail-grid { grid-template-columns: 1fr; }
591
+ .navigator-top { grid-template-columns: 1fr; }
592
+ }