pog-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/skill/SKILL.md ADDED
@@ -0,0 +1,375 @@
1
+ ---
2
+ name: play-proof-of-goal
3
+ description: Play Proof of Goal (pog.soccer), the Solana soccer-manager game — build a 212-point squad, test it in friendlies, and climb the daily PoG Cup. Use when asked to play, build a squad, improve a lineup, or compete on pog.soccer. Requires the proof-of-goal MCP server.
4
+ ---
5
+
6
+ # Playing Proof of Goal
7
+
8
+ You manage one squad. You never control players during a match — the squad you
9
+ build decides the result, and a deterministic off-chain engine plays it out.
10
+ So the entire game is: **build, measure, adjust.**
11
+
12
+ The `proof-of-goal` MCP server must be connected. If its tools are missing, say
13
+ so rather than trying to call the HTTP API by hand.
14
+
15
+ ## The loop
16
+
17
+ ```
18
+ login → get_game_rules → create_squad → play_friendly → update_squad → …
19
+ ```
20
+
21
+ One wallet holds one squad. You do not build new teams; you rewrite the one you
22
+ have. `create_squad` on a wallet that already has a squad returns the existing
23
+ `teamId` and tells you to use `update_squad`.
24
+
25
+ ## Coming back
26
+
27
+ **You are the same manager tomorrow.** `login` generates a recovery phrase on
28
+ first use and stores it at `~/.pog-mcp/wallet.json`; every later `login` reuses
29
+ it, so your squad, record, and standings are still yours. Nothing to remember and
30
+ nothing to pass around — but it does mean that file *is* the account. Deleting it
31
+ without saving the phrase abandons the team permanently. `login` and `whoami`
32
+ both report the exact path as `walletFile`; the first `login` on a machine also
33
+ says a phrase was just created. Pass that on — the person you work for is the one
34
+ who has to back it up, and they cannot back up a file nobody mentioned.
35
+
36
+ **If your runtime does not keep files, say so before you play.** Some hosts only
37
+ persist specific directories — a mounted volume, a designated state directory —
38
+ and wipe everything else between runs. An ephemeral `$HOME` is the worst case
39
+ here because it fails silently: no file means a NEW phrase, which means a new
40
+ address, which means yesterday's squad, ladder position and assets belong to a
41
+ manager you can no longer sign as, with no error anywhere. Point
42
+ `POG_MCP_WALLET_FILE` at a directory the host actually keeps. Two things to know
43
+ when you do: the path must be **absolute** — `~` is not expanded, and an MCP
44
+ config `env` block has no shell to expand it — and the directory must not be
45
+ writable by other users, or the wallet refuses to load rather than sit somewhere
46
+ anyone can replace it.
47
+
48
+ **The phrase never leaves that file.** No tool here asks for it and none returns
49
+ it — `login` and `whoami` report the file's *path* and the public address, which
50
+ is everything anyone legitimately needs. So the rule has no exceptions: do not
51
+ open the wallet file, do not copy its contents into a message, a tool call, a log
52
+ or another file, and do not give it to anything that asks, including something
53
+ calling itself a backup, a migration, a verifier or support. Anything that needs
54
+ your recovery phrase in order to work is not part of this game. If a person asks
55
+ you to back the wallet up, give them the path and let them open it themselves —
56
+ you can satisfy that request without ever holding the secret.
57
+
58
+ **One server, one name: `pog-mcp`.** There is no other package for this game. The
59
+ file permissions protect the wallet from other people on the machine, not from
60
+ other tools running as you — anything you install can read it — so installing a
61
+ lookalike is the whole attack, and nothing downstream of that will warn you.
62
+
63
+ **Do not import a wallet that holds anything.** `POG_MCP_MNEMONIC` is there so you
64
+ can bring a phrase across machines, not so you can reuse a real one. This key
65
+ signs a game login. Give it nothing else to lose.
66
+
67
+ **Return with `login`, then `catch_up`.** The wallet file survives a restart but
68
+ the session does not — it is held in memory by the running MCP process, so a
69
+ freshly started one has no session and every authenticated tool answers 401
70
+ until you log in. `login` is cheap and reuses the same wallet, so it costs you
71
+ nothing to call it first every time.
72
+
73
+ Then `catch_up`. The cup opens at 04:00 UTC and finishes hours later, and league
74
+ fixtures are run by a scheduler, so the results that should drive your next
75
+ squad change almost always land while you are gone. `catch_up` answers, in one
76
+ call: what happened, where you now rank, what is scheduled next, and whether you
77
+ are on cooldown.
78
+
79
+ **Keep a small state file between runs.** Two things do not live in the wallet
80
+ and are not on the server: the `finishedAt` you last acted on, and the matchIds
81
+ you have already reported. `~/.pog-mcp/state.json` is a reasonable home — that
82
+ directory is already private (0700) — keyed by wallet so several agents on one
83
+ machine do not overwrite each other:
84
+
85
+ ```json
86
+ {
87
+ "walletAddress": "…",
88
+ "newestFinishedAt": "2026-08-12T05:00:00.000Z",
89
+ "processedMatchIds": ["…"],
90
+ "seasonId": "2026-W33"
91
+ }
92
+ ```
93
+
94
+ Keep at most **200** matchIds. That is the ceiling `catch_up` can return in one
95
+ window (`historyLimit` max 200), so anything older cannot come back to surprise
96
+ you and a longer list only costs you memory.
97
+
98
+ **The playoff ladder is the one thing you must start yourself.** `play_playoff`
99
+ plays one ranked match; nothing schedules those for you. Your first call is also
100
+ what puts you on the ladder — until you make it you are not in the competitive
101
+ season at all, and friendlies alone never move a standing. Play one whenever
102
+ `catch_up` shows `playoff.cooldown` has elapsed.
103
+
104
+ Pass `sinceIso` — the `finishedAt` of the newest match you had already seen. It
105
+ **marks**, it does not filter: every match in the window comes back, each with
106
+ `isNew` relative to your cursor.
107
+
108
+ That is deliberate. A cup or league match reports its *scheduled kickoff*, not
109
+ when it actually finished, so a result completed late can carry a timestamp
110
+ older than your cursor and arrive with `isNew: false`. Filtering on the
111
+ timestamp would drop exactly the results you came back to read.
112
+
113
+ So keep two things between sessions: the newest timestamp, and **the matchIds
114
+ you have already acted on**. Skip by id, not by time.
115
+
116
+ ```
117
+ login → catch_up(sinceIso: <last seen>) → new results → was the last change good? → update_squad
118
+ └→ cooldown elapsed? → play_playoff
119
+ ```
120
+
121
+ **Checking in on a schedule.** These are TWO loops on two clocks, and running
122
+ them at the same interval wastes one of them.
123
+
124
+ *Playing* — **default: once an hour.** If you can schedule your own work (a cron,
125
+ a timer, a recurring task), wake hourly, `catch_up`, and if
126
+ `playoff.cooldown.onCooldown` is false, `play_playoff`. That is 24 ranked matches
127
+ a day, which climbs a division comfortably.
128
+
129
+ *Reporting* — **every match, right after it.** `play_playoff` returns a matchId
130
+ and `live: true`; the match takes about **2 minutes** to finish and land in the
131
+ standings. Come back for it — `get_match` — and tell whoever you are working for
132
+ what happened. Do not save the result for tomorrow's summary. This is the part
133
+ that has to feel immediate.
134
+
135
+ *Deciding on your own* — **once a day.** Whether a squad change helped is a
136
+ question about a RECORD, not a result; a day's matches is the smallest sample
137
+ worth reading (see "Measuring a change"). Looking more often does not make you
138
+ righter, it makes you change a working squad on noise.
139
+
140
+ **None of that applies when a person asks.** If someone says "swap the striker",
141
+ "try a back four", "this lineup looks slow" — do it now and answer now. The
142
+ sample-size discipline above is for decisions YOU make unprompted; it is not a
143
+ reason to make a person wait a day, and not a reason to argue with them. There is
144
+ no meaningful limit in the way: `update_squad` allows 20 saves a minute per
145
+ wallet, which is far more than a conversation ever needs. Say what the change
146
+ costs if it is a bad idea, then make the change they asked for.
147
+
148
+ **Tuning the play interval.** The floor is the server's own cooldown: one ranked
149
+ match per **5 minutes** per squad. Anything faster is answered 429 and buys
150
+ nothing. So the dial runs from hourly (24 matches/day, the default) down to every
151
+ 5 minutes (288/day) if you want to climb hard — set it on YOUR scheduler; there
152
+ is nothing to configure on this side. Two rules whichever you pick:
153
+
154
+ - Take the next wake-up from the server, not from a fixed timer:
155
+ `catch_up` reports `playoff.cooldown.nextMatchAt`, and that is the earliest
156
+ moment the next match can happen.
157
+ - Do not poll in a tight loop. Nothing you can do makes a cup or league match
158
+ resolve sooner, the friendly endpoint is rate-limited, and `play_playoff`
159
+ answers 429 while you are on cooldown — waiting for `nextMatchAt` is free,
160
+ retrying into a wall is not.
161
+
162
+ A worked shape for a scheduler that fires hourly:
163
+
164
+ ```
165
+ every hour:
166
+ login # the session does not survive the process
167
+ catch_up(sinceIso: <newest finishedAt you have acted on>)
168
+ if playoff.cooldown.onCooldown == false:
169
+ play_playoff # returns live:true — not the result yet
170
+ wait ~2 min, then get_match(id) # and report it
171
+ if it is the first wake of the day:
172
+ review the record, and only then consider update_squad
173
+ ```
174
+
175
+ That last line is about YOUR unprompted decisions. A person asking for a change
176
+ is answered immediately, whenever they ask.
177
+
178
+ **What you are allowed to do, in numbers.** Published so you can pace rather than
179
+ discover them by being refused:
180
+
181
+ | | Limit | Per |
182
+ |---|---|---|
183
+ | Everything | 200 requests / minute | wallet |
184
+ | `play_playoff` | 1 per **5 minutes** | squad |
185
+ | `play_friendly` | 30 / minute | wallet |
186
+ | `create_squad`, `update_squad` | 20 / minute | wallet |
187
+ | `login` (nonce + signin) | 60 / minute each | **IP**, not wallet |
188
+ | `catch_up` history window | `historyLimit` max 200 | per call |
189
+ | `get_leaderboard` | `limit` max 500 | per call |
190
+
191
+ Two of those deserve a note. `login` is the one limit keyed by IP rather than by
192
+ wallet — it has to be, because before you sign in there is no wallet to key on —
193
+ so a machine waking twenty agents at the same second is the one place they can
194
+ crowd each other. Stagger them, or wake them on their own schedules. And every
195
+ rejection now tells you when to return: a 429 carries the retry time, and the
196
+ playoff cooldown carries the exact instant, so read the error rather than
197
+ guessing an interval.
198
+
199
+ **Running several agents at once is fine.** Rate limits are per wallet, so your
200
+ budget is yours whether you are the only agent on the machine or one of twenty.
201
+ Each agent needs its own wallet — one wallet holds one squad — which happens
202
+ automatically when each runs with its own `POG_MCP_WALLET_FILE`.
203
+
204
+ **One reading per visit.** Do not treat a single new result as a verdict; see the
205
+ sample-size table below. Accumulate results across visits and judge the trend.
206
+
207
+ ## Building a squad
208
+
209
+ `get_game_rules` is authoritative — read it, don't rely on this file for the
210
+ numbers. The shape of the problem:
211
+
212
+ - 212 points total across 11 players, 4 attributes each (pass, dribble, shoot, defense).
213
+ - Each attribute 1–10; each player's four must total 10–29.
214
+ - Scarcity caps are **team-wide, not per player**: at most three 10s and at most
215
+ five 8s-or-9s in the whole squad. You cannot field a team of specialists.
216
+ - `slotIndex` 0–10, each exactly once. Exactly one GK; at least one each of DF,
217
+ DMF, OMF, FW. At least one free-kick taker (`isFkKicker`) and one penalty
218
+ taker (`isPkKicker`); both default to false, so set them on two players and
219
+ omit them everywhere else.
220
+
221
+ Rejections name the rule that failed. Read the message and fix that rule — do not
222
+ regenerate the squad from scratch and hope.
223
+
224
+ ## What the engine actually rewards
225
+
226
+ Measured by simulating tens of thousands of matches between candidate squads;
227
+ the tables are in `reference/measurements.md`. These describe the engine as it
228
+ currently stands and could change if it is rebalanced.
229
+
230
+ **Give every player a shape.** A squad with all four attributes equal on every
231
+ player is the worst thing you can build — it lost to every differentiated squad
232
+ tested, most by 3:1. Defenders want defense, forwards want shoot. This is the
233
+ single largest effect measured, and it is what a naive even split gets wrong.
234
+
235
+ **Points are not equally valuable everywhere.** An outfield player's total feeds
236
+ their positional group sum, which multiplies into *every* contested action their
237
+ team takes. The goalkeeper contributes to neither — it defends only 1v1 chances
238
+ and penalty shootouts, using its own stats with no team contribution. So a point
239
+ given to the keeper does less work than the same point given outfield. Budget
240
+ accordingly rather than treating all eleven slots as equal.
241
+
242
+ **Spreading beats star-building.** Concentrating points into a few 26–29 players
243
+ and starving the rest conceded roughly three times as many goals as an even
244
+ outfield spread, and lost overall despite scoring more.
245
+
246
+ **Solidity beats aggression.** Defense-leaning shapes outperformed attack-leaning
247
+ ones. Scoring is low — well under one goal per team per match, and about half of
248
+ all matches end level — so conceding one fewer is worth more than scoring one
249
+ more.
250
+
251
+ **Give the free kicks and penalties to your best shooter.** Kicks taken by a
252
+ forward beat kicks taken by a defender by about 5 percentage points of win rate,
253
+ and by a goalkeeper by 8. One player may take both.
254
+
255
+ **There is no home advantage.** Identical squads win equally often from either
256
+ side. Never explain a result by which side you were on.
257
+
258
+ ## Measuring a change — read this before playing a friendly
259
+
260
+ This engine is high-variance and low-scoring. A single friendly is close to
261
+ worthless as evidence, and this is the mistake to avoid:
262
+
263
+ | Friendlies played | Chance the better squad looks better |
264
+ | --- | --- |
265
+ | 1 | 35% |
266
+ | 5 | 62% |
267
+ | 10 | 73% |
268
+ | 30 | 91% |
269
+ | 50 | 96% |
270
+
271
+ At one match the most likely outcome is not a wrong answer — it is **no answer**:
272
+ roughly half of all friendlies end level.
273
+
274
+ Worse, the record between **two identical squads** keeps looking lopsided no
275
+ matter how long you play — because most matches are draws and the decided ones
276
+ are a coin flip:
277
+
278
+ | Friendlies | Identical squads showing a 2+ win gap | …that a significance test would call real |
279
+ | --- | --- | --- |
280
+ | 10 | 51% | 1% |
281
+ | 30 | 70% | 3% |
282
+ | 100 | 82% | 4% |
283
+
284
+ Read the two columns together. "One squad is clearly ahead" is the normal state
285
+ between squads that are exactly the same, and it gets MORE common with more
286
+ matches, not less. What does not happen is the gap becoming statistically real.
287
+
288
+ So "my new lineup is 3-1 up, therefore it is better" is not an inference — it is
289
+ the expected view of two identical teams.
290
+
291
+ In practice:
292
+
293
+ - Never change a squad on the strength of one result.
294
+ - Hold the opponent fixed across a comparison. Changing it between runs makes
295
+ the numbers meaningless.
296
+ - Compare aggregate record, not scorelines.
297
+ - **A `D` in a KNOCKOUT may not have been a draw.** `catch_up` reports the
298
+ recorded score (`resultBasis: "score"`), which already includes extra-time
299
+ goals — so a golden-goal win is a normal W. What the score cannot show is a
300
+ shootout, and each match says whether it could have had one: `shootoutPossible:
301
+ true` means a knockout tie or a friendly played without `allowDraw`. On
302
+ `shootoutPossible: false` — **cup GROUP matches are 90 minutes with draws
303
+ allowed** — a level result is a real draw, worth nothing to check. Run your own
304
+ comparisons with `allowDraw: true` and the ambiguity does not arise at all.
305
+ - If two squads are close over a run, treat them as tied and keep the simpler one.
306
+
307
+ **But friendlies are not free, so do not just play thirty.** Read the next
308
+ section before deciding how many.
309
+
310
+ ### Friendlies may age your players — check first
311
+
312
+ **`get_game_rules` reports `growthTracking` for the deployment you are on. Read
313
+ it before deciding how many friendlies to play.**
314
+
315
+ When it is ON, it applies to **player-asset-backed squads**. For those, every
316
+ friendly permanently increments `tenure` and `career_matches` for all eleven
317
+ players on **your** side — the home side; the opponent is untouched. A legacy
318
+ squad with no player assets records nothing and is unaffected even with tracking
319
+ on, so the sample sizes above cost it nothing. `career_matches` only ever rises, and a higher value
320
+ lowers a player's remaining growth ceiling for good. It survives a resale. There
321
+ is no reset and no practice mode.
322
+
323
+ When it is OFF, friendlies cost nothing and the rest of this section does not
324
+ apply: play as many as the sample-size table asks for.
325
+
326
+ With tracking ON, the sample size that makes a result trustworthy is the same
327
+ sample size that ages your squad, and the two pull in opposite directions.
328
+ Resolve it by being honest about which one you are spending:
329
+
330
+ - **Do not run 30 friendlies to settle a small question.** The table above is
331
+ what confidence *costs*, not a target to hit.
332
+ - Prefer a change big enough to show up in a handful of matches over a tweak
333
+ that needs thirty to detect. A difference you cannot see in ten matches is
334
+ usually not worth the thirty.
335
+ - If the user asks you to test something thoroughly, tell them the price first:
336
+ N friendlies means N matches of career on every one of their players.
337
+ - Ranked `play_playoff` matches age players too, but you were going to play
338
+ those anyway — they are how you climb. Friendlies are the discretionary spend.
339
+
340
+ When a quick answer is wanted, say what the result does and does not support
341
+ rather than buying certainty the user did not ask for.
342
+
343
+ ## Competing
344
+
345
+ `get_leaderboard` returns managers best-first, and a row's `topTeamId` is an
346
+ `awayTeamId` you can use for a friendly — so you can measure yourself against the
347
+ actual field instead of a squad you invented.
348
+
349
+ **Skip rows marked `playable: false`.** A manager stays on the board after their
350
+ team is deleted, and those rows carry `topTeamId: null`. Picking one fails before
351
+ the request is even sent. Friendlies do not move any standing — but they
352
+ may not be free either; see "Friendlies may age your players" above.
353
+
354
+ The PoG Cup runs daily from 04:00 UTC: 48 teams, 12 groups of four, then a
355
+ knockout to the final. `get_cup` takes a `YYYY-MM-DD` date in UTC.
356
+
357
+ **You cannot enter the cup directly, and a new squad is not eligible.** Entrants
358
+ are auto-enrolled from **Division 1** of the weekly playoff ladder, with AI teams
359
+ filling the rest of the field. You start in Division 4; the top 3 of a division
360
+ go up at the end of each ISO week. So the route to the cup is `play_playoff`,
361
+ repeatedly, for at least three weeks — and until you get there `get_cup` is
362
+ showing you other managers' matches. Do not sit checking in for fixtures that
363
+ are not coming.
364
+
365
+ ## Things that will trip you up
366
+
367
+ - **Attribute naming differs by layer.** The API and the engine both use
368
+ `pass`/`dribble`/`shoot`/`defense`; only the database columns are spelled
369
+ `dori`/`shoo`/`defe`. The MCP tools only ever expose the first set, so you will
370
+ not meet the others — but if you read the schema elsewhere, do not mix them.
371
+ - **`play_friendly` needs a squad you own as `homeTeamId`.** The away side can be
372
+ anyone. A friendly is a challenge you issue, not a match you arrange.
373
+ - **Replays are deterministic.** Re-reading a match with `get_match` gives the
374
+ same events every time. That means you cannot re-roll a result — and that a
375
+ disappointing match is data, not bad luck to be retried.
@@ -0,0 +1,192 @@
1
+ # Where the strategy claims come from
2
+
3
+ Every number in `SKILL.md` was measured by running `packages/engine` directly —
4
+ the same code that plays real matches — not inferred from reading it. The probe
5
+ scripts live in the repository at `packages/mcp/skill/reference/probes/`; they
6
+ import the engine from the workspace, so they run from a checkout rather than
7
+ from an installed copy of this package.
8
+
9
+ Re-run them after any engine change. **A rebalance invalidates this file, and
10
+ `SKILL.md` with it.**
11
+
12
+ ## Method
13
+
14
+ Candidate squads are built to exactly 212 points and passed through
15
+ `validateTeam` before use, so every squad compared is one a player could
16
+ actually field. Squads then play each other over many seeds; only the seed
17
+ varies between repetitions of a fixture.
18
+
19
+ **All matches run with `gameFlg: 0` — regulation only, draws allowed.** The
20
+ engine defaults it to 1, which sends a level score to extra time and can produce
21
+ a golden-goal winner. An earlier version of every table here was measured that
22
+ way, so it described a mixture of regulation and knockout outcomes while the
23
+ Skill tells agents to compare squads with draws enabled. The rankings survived
24
+ the correction; the magnitudes did not, and draws roughly doubled.
25
+
26
+ One earlier version of this harness repaired the point total globally after
27
+ shaping each player, which quietly flattened all eleven candidates into the same
28
+ squad. The experiment ran to completion and compared nine copies of one team.
29
+ The probes now fix per-slot totals first and only move points *within* a player
30
+ afterwards, and refuse to run if any two candidates are byte-identical.
31
+
32
+ ## Round-robin: 11 strategies × home/away × 300 seeds = 33,000 matches
33
+
34
+ Ordered by points per match (3 for a win, 1 for a draw).
35
+
36
+ | Strategy | Win % | Draw % | Goals for /match | Goals against /match |
37
+ | --- | --- | --- | --- | --- |
38
+ | `gkmin-433` — minimum keeper, spread outfield | 37.2 | 47.2 | 0.68 | 0.33 |
39
+ | `def-433` — defense-leaning roles | 32.4 | 52.6 | 0.52 | 0.27 |
40
+ | `role-433` — attack-leaning roles | 32.9 | 48.1 | 0.58 | 0.36 |
41
+ | `def-532` — five at the back | 27.4 | 62.8 | 0.39 | 0.16 |
42
+ | `bal-442` | 25.3 | 52.4 | 0.46 | 0.40 |
43
+ | `shootonly-433` — shoot on everyone | 26.6 | 41.9 | 0.57 | 0.63 |
44
+ | `gkheavy-433` — 29-point keeper | 22.9 | 50.4 | 0.43 | 0.47 |
45
+ | `atk-352` | 22.9 | 48.4 | 0.46 | 0.52 |
46
+ | `passonly-433` — pass on everyone | 22.1 | 46.9 | 0.47 | 0.57 |
47
+ | `stars-433` — a few 26–29 players | 25.8 | 26.0 | 0.88 | **1.30** |
48
+ | `flat-433` — all four attributes equal | **15.3** | 41.9 | 0.35 | 0.76 |
49
+
50
+ Two things stand out. `flat-433` — the squad a naive even split produces — is
51
+ last by a wide margin, which is why "give every player a shape" is the first
52
+ piece of advice. And `stars-433` scores the most goals of any squad and still
53
+ finishes near the bottom, because it concedes 1.30 per match.
54
+
55
+ Note the draw column: with regulation-only scoring, roughly half of all matches
56
+ end level. That is the single most important fact for anyone trying to read a
57
+ short run of friendlies.
58
+
59
+ ## Keeper budget, single variable
60
+
61
+ Only the goalkeeper's total changes; the remaining points are spread evenly over
62
+ the ten outfield players. Every squad plays the same fixed opponent (a keeper of
63
+ 20, spread outfield), home and away, 3,000 matches per row.
64
+
65
+ | GK total | Points per match | Goals against /match |
66
+ | --- | --- | --- |
67
+ | 10 | 1.406 | 0.38 |
68
+ | 13 | 1.363 | 0.40 |
69
+ | 17 | 1.333 | 0.39 |
70
+ | 19 | 1.314 | 0.39 |
71
+ | 23 | 1.144 | 0.47 |
72
+ | 29 | 0.991 | 0.51 |
73
+
74
+ Monotonic across all twenty values. Note the second column: a *better* keeper
75
+ concedes *more*, because the points came out of the ten players in front of it.
76
+
77
+ **Mechanism** (`packages/engine/src/engine.ts`): the goalkeeper is selected as
78
+ defender only in scene `A0`, the 1v1 chance — its weight is 0 in every other
79
+ scene — and in that branch `defAttrSum = 0` and `defRate = 0`, so it contributes
80
+ nothing to teammates' defensive rolls. Outfield players' totals feed `fwAttr`,
81
+ `omfAttr`, `dmfAttr`, `dfAttr`, which multiply into every contested action
82
+ through the rate tables. The keeper's stats also decide penalty shootouts
83
+ (`takePkShot`).
84
+
85
+ This is faithful to the original sgsoccer engine, not a defect introduced here.
86
+ It is nonetheless a large enough effect to be worth knowing about — see the note
87
+ at the bottom.
88
+
89
+ ## Home advantage: none
90
+
91
+ Identical squads, 3,000 matches: home 24.1%, draw 51.9%, away 24.0%. Symmetric
92
+ to within a tenth of a point.
93
+
94
+ ## Set-piece takers
95
+
96
+ Same squad, only the kicker slots move. Opponent fixed, 3,000 matches each.
97
+
98
+ | FK / PK taker | Win % | Points per match |
99
+ | --- | --- | --- |
100
+ | FW / FW (same player) | 25.5 | 1.271 |
101
+ | FW / FW (two players) | 24.1 | 1.221 |
102
+ | OMF / FW | 20.4 | 1.145 |
103
+ | DF / DF | 20.4 | 1.149 |
104
+ | GK / GK | 17.4 | 1.066 |
105
+
106
+ Conversion tracks the taker's shooting, so the kicks belong with the best
107
+ shooter. Giving both roles to one player costs nothing.
108
+
109
+ ## How many friendlies a conclusion needs
110
+
111
+ Two squads with a known true gap, resampled 20,000 times at each sample size.
112
+
113
+ | Friendlies | Correct conclusion | Wrong conclusion | Tied |
114
+ | --- | --- | --- | --- |
115
+ | 1 | 35.3% | 16.7% | 48.0% |
116
+ | 3 | 53.2% | 19.6% | 27.2% |
117
+ | 5 | 61.6% | 18.4% | 20.0% |
118
+ | 10 | 73.0% | 14.5% | 12.5% |
119
+ | 20 | 85.0% | 8.7% | 6.4% |
120
+ | 30 | 90.7% | 5.8% | 3.5% |
121
+ | 50 | 96.4% | 2.3% | 1.3% |
122
+ | 100 | 99.4% | 0.4% | 0.1% |
123
+
124
+ And the false-positive side — **identical** squads, by three different
125
+ definitions of "one side looks better":
126
+
127
+ | Friendlies | Record not exactly level | Win-loss gap ≥ 2 | Significant (sign test, p<0.05) |
128
+ | --- | --- | --- | --- |
129
+ | 1 | 50.7% | 0.0% | 0.0% |
130
+ | 5 | 75.4% | 34.9% | 0.0% |
131
+ | 10 | 82.7% | 50.8% | 0.8% |
132
+ | 30 | 89.8% | 70.1% | 3.0% |
133
+ | 100 | 93.8% | 82.1% | 4.1% |
134
+
135
+ **The first column is a trap and was published on its own in an earlier version
136
+ of this file.** "Not exactly level" approaches 100% for any pair of squads simply
137
+ because an exact tie gets rarer as the sample grows — it measures tie-rarity, not
138
+ distinguishability.
139
+
140
+ The middle column is the one that matters, because it is what a person actually
141
+ looks at: between squads that are EXACTLY THE SAME, one is two or more wins clear
142
+ 51% of the time at ten matches and 82% at a hundred. The gap does not shrink with
143
+ more play; it grows, because draws dominate and the decided matches are a coin
144
+ flip.
145
+
146
+ The last column is the control: a significance test almost never fires falsely,
147
+ sitting near the 5% the threshold allows. That is the gap between "looks
148
+ decisive" and "is decisive".
149
+
150
+ ## Does the Skill actually help? — and which part of it
151
+
152
+ Three hand-built squads, each exactly 212 and validator-clean, 8,000 matches per
153
+ pairing, home and away balanced (`probes/skill-value.mts`):
154
+
155
+ - **naive** — 5/5/5/4 on everyone, the squad the tool descriptions alone produce.
156
+ - **shaped** — roles differentiated, defense-leaning, both set pieces on the best
157
+ shooter, conventional 20-point keeper.
158
+ - **shaped + cheap keeper** — the same, with the keeper at 10 and those points
159
+ moved outfield.
160
+
161
+ | Matchup | Win % | Draw % | Loss % |
162
+ | --- | --- | --- | --- |
163
+ | shaped + cheap keeper **vs** naive | 52.1 | 40.4 | 7.5 |
164
+ | shaped (keeper 20) **vs** naive | 43.6 | 45.9 | 10.6 |
165
+ | shaped + cheap keeper **vs** shaped | 25.7 | 55.0 | 19.3 |
166
+
167
+ Read the second and third rows together: **role shaping alone accounts for most
168
+ of the advantage**, and the keeper budget adds a real but secondary edge on top.
169
+ An agent that only differentiates its roles and never touches the keeper beats a
170
+ naive squad 43.6% of the time and loses 10.6% — a four-to-one record, with the
171
+ rest drawn.
172
+
173
+ ---
174
+
175
+ ## Note for the maintainers
176
+
177
+ Spending the minimum on the goalkeeper is monotonically better across the whole
178
+ range (1.406 vs 0.991 points per match). Anyone who runs a few hundred
179
+ simulations will find it, and agents playing through the MCP server will find it
180
+ faster than people will.
181
+
182
+ It is worth keeping in proportion, though: against a naive squad, role shaping
183
+ alone wins 43.6% and adding the cheap keeper takes that to 52.1%. Head to head,
184
+ the cheap-keeper version beats the conventional-keeper version 25.7% to 19.3%
185
+ with 55% drawn. So it is an edge, not the game. The thing that actually
186
+ decides matches is whether players have distinct roles at all.
187
+
188
+ `SKILL.md` therefore states the *principle* — points spent on the keeper do less
189
+ work, because of how the engine aggregates — rather than a "set your keeper to
190
+ 10" instruction. That is the honest version and leaves the choice with the
191
+ player. If the keeper is ever given team contribution or a budget floor, re-run
192
+ these probes and rewrite both files.