Generate a cluster of Resonance cards following the standard set distribution, validate through the CCGNF toolchain, iterate with the user, and commit approved cards to encoding/cards/. Invoke when the user asks to "generate cards", "make a card cluster", "design new cards", or otherwise requests card creation for Resonance.
Creates a self-consistent cluster of cards for Resonance (the reference CCG in this repo), validated through the CCGNF pipeline, presented to the user for approval, and finally committed to the long-term encoding under encoding/cards/.
python3 random.choices() calls, not "what feels right." The seed is recorded in the working file header and a re-roll uses seed + 1.design/Supplement.md, followed by the CCGNF Card NAME { ... } declaration. These MUST stay in sync after every edit.| Parameter | Default | Notes |
|---|---|---|
N (size) | 10 | Total cards in the cluster |
| Faction filter | none (all factions fair game) | e.g., "5 EMBER cards" |
| Rarity filter | none (use standard distribution) | e.g., "only commons" |
| Theme | none | Narrative shape: "removal", "go-wide", "attrition" |
| Seed | date +%s at invocation | Use seed + 1 on re-roll |
Record all inputs in the working file header.
make build CONFIG=Release
Fall back to dotnet build Ccgnf.sln -c Release if make is unavailable (common on Windows/bash where make may not be installed). The dotnet fallback is a fully supported path — don't ask the user to install make. If the build itself fails, report the error and stop — do not proceed with a broken toolchain.
Load encoding/cards/DISTRIBUTION.md. Note the current counts per faction × rarity and the deficit vs. the §4 target distribution from design/Supplement.md:
Don't pick weights by eye. Run the helper:
python3 tools/cluster-rarity-weights.py
It reads the current Totals table from DISTRIBUTION.md and prints random.choices-ready weights using weight = target × clamp(target/current, 0.5, 2.0) — over-represented rarities get downweighted, under-represented ones get upweighted, with the clamp preventing extreme swings on small sets. Use the printed weights verbatim in step 3's rarity roll (unless the user gave a rarity filter).
If a user-specified theme implies a non-standard distribution (e.g., "give me a mythic cycle"), prefer the user's intent over the helper's tilt.
All four of these use python3 -c with random.seed(int(seed)). Invoke each as a separate shell call so the seed stream is deterministic.
Rarity split (unless user overrode) — use the deficit-tilted weights from step 2, not the raw target ratios:
python3 -c "
import random, sys
random.seed(int(sys.argv[1]))
n = int(sys.argv[2])
# Weights from: python3 tools/cluster-rarity-weights.py
print(','.join(random.choices(
['C', 'U', 'R', 'M'],
weights=[<C>, <U>, <R>, <M>],
k=n)))
" <seed> <N>
Faction per slot (adjust weights against current over-representation):
python3 -c "
import random, sys
random.seed(int(sys.argv[1]) + 100)
n = int(sys.argv[2])
# Base weights: mono-factions ~18% each, dual 5%, neutral 15%.
print(','.join(random.choices(
['EMBER', 'BULWARK', 'TIDE', 'THORN', 'HOLLOW', 'DUAL', 'NEUTRAL'],
weights=[18, 18, 18, 18, 18, 5, 15],
k=n)))
" <seed> <N>
Card type per slot, per-faction-tuned weights:
| Faction | Unit | Maneuver | Standard |
|---|---|---|---|
| EMBER | 60% | 35% | 5% |
| BULWARK | 50% | 30% | 20% |
| TIDE | 45% | 45% | 10% |
| THORN | 55% | 25% | 20% (Kindle Standards) |
| HOLLOW | 50% | 40% | 10% |
| NEUTRAL | 60% | 35% | 5% |
| DUAL | 80% | 15% | 5% |
Keyword selection per faction: PRNG-pick from the signature keyword set in design/Supplement.md §2.*. Avoid re-using the same keyword/mechanic for multiple cards in one cluster unless the user asks for a theme.
Per-faction signature pools:
| Faction | Signature keywords |
|---|---|
| EMBER | Surge, Blitz, Ignite |
| BULWARK | Fortify, Mend, Sentinel |
| TIDE | Drift, Recur, Reshape |
| THORN | Rally, Sprawl, Kindle |
| HOLLOW | Phantom, Shroud, Pilfer |
| NEUTRAL | (none — Neutral cards are glue, no signature keyword) |
DUAL slot handling: a DUAL slot needs two PRNG decisions, in this order:
design/Supplement.md §6.3:
EMBER/THORN, BULWARK/TIDE, HOLLOW/TIDE, THORN/BULWARK, EMBER/HOLLOW.Sketch:
random.seed(seed + 300)
for slot in slots:
if slot.faction == 'DUAL':
pair = random.choice(DUAL_PAIRS) # ('EMBER','THORN'), etc.
pool = pools[pair[0]] + pools[pair[1]]
slot.keyword = random.choice(pool + [None])
elif slot.faction == 'NEUTRAL':
slot.keyword = None # glue, no signature
else:
slot.keyword = random.choice(pools[slot.faction])
Keyword/type compatibility: a few keywords are type-locked. If the PRNG roll lands on an incompatible pair, treat the keyword as a hint and substitute a faction-flavored effect — do not force the keyword in.
| Keyword | Only legal on | If rolled on something else |
|---|---|---|
| Kindle | Standard | Substitute a Sprawl-/Rally-flavored effect (THORN). |
| Interrupt | Maneuver | Substitute a non-Interrupt Maneuver effect. |
(See design/Supplement.md §2.4 and §2.6 for the canonical lists.)
Offsetting seed between calls (seed, seed+100, seed+200, ...) keeps the streams independent.
For each slot, assemble:
design/Supplement.md §5 power-budget. Cost typically [1..6] for C/U, [3..7] for R/M.design/GameRules.md §11 and encoding/engine/03-keyword-macros.ccgnf.encoding/cards/*.ccgnf for the proposed name. If taken, re-roll the name (PRNG seed + offset).Sanity check the rarity × type combo before authoring. Some combinations are legal-but-unusual; if the PRNG lands on one, pause to confirm the design space is real:
Path: encoding-artifacts/working-<UTC-timestamp>.ccgnf where timestamp is date -u +%Y%m%dT%H%M%S.
The block comment is the spec, not a scratchpad. Write the final form on the first pass — no draft notes, no parentheticals like "actually X" or "TODO: rename", no in-comment self-corrections. The block comment is what's read by humans first and copied into design/Supplement.md-style references later. Sloppy comments cause sync-check failures in step 7 that you then have to chase.
Layout:
// =============================================================================
// Cluster working file
// Generated: <iso8601-utc>
// Seed: <seed>
// N: <N>
// Factions: <allocated distribution>
// Rarities: <allocated distribution>
// =============================================================================
/*
* Cinderling [C] (EMBER, Unit) — cost 1 — 2/1
* Blitz.
*
* A 1-drop that pressures the Conduit immediately.
*/
Card Cinderling {
factions: {EMBER}, type: Unit, cost: 1, force: 2, ramparts: 1, rarity: C
keywords: [ Blitz, DeploymentSickness ]
abilities: []
// text: Blitz.
}
/*
* <next card description> ...
*/
Card NextOne { ... }
dotnet run --project src/Ccgnf.Cli --no-build -c Release -- \
--log-level error encoding-artifacts/working-<ts>.ccgnf
Iterate on errors up to 10 times:
If still broken after 10 iterations, stop and surface the errors to the user with context. Do not present a broken cluster.
(When the validator lands, extend this step to run it too and iterate on its diagnostics as well.)
Walk each card and verify the block comment and CCGNF agree:
cost N — F/R in the comment matches cost: N, force: F, ramparts: R.[C/U/R/M] matches rarity: C|U|R|M.(EMBER, Unit) matches factions: {EMBER}, type: Unit.Any mismatch → fix the appropriate side before presenting.
Render the cards in chat in design/Supplement.md style, grouped by faction then rarity. Include a summary header:
Cluster of 10 cards (seed 1714572600):
- 5 EMBER, 2 BULWARK, 2 TIDE, 1 NEUTRAL
- 4 C, 4 U, 1 R, 1 M
Working file: encoding-artifacts/working-20260417T210000.ccgnf
Then each card as:
**Cinderling** [C] *(EMBER, Unit) — 1 — 2/1*
Blitz.
*A 1-drop that pressures the Conduit immediately.*
When the user asks for changes:
Never present a cluster that failed validation or is out of sync.
When the user says "looks good" / "ship it" / equivalent:
Append each card to the appropriate long-term file:
encoding/cards/<faction-lowercase>.ccgnfencoding/cards/dual.ccgnfencoding/cards/neutral.ccgnfCard {} declaration, separated by a blank line from the prior card.Rename the working file to indicate finalization:
encoding-artifacts/working-<ts>.ccgnf → encoding-artifacts/cluster-<ts>.ccgnfRegenerate encoding/cards/DISTRIBUTION.md:
make card-distribution
# or, if make isn't available:
python3 tools/update-card-distribution.py
The Python script is the real worker; make card-distribution is just a wrapper. Either is fine. The script scans every faction file, counts rarity × faction, handles dual pairs, and rewrites the table.
Run the encoding corpus test to confirm the appended cards still parse cleanly when combined with the rest of the set:
dotnet test --filter "EncodingCorpusTests" --nologo
Ask the user: "Ready to commit and push?"
If yes:
git add encoding/cards/ encoding-artifacts/cluster-<ts>.ccgnf
git commit -m "Add <N>-card cluster (seed <seed>): <short summary>"
git push origin main
If no: leave changes on disk and say where they are. The user can edit or commit manually.
encoding/cards/DISTRIBUTION.md follows this structure:
# Resonance — Card Set Distribution
*Regenerated by the `card-cluster` skill after each approved cluster.*
Target (per `design/Supplement.md §4`, 250-card set):
- Common: 44% (110 cards) | Uncommon: 32% (80 cards)
- Rare: 18% (45 cards) | Mythic: 6% (15 cards)
## Current counts — mono-faction
| Faction | C | U | R | M | Total | % of set |
|----------|---|---|---|---|-------|----------|
| EMBER | 4 | 2 | 1 | 1 | 8 | X% |
| ... | | | | | | |
## Dual-faction
| Pair | C | U | R | M | Total |
|-----------------|---|---|---|---|-------|
| EMBER/THORN | 0 | 1 | 0 | 0 | 1 |
| TIDE/HOLLOW | 0 | 0 | 1 | 0 | 1 |
## Neutral
| Rarity | Count |
|--------|-------|
| C | 3 |
| ... | |
## Grand total: N cards
Last updated: <iso8601-utc>
.gitignore has a rule for encoding-artifacts/working-*.ccgnf. Only cluster-*.ccgnf (finalized) gets tracked.| Condition | Action |
|---|---|
| Toolchain build fails | Stop; show the build error; ask user to resolve. |
| Parser doesn't converge after 10 iterations | Stop; show the final error set; ask for guidance. |
| Sync mismatch after edit | Fix immediately; treat as a bug, not a feature. |
| Appended cards break the corpus test | Roll back the append; report the offending card. |
| User rejects a card mid-cluster | Re-roll that slot (PRNG seed += 1000 + slot_idx) without invalidating other slots. |
User: Make me 5 new BULWARK commons focused on attrition.
Skill:
- Reads DISTRIBUTION.md; BULWARK C currently 4.
- Toolchain: already Release-built, skip.
- Seed: 1714572600 (from timestamp).
- PRNG rarity: all 5 C (user override).
- PRNG faction: all 5 BULWARK (user override).
- PRNG type: [Unit, Unit, Unit, Maneuver, Standard].
- PRNG keyword pick: [Fortify, Sentinel, Mend, Mend, Fortify-on-Standard].
- LLM names each; checks for collision with existing BULWARK cards.
- Writes encoding-artifacts/working-20260417T210000.ccgnf.
- Validates — 0 diagnostics.
- Sync-checks — all aligned.
- Presents the 5 cards in Supplement style.
User: Change card 3's cost to 2.
Skill:
- Updates both the block comment and the Card{...} cost field.
- Re-validates — clean.
- Re-presents.
User: Looks good.
Skill:
- Appends all 5 to encoding/cards/bulwark.ccgnf.
- Renames working file to cluster-20260417T210000.ccgnf.
- Regenerates DISTRIBUTION.md (BULWARK C is now 9).
- Runs corpus test — green.
- Asks: "Ready to commit and push?"
User: Yes.
Skill:
- git add encoding/cards/ encoding-artifacts/cluster-20260417T210000.ccgnf
- git commit -m "Add 5-card BULWARK commons cluster (seed 1714572600): attrition set"
- git push origin main