Forge’s combat AI is mostly rule-based heuristics, not deep game-tree search.
Entry points are in forge-ai/src/main/java/forge/ai/AiController.java:1297: attacking delegates to AiAttackController.declareAttackers, blocking to AiBlockController.assignBlockersForCombat.
For attacks, forge-ai/src/main/java/forge/ai/AiAttackController.java:806 does roughly this:
- Pick a defender: weak opponent, planeswalker near ultimate, or battle depending on context.
- Add forced attackers first: must-attack, end-of-turn-leaves-play attackers, attack requirements.
- Detect special plans like lethal assault, Exalted, attackMax, Reconnaissance, Lightmine Field, Revenge of Ravens.
- Estimate board pressure: AI attackers, opponent next-turn attackers, unblocked damage, counterattack damage, numeric advantage.
- Convert that into aiAggression from 0 to 6. 0 stays home, 1 mostly unblockables, 2-4 increasingly willing to trade, 5 all-out, 6 special/max/exalted style.
- For each possible attacker, call shouldAttack.
The “should this attack?” logic is in forge-ai/src/main/java/forge/ai/AiAttackController.java:1452. It first avoids using creatures with important tap abilities, then checks isEffectiveAttacker, then computes SpellAbilityFactors: can this be killed, can it kill blockers, is it unblockable, are blockers dangerous, does it have lifelink/wither/afflict/annihilator/trample, etc. The final decision is a switch on aiAggression.
For blocking, forge-ai/src/main/java/forge/ai/AiBlockController.java:1071 is staged:
- Sort available blockers weak-to-strong.
- makeGoodBlocks: profitable or safe blocks first.
- makeGangBlocks: double/triple blocks when they can kill something valuable.
- If life is in danger, make trade blocks, then chump blocks.
- Reinforce against trample if still in danger.
- If not in danger, add blockers to finish off attackers already blocked but not killed.
- Apply required blocks, planeswalker-saving chumps, menace/nonlethal gang blocks, then validate legality.
The card “heuristic” is mainly forge-ai/src/main/java/forge/ai/CreatureEvaluator.java:29. It gives creatures a numeric value from power, toughness, mana value, evasion, first/double strike, deathtouch, lifelink, trample, infect, vigilance, protection, indestructible, hexproof, mana abilities, bad drawbacks, etc. Combat uses that score to prefer losing worse creatures and killing better ones.
Combat outcome prediction lives in forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java:171: it predicts unblocked damage/poison, life danger, first strike, prevention/replacement, indestructible/regeneration/shields, deathtouch, flanking, pump-like bonuses, and whether a blocker kills an attacker or vice versa. So “what to block/attack with” is a mix of legality, tactical death prediction, life-danger checks, and creature-value comparison.
It is not pure search. Combat is a hybrid: heuristic construction first, then bounded simulation/local search.
For combat declarations, Strategist bypasses the generic action search and delegates directly to CombatAdvisor for DeclareAttackers / DeclareBlockers: ai/src/main/ kotlin/com/wingedsheep/ai/engine/Strategist.kt:41.
Attack choice flow:
- Starts with a heuristic seed: lethal alpha check, mandatory attackers, evasive/vigilance/indestructible/no-blocker attackers, survives-all-blockers cases, trample-through cases, and isProfitableAttack: ai/src/main/kotlin/com/wingedsheep/ai/engine/CombatAdvisor.kt:65.
- Then runs local search at declare attackers: try add/remove one attacker, simulate full combat, and keep improvements under a 1 second deadline: ai/src/main/ kotlin/com/wingedsheep/ai/engine/CombatAdvisor.kt:171.
Block choice flow:
- Sorts attackers by effectiveDamage, then builds a heuristic blocking plan: ai/src/main/kotlin/com/wingedsheep/ai/engine/CombatAdvisor.kt:229.
- The heuristic passes are free kills, profitable trades, surviving blockers for damage prevention, then gang blocks: ai/src/main/kotlin/com/wingedsheep/ai/engine/ CombatAdvisor.kt:484.
- If useSimulation = true, it improves the seed via bounded local search, capped at MAX_BLOCK_SIMULATIONS = 10: ai/src/main/kotlin/com/wingedsheep/ai/engine/ CombatAdvisor.kt:35.
There are definitely heuristics involved in ordering:
- Block mutation generation tries removals/additions/moves based on value, effective damage, cheapest blockers, and legal block checks before simulating: ai/src/main/kotlin/com/wingedsheep/ai/engine/CombatAdvisor.kt:430.
- Combat values come from CombatMath.creatureValue / combatTradeValue, weighting power, toughness, flying, deathtouch, first strike, double strike, lifelink, trample, indestructible, etc.: ai/src/main/kotlin/com/wingedsheep/ai/engine/CombatMath.kt:679.
- Trade willingness is life-scaled, so the AI accepts worse trades when low on life: ai/src/main/kotlin/com/wingedsheep/ai/engine/CombatMath.kt:605.
The generic non-combat search also uses heuristics for move ordering: it quick-simulates actions, sorts by 1-ply evaluation, then only deeper-searches capped top candidates with alpha-beta: ai/src/main/kotlin/com/wingedsheep/ai/engine/Searcher.kt:244. The board evaluator itself is heuristic too: life, board presence, card advantage, threat assessment, and tempo weights: ai/src/main/kotlin/com/wingedsheep/ai/engine/AIPlayer.kt:179.
So the short version: combat choices are heuristic-seeded, simulation-validated local search; general actions use shallow/deeper minimax-style search with heuristic move ordering.
In AI search, it is closest to immutable snapshot branching, not an event journal or undo log.
GameState is a Kotlin data class described as an “immutable snapshot of the entire game state” and the engine API is explicitly pure: (GameState, Action) -> (GameState, Events) in rules-engine/src/main/kotlin/com/wingedsheep/engine/state/GameState.kt:21. State changes use copy(…), withEntity, updateEntity, addToZone, removeFromZone, etc., each returning a new GameState instead of mutating the old one, e.g. rules-engine/src/main/kotlin/com/wingedsheep/engine/state/ GameState.kt:136.
For AI search specifically, ai/src/main/kotlin/com/wingedsheep/ai/engine/GameSimulator.kt:11 says it directly: because GameState is immutable, simulating an action is just calling process() on the same state, with no rollback or cleanup needed. Searcher branches by repeatedly calling simulator.simulate(state, action.action) and evaluating the returned result.state; see ai/src/main/kotlin/com/wingedsheep/ai/engine/Searcher.kt:51 and deeper plies at ai/src/main/kotlin/com/wingedsheep/ai/engine/Searcher.kt:126.
So the shape is:
root GameState simulate(action A) -> new GameState A simulate(action B) -> new GameState B simulate(action C) -> new GameState C
No journal replay. No undo stack inside search. No mutable board with reversible deltas.
Events do exist, but they are descriptive output, not the source of truth. ExecutionResult returns both state and events; events describe what happened for UI, triggers, logs, undo policy, etc. The state itself is already materialized. See rules-engine/src/main/kotlin/com/wingedsheep/engine/core/ExecutionResult.kt:14.
There is also a layering system, but that is for Magic continuous effects, not for recording history. GameState.projectedState lazily computes a ProjectedState by applying continuous effects in layer order once per immutable state instance; see rules-engine/src/main/kotlin/com/wingedsheep/engine/state/GameState.kt:104 and rules-engine/src/main/kotlin/com/wingedsheep/engine/mechanics/layers/StateProjector.kt:24.
The only caveat: this is “copy-on-write-ish” at the API level, but not a custom persistent data structure engine. Kotlin copy, Map +, and List + are used to build replacement snapshots. So search trades rollback complexity for allocation/copy cost. The repo even has a StateCloneBenchmark measuring copy(), updateEntity, and moveToZone, which confirms this is a known performance axis.
Lets improve AI combat, we can use the heuristic or search AI, but here’s what I’m thinking:
- If it’s the AI’s turn, calculate how much damage it could push through with optimal blocks from opponent (no spells played from hand, but can use abilities on board). Calculate the game state after optimal combat (opponent life, opponent board, AI board)
- If the AI is being attacked, do the same thing, but with opponent’s attacking creatures. Calculate optimal combat and evaluate the board state after combat.
- Trickery - If the AI has instant boots/damage or boost/damage abilities, don’t play them until after blockers are declared (or only play them to save a creature that’s about to be killed with another instant (ex. lightning bolt)), but calculate them when evaluating optimal combat.
Write a plan for this and suggest a couple of ways to do this and pros/cons of each approach. Also call out any missing details needed for the plan.
Why isn’t the AI attacking in this board position? It should be able to kill the player by activating the Flying Carpet or just attacking into the player. The player will be forced to bock all creatures, killing the Scathe Zombies w/Abu Jafar and losing Pikemen, Blacksmith and Abu Jafar
Board state AI - Life:11
- Drudge Skeletons
- Scathe Zombies w/2 Unholy Strength
- Lost Soul
- Bad Moon (x2)
- Flying Carpet (x2)
- Swamp x5
- Cards in hand: 0
Player - Life:3
- Pikemen
- Argivian Blacksmith w/ Artifact Ward and Lance
- Abu Ja’Far
- Ivory Cup
- Plains x6
- Cards in hand: 2
- AI with pinger always chooses the 1st creature, should look for lethal pings or pings against player also (eval board state)
- UI: when casting phantasmal terrain on a land, the land type should changes or somehow signify what type of land it is now.
- Implement Desert deal damage ability
I want to build an oracle verification system for mage-go.
This will consist of 2 major pieces: Recording and Replaying
Recording:
XMage is the gold standard for this and I want to record a game XMage plays against 2 AI opponents.
Take a snapshot every time the player has the opportunity to take an action. Game state excluding library and all the available actions the player could’ve taken at that step including text names of the action to take and legal targets, if any. (aside: Does XMage have targeting in available actions, or is that a “sub-choice” of the action?)
Also, triggered events can be ordered on the stack by the controlling player when more than 1 triggered event fires. Capture the ordering also, since ordering can affect game state after resolution.
This recording should be formatted to json and written to a file once the game completes.
Verify src/xmage/Mage.Tests/src/test/java/org/mage/test/selfplay provides these features.
- 220 call sites use a target: WithTarget(…) (127) + NewTargetedSpell(…) (93)
- 108 NewSpellAbility calls — many of these are untargeted (sweepers like Wrath of God, global effects like Howling Mine, Earthquake-style “each player/creature”)
- ~37 mana/equip/keyword abilities that don’t target at all
So “most need a target” is more like ~55%. That’s a meaningful minority of untargeted effects, and crucially — “target” is a rules-engine concept in MTG, not just an English word. Targeted effects check hexproof/shroud/protection (target.go:85, CanBeTargetedBy); sweepers don’t. Collapsing the two would break those checks.
Looking at what already exists at target.go:11-20 and the PermanentSelector system in doc.go:386:
- Targets (Target interface) — for “target X” rules text. Already composable via PermanentFilter combinators: Or(IsArtifact, IsEnchantment),
Not(HasColorFilter(Black)), And(…) (target.go:280, doc.go:301-307). Also has TargetAnyTarget() for “creature or player”.
- Selectors (ToSource, ToTarget, ToAllMatching(filter), ToGathered) — for non-targeted effects like Wrath, Berserk’s self-boost, Earthquake. Already composable via
the same filter combinators (doc.go:189-195).
So the composability you want is already there — it just lives in PermanentFilter/CardFilter, not on Target itself. Extending the abstraction probably means adding more filter combinators (e.g. a Union(filterA, filterB) if it doesn’t exist, or IsAttacking And HasColor(Green)) rather than restructuring Effect.
Where the current API does have a wart: WithTarget and Targeting(selector) are two different chains that look similar at a card-author’s call site. If you want to unify the call shape, the cleaner move is making every effect declare a Selector where Selector has two concrete impls — Targeted(target) and Untargeted(selector) — and the engine dispatches hexproof checks based on which. That preserves the rules distinction while giving one uniform call site.
The “NoTarget sentinel” version is risky because the rules distinction would have to live somewhere else (a flag on the effect?) and is easy to forget when authoring a card.
- remove BoostUntilEndOfTurn from DSL and update tests to use new Boost
- what are createTokenAttacking/Blocking effects?
- PutOnBattlefieldAttacking seems specific, why not PutOnBattlefield and have a way to specify whether it’s attacking or blocking
- game_mutator is basically GameReader at this point, refactor to new file name
- [x] card has IsToken() - a card is never a token, remove it
- [x] also weird checks for if tokens are in the graveyard, tokens can’t go to the graveyard only cards can
- preventAttackingUntilEndOfTurn is too specific, prevent attacking is fine, make the length definable
- [x] weird inStep checks, wtf does that even mean? - removed
- The engine does not yet implement timestamp ordering or dependencies within a
layer — effects are applied in insertion order. This is correct when only one effect occupies a layer, but may produce wrong results when multiple effects compete within the same layer.
DealDamage(Fixed(1)), DealDamageToPlayers(Fixed(1), SelectController()),
- [ ] add an “animate artifact” effect that turns an artifact into a creature with P/T equal to casting cost and add it to cards that animate artifacts (titania’s song, phrexian gremlins, animate artifact, etc)
- [ ] why is there DealDamageToCreatures and DealDamageToPlayers effects? Just have a DealDamage effect and then allow specifying targets Have to clarify between possible
targets and actual targets. ex AnyTarget vs EachPlayer vs AllCreatures, etc
- [x] make sure replacement effects like regen and forcefield drop off at the end of your turn. Also, note them on the card somehow?
Lets unify spells and non-mana abilities in the game engine. Spells and Abilities both share these common traits:
- cost - can be mana cost, or tap, or sacrifice a creature, or discard a card, etc
- effect - effect on resolution
- targets - if the effect specifies a target, valid target is the target, otherwise can be something like AllCreatures, AnyCreature, AnyPlayer, AnyOpponent, etc
identify any issues
- [] high tier enemies shouldn’t give basic lands as rewards
- [] better UI for knowing what options are available. Maybe cards with an action get yellow border instead?
- [] Show hand cards at the bottom of the screen, angled to look like a hand and a slight zoom effect when you mouse over them
- [X] Bug when loading game and going into a city and going to buy cards. All card images show up as the first card in the store, but are actually different cards.
- [X] in duel.go Show target when enemy cast spells (draw arrow)
- [X] in duel.go lands go to 3rd row. non-creature permanents go to 2nd row. When a non-creature permanent becomes a creature, move it to the 1st row
- [X] UI for X spells (like disintegrate)
- [X] bug with Sprite registry, is not showing images correctly in duel and rogue faces
- [X] in duel.go Add graveyard, clicking on graveyard shows cards
- [] Implement Fellwar Stone
- [] Raging River + UI needs
- [] Deathgrip - no way to choose spell to counter (maybe show the stack as a UI?)
- [X] Kudzu should allow player to choose which land to target instead of auto-selecting land
- [X] Island Sanctuary triggers after card is drawn
- [X] Desert doesn’t deal damage, add damage ability
- [X] lucky charms (crystal rod, ivory cup, etc) should cost 1 to activate and should be optional
- [X] jalum tome triggers on cast, it’s an ability not an ETB effect
- [X] Jade Statue - shouldn’t be able to attack the 1st turn it’s played
- [X] Forcefield - went from 1 life to 3 life?
- [X] Animate Dead - only play on dead creatures, not living creatures
When generating the level, make 5 zones, 1 for each color of magic. In each zone put the wizard’s castle for that color.
For now, when the player touches a wizard’s castle they enter a duel with the wizard of that castle. The rogue they are dueling is one of the 5 level 11 rogues, per the color of the deck (ex. Great Druid is the green castle wizard)
Also, when generating terrain for the level generate an appropriate type of terrain near the castle (ex. near blue generate sand, near black -swamp, near green, forest, etc)
The sprites for a wizard’s castle are in Castles1 and Castle2.
2 columns 6 rows
| White Castle | White Castle Destroyed |
| Blue Castle | Blue Castle Destroyed |
| White shadow | White Destroy shadow |
| Black castle | Black shadow |
| Black castle destroy | black destroy shadow |
2 columns 4 rows
| green castle | green castle destroyed |
| red castle | red castle destroyed |
| green shadow | green destroy shadow |
| red shadow | red destroy shadow |
—
- Zone-Based Difficulty (Biggest Win)
Right now all enemies level 1-10 spawn anywhere with equal probability. This is the core source of the “random” feeling.
Suggestion: Divide the map into concentric zones radiating from the player’s starting position (or from cities). Spawn enemy level based on zone:
- Inner ring (near starting city): levels 1-3
- Middle ring: levels 4-7
- Outer ring / mountains / marshes: levels 8-10
- Boss lairs: fixed locations for level 11-12
This gives the player a sense of direction — “I need to get stronger before venturing into the mountains.” The terrain types already map nicely to this (plains = safe, mountains/marsh = dangerous).
Implementation: In SpawnEnemies() in game/world/level.go, filter the rogue pool based on the spawn tile’s distance from the nearest city or from map center, instead of the flat level <= 10 check.
—
- Visible Enemy Indicators
Let the player see how dangerous an enemy is before contact. Options:
- Tint enemy sprites by tier (green/yellow/red glow or aura)
- Show the enemy’s level number floating above them
- Use different walking sprites per tier (you already have per-rogue sprites)
This turns encounters from “random punishment” into risk/reward decisions: “Do I engage that red enemy for a better ante card, or avoid it?”
—
- Progressive Spawning Over Time
Currently SpawnEnemies draws from the same pool regardless of game state. Tie the spawn pool to progression markers:
- Days elapsed: After day 10, allow level 5+. After day 25, allow level 8+.
- Duels won: Each 3 wins shifts the minimum spawn level up by 1.
- Amulets collected: Each amulet unlocks the next difficulty tier.
This creates a natural difficulty ramp even without zones.
—
- Reward Scaling & Card Economy
The engagement loop needs better pull (reasons to seek fights), not just push (enemies chasing you):
- Bounty system: Higher-level enemies drop better cards or bonus gold. Right now you only get
their ante card — add a gold bounty scaling with enemy level.
- City demand board: Cities could post wanted rogues with specific bounties, giving the player
a reason to hunt specific enemies.
- Card trader in cities: Let players sell unwanted cards for gold, creating a use for weak
cards won from easy fights.
—
- Encounter Variety (Random Encounters)
Your RandomEncounterScr system exists but feels like it could carry more weight:
- Positive events: Find a treasure chest (gold/card), healing spring (restore life), wandering
merchant (buy at discount)
- Choice events: “A traveler offers to trade a random rare for 3 of your commons” — meaningful
risk/reward decisions
- Lore/map reveals: Learn which rogues patrol a region, reveal city locations on minimap
—
- Retreat / Flee Mechanic
Getting caught by a level 10 enemy when you’re weak feels bad if the only options are “duel or bribe.” Consider:
- Flee option in DuelAnteScr — costs food instead of gold, with a % chance based on level
difference
- Partial loss on flee — lose some gold but keep your cards
- This makes the bribe system feel like a premium option rather than the only escape
—
- Quest-Driven Exploration
Quests currently feel disconnected from the core loop. Tighten them:
- Quest enemies appear on the map with a distinct marker so you can hunt them
- Chain quests: Completing a delivery quest unlocks a defeat quest in the destination city
- Quest rewards that affect the world: Completing a quest could clear high-level enemies from a zone, or unlock a new city shop tier
— Priority Ranking
If I were implementing these, I’d start with:
- Zone-based difficulty — immediately fixes the “random” feel
- Visible enemy indicators — gives player agency over encounters
- Progressive spawning — creates session-level arc
- Reward scaling — makes engaging strong enemies feel worth it
These four changes together would transform the loop from “wander and hope” to “explore, assess risk, grow stronger, push further” — which is the classic roguelike loop that keeps players coming back.
UI: Pause after the AI casts a spell or uses an ability, even if the player has no responses. Also show targets for AI spells and abilities
in game/screens/duel.go Only available blockers should be highlighted as available (ex, tapped creatures can’t be blockers)
Duel should show what AI creatures are blocking which player attackers, similar to how UI shows what AI creatures are being blocked by player’s creatures
UI: Don’t allow illegal blocks (UI shows I can block an islandwalking creature but I have a island in play)
BUG: Activated ability of dragon engine with mana flare out still taps 2 mountains to activate ability
For cards that are VintageRestricted, don’t show them in buycards, or give them as ante cards or ulimiteduse them in player’s starting decks
Tier 1 — High-frequency, high-impact (would unlock the most cards)
- Destroy effects (44 unparsed) — “Destroy target creature”, “Destroy target artifact”, “Destroy all creatures”. Core removal. Many instants/sorceries are just this.
- Draw cards (26 unparsed) — “Draw a card”, “Target player draws X cards”. Essential for card advantage spells like Ancestral Recall.
- Gain/Lose life (46 combined) — “You gain X life”, “Target player loses X life”. Simple number effects like healing salve.
- Tap/Untap (78 unparsed) — “Tap target creature”, “Doesn’t untap during untap step”. Common control effects (e.g. Icy Manipulator, Winter Orb).
- Return/Bounce (27 unparsed) — “Return target creature to its owner’s hand”, “Return target creature card from your graveyard to your hand”. Core blue mechanic.
Tier 2 — Medium frequency, important for gameplay
- Counterspells (14 unparsed) — “Counter target spell”. Needs stack interaction but critical for blue.
- Sacrifice effects (91 unparsed) — “Sacrifice a creature”, “As an additional cost, sacrifice…”. Many cards require this.
- Discard (20 unparsed) — “Target player discards a card”. Common black mechanic (e.g. Hymn to Tourach, Mind Twist).
- Tokens (21 unparsed) — “Create X/Y creature tokens”. Needs a token generation system.
- Restrictions/can’t (102 unparsed) — “Can’t be blocked”, “Can’t attack”, “Can’t be the target of spells”. Large category but very varied text.
Tier 3 — Less common but adds depth
- Prevent damage (37 unparsed) — “Prevent X damage”, “Prevent all combat damage”. Needs a damage replacement system.
- +1/+1 counters (26 unparsed) — Persistent stat modifications. Needs counter tracking on permanents.
- Exile/Remove (19 unparsed) — “Exile target creature”. Needs the exile zone to be functional.
- Gain control (4 unparsed) — “Gain control of target creature”. Needs controller-change logic.
- Tutor/Search (3 unparsed) — “Search your library for a card”. Needs library browsing UI.
Also worth noting: Keywords parsed but not enforced in the engine
- Protection (16 cards in scryfall) — parsed, but no actual prevention/targeting logic
- Banding (12 cards) — parsed, no combat logic
- Fear/Intimidate — parsed, no blocking restriction
- Shadow/Horsemanship — parsed, no evasion logic
- Indestructible/Hexproof/Shroud — defined in effects but not enforced
- Double Strike — parsed but combat doesn’t handle it
- Regeneration — parsed but no “save from death” logic
My recommendation for biggest bang-for-buck
Start with Destroy, Draw cards, and Gain/Lose life — they’re simple effects (target + number), map cleanly to new Effect types like DirectDamage already works, and would unlock a huge chunk of instants/sorceries. After that, Tap/Untap and Bounce would open up most of the remaining common spells.
Support adding counters to creatures.Counters can be different types (+1/+1, +0/-2, etc) and affect the creatures power and toughness.
Track counters on each creature and use that when calculating creature power/toughness.
Counter images are in assets/art/sprites/duel/Cardcounters.pic.png.
Menace means a creature can’t be blocked except by two or more creatures. It forces the defending player to commit multiple blockers or take the damage, making it a strong evasion ability on aggressive creatures.There’s only 1 card in the old school set that involves menace (Goblin War Drums)… this is very low priority.
Update the message as it changes phases and specify what actions the player needs to take. Ex, when discarding it should say something like “Hand size > max hand size, choose card to discard.” Instead of double-clicking on a card to perform an action, it should be a single click. And to view a card in the magnifier either hover over the card or double-click on it When it’s the players turn and they are selecting attackers, clicking on an available creature should change it’s attacking status from either not-attacking or attacking based on it’s previous status. When a creature is marked to attack it should have a yellow border around it. When the player is selecting blockers clicking on a creature should mark it as blocking and the player should click on an opponent’s creature to block it. When a creature is blocking it should have a red border around. clicking on a blocking creature should remove it as a blocker. Also draw an arrow from the blocker to the attacker to signify what creature is blocking which other creature Many in-game actions require a target (casting a lightning bolt, playing an aura enchantment, etc). When performing an action that requires a target update the status message saying something like “Choose a target”. Then highlight all possible targets with a yellow border.Once the player clicks on a target change the status message to “targeting X” and add a “cancel” button to the status message. If the player clicks cancel it reverts back to choose a target. If the player chooses cancel again, undo the action and return any spent mana to their mana pool.
Show the card associated with the active spell/ability on the stack in the magnifier. This should replace whatever card was previously in the magnifier and should also change based on players responding/adding new items to the stack. Add keyword images when creatures have appropriate keyword. Keyword images are in asserts/art/sprites/duel/Abilities.pic.png.1 column, has the following images in it (in order):
Green Red Blue Black White Protection from Green Pro Red Pro Blue Pro Black Pro White Pro artifact Fly Trample banding First strike regeneration reach blank
When a creature has a keyword (either from the card itself, or from an enchantment or other static ability) add the appropriate keyword to the bottom left of the card. Multiple keywords should have multiple images and shift over so the keyword images don’t overlap each other, but still stay on the card itself.
after winning a game against an Elementalist it said that I beat a merfork shaman, which is wrong:- you just beat: Merfolk Shaman
I think there is an issue with the duel_ante/duel handoff, or it’s a pointer issue or it’s a race condition where the enemy list is getting modified.
Destroy effects (44 unparsed) — “Destroy target creature”, “Destroy target artifact”, “Destroy all creatures”. Core removal. Many instants/sorceries are just this.see assets/card_info/scryfall_cards.json for the full list of cards and assets/card_info/parsed_cards.json for card parsing rules/format.
When a card leaves the battlefield (goes to graveyard, library, exile, etc) all counters should be removed and all auras attached to it should be discarded.
Enforce minimum deck size (flll with temporary random lands if below min size and starting a duel) and use player starting life in game/domain/player.go
the random encounters should give the player a land associated with the color/terrain type of the encounter (ex. plains gives a plains land, swamp gives a swamp, etc)
LATER Build a card image from the card back, the card art (scryfall ArtURL), text, card name and casting cost (assert/card/Manasymbols.pic.png)
Requires using mana symbols, which shouldn’t be stored in the repo.
https://claude.ai/share/14892337-d846-4ced-a18d-dee4222eb096
AI: Define positive vs negative creature enchantments. Positive enchantments help a player, negatives hurt.
- Winbk_Manaburn.pic.png - some pixels are white that should be black
- Winbk_Phase.pic.png - some pixels are white that should be black
The Winbk_Phase.pic.png is a sprite, it has 2 columns the first column (black background) is the default and whatever the current phase we’re in is the white background (column 2).
Update the phase image to handle that.
Note: Moving to getting card images from scryfall.com instead of storing them in the repo: see https://claude.ai/chat/f1e34184-a73c-4598-9b3e-2f81ad49658d.When the game starts, in a separate go func, start loading card images for the players deck and enemy decks. Preallocate the space needed for all these cards.
Instead of loading cards from cardimages.zip, use the PngURL (in assets/card_info/scryfall_cards.json.zst, see utils/download_card_images.py for reading PngURL) to fetch card images from scryfall.com.
Cache the images once fetched (like it works now). This fetching can be done for local and web builds.
If a card image hasn’t been downloaded yet and needs to be shown on the screen, use the blankcard image (current functionality).
Critical
- Combat is faked — duel_ante.go resolves duels with rand.Float64() > 0.25 (flat 75% win rate). The MTG rules engine exists in mtg/ but isn’t wired up. This makes deck-building feel pointless. This is by far the biggest issue.
- No audio at all — Zero sound effects or music anywhere. Button clicks, card purchases, combat, movement — all silent. Even simple click/hover sounds would make a big difference.
- Debug prints left in — button.go prints “Pressed” and “Hover” to stdout on every interaction. buycards.go prints button counts. These should be removed.
— High Impact
- Abrupt screen transitions — Every screen change is an instant cut (city entry, shop opening, duel start/end). Fade-to-black or slide transitions would make navigation feel much smoother.
- Camera snaps to player — The world map camera instantly locks to position with no smooth follow, easing, or anticipation. Even basic lerp would help.
- No quest HUD — Active quest and days remaining only show in the wiseman screen. The world frame should display current quest progress so players don’t forget what they’re doing.
- No visual feedback for actions — Buying/selling cards, accepting quests, winning duels — all happen with minimal or no feedback. Needs success indicators, animations, or at least flash effects.
— Medium Impact
- No card animations — Cards appear/disappear instantly in the deck editor and shop. Draw, flip, shuffle, and slide animations would add a lot of feel.
- Random encounter screen is a stub — Just shows a sprite and “Random Encounter” text, press Escape to dismiss. No choices, text, or consequences.
- Hardcoded screen dimensions — Magic numbers throughout (buycards.go even has a TODO about it). Makes the game fragile at different resolutions.
- Wiseman text pagination broken — Multi-page stories get truncated with “…” but there’s no way to advance pages.
- No particle effects — The game is entirely static visually. Ambient particles, purchase sparkles, or combat effects would add life.
— Low-Hanging Fruit (Quick Wins)
- Smooth scrolling — The scrollable list jumps instantly; adding interpolation is straightforward.
- Number counting animations — Gold, food, and life changes are instant. Counting up/down is simple and satisfying.
- Button press animation — Buttons have states but no scale/bounce on click. A small scale tween makes them feel tactile.
- Minimap player pulse — Currently blinks on/off every few frames. A smooth alpha pulse would look much better.
- Deck stats in editor — No mana curve or color distribution display. The 872-line editor is the most polished screen but still missing this.
Card sell prices are too high in the cities, make them 50%-75% cheaper, try to keep the sale price < 100 for cards, but keep the buy price similar to what it was before.
UI: Add a way for player to make choices. Ex. pay upkeep costs, choose card modes (ex. Healing Salve). Bring up a box that has the card in it with a button for each option. Also allow pressing a number to select one of the options. Example for Healing Salve:
- Gain 3 life
- Prevent the next 3 damage
This wouldn’t affect targeting or selecting blockers, only making choices that don’t involve targeting. In the case of healing salve you’d select the target, click done and then it would bring up the options menu.
When bringing up the card and the options shade the rest of the screen so it’s a little darker, like it’s disabled.
Use Tradbut1_png for the buttons displaying the options. See an example ing random_encounter.
UI: Way for player to pay upkeep costs, maybe just “pay upkeep cost yes/no” then if it’s a mana cost, just pay the mana, if it’s a discard cost let player choose which card to discard
UI: when placing creatures on the board, adjust creatures so there’s less space between them as more creatures are played, so they all fit on the screen.
UI: when selecting attackers instead of showing * for available attackers, draw a yellow border around them. If the player selects them turn the border green.
The declare blockers and combat damage steps are skipped if no creatures are declared as attackers or put onto the battlefield attacking.
Also update AvailableActions to remove illegal blocks (ex. a create without reach or flying can’t be assigned as a blocker to an attacker with flying)
Write tests to verify the correct behavior
- Flying
- First strike First strike is a keyword ability in Magic: The Gathering that allows a creature to deal combat damage before creatures without first strike during the combat damage step. This means that if a creature with first strike is involved in combat, it can potentially eliminate its opponent before they have a chance to deal damage back.
- Reach Reach is a keyword ability in Magic: The Gathering that allows a creature to block other creatures with flying. Essentially, creatures with reach can stop flying creatures from attacking, even though they themselves do not have flying.
- trample Trample is a keyword ability in Magic: The Gathering that allows an attacking creature to deal excess damage to the defending player if it is blocked. When a creature with trample is blocked, it must assign enough damage to destroy the blocking creature(s) first, and any remaining damage can then be dealt to the defending player.
- haste Haste is a keyword ability in Magic: The Gathering that allows a creature to attack or use activated abilities that require tapping immediately after it enters the battlefield, bypassing the summoning sickness rule. This means the creature can act right away, rather than having to wait until your next turn.
- vigilance Vigilance is a keyword ability that allows a creature to attack without tapping, meaning it can still block on the opponent’s next turn. This ability is primarily found on white creatures and helps maintain both offensive and defensive capabilities during gameplay.
- deathtouch Deathtouch is a keyword ability in Magic: The Gathering that means any amount of damage dealt by a creature with deathtouch to another creature is considered lethal damage, resulting in that creature being destroyed. This ability allows smaller creatures to effectively trade with larger ones in combat.
- lifelink Lifelink is a keyword ability in Magic: The Gathering that allows a player to gain life equal to the damage dealt by a source with lifelink. This life gain occurs simultaneously with the damage, meaning if a creature with lifelink deals 5 damage, its controller gains 5 life at the same time.
- landwalk Landwalk is a keyword ability in Magic: The Gathering that allows a creature to be unblockable if the defending player controls a land of a specified type, such as “islandwalk” for Islands or “swampwalk” for Swamps. This means that if the opponent has the relevant land type, they cannot block the creature with that ability.
I’m building a magic the gathering game (see mtg/GEMINI.md) and I’d like to parse the card text and return structured data describing the ability of the card.
What are some systems/libraries I could use to do this.
Example: This creature gets +1/+2 as long as you control a Forest. -> {“type”: “staticboost”, “condition”: {“zone”: “battlefield”, “type”: “land”, “subtype”: “forest”}, “boost”: “+1/+2”} Lightning Bolt deals 3 damage to any target. -> {“type”: “directdamange”, “amount”: 3, “target”: “targetany” } Flying {R}: This creature gets +0/+1 until end of turn. -> [{“type”: “ability”, “ability”: “flying”}, {“type”: “activated_ability”, “cost”: “R”, “boost”: “+0/+1”] {1}{G}, {T}: Target creature gets -2/-0 until end of turn. -> {“type”: “activated_ability”, “cost”: “{1}{G}, {T}”, “boost”: “-2/-0”}
I’d like to use a hybrid approach of regex to cover common cases and full grammar parser/LLM/manual configuration for complex cases.
Write a separate command to do card parsing and then write out the parsed results so edge cases can be handled manually. see cmd/ directory for separate commands. The parsing logic should be stored in mtg/parser/
when loading cards read assets/card_info_parsed_cards.json to get card effects and store them on the card object. These effects will be used in the engine (see mtg/). card struct is stored in game/domain/card.go.
Lets implement combat into the mtg engine. all the logic is in the mtg/ directoryRead mtg-Turn-structure.org in the combat phase to understand the steps.
- Write a function AvailableAttackers that returns a list of cards that can be declared as attackers. To be able to attack a card must be Active and not Tapped (see IsActive() function in mtg/core_engine/card.go). It must also be a creature card and currently in the player’s battlefield zone.
- Declaring an attacker causes the attacking card to tap.
- AvailableActions() will need to be modified based on the phase/sub-phase that we’re currently in (ex. only in the main phases can you play land or cast sorceries)
Write tests for this and add this functionality to cmd/mtg_test where players attack and block randomly.
| Beginning of Combat | “At beginning of combat” triggers; players get priority |
| Declare Attackers | Active player declares attackers; triggers fire; players get priority |
| Declare Blockers | Defending player declares blockers; damage assignment order set |
| Combat Damage | First/double strike damage (if applicable), then regular damage |
| End of Combat | “At end of combat” triggers; creatures removed from combat |
- Beginning of Combat Step: Triggers that happen “at the beginning of combat” occur here. Players get priority.
- Declare Attackers Step: The active player declares attackers. Turn-based actions (tapping/declaring attackers) happen first, then players get priority.
- Declare Blockers Step: The defending player declares blockers. Turn-based actions (declaring blockers) happen, then players get priority.
- Combat Damage Step: If creatures with first/double strike exist, this happens twice; otherwise, once. Damage is assigned, then players get priority.
- End of Combat Step: Triggered abilities that happen “at end of combat” trigger here. Players get priority before creatures are removed from combat.
Bug: creatures are able to attack immediately after they’re played, fix this bug
output generated by cmt/mtg_test/main.go mtg game logic in mtg/
Creatures can only attack if IsActive() returns true (have to be untapped and not have summoning sickness)
ex of bug:
Turn 2 - Player 1 (Life: 6) Phase: Untap Phase: Upkeep Phase: Draw Drew a card Phase: Main1 Available actions: [PlayLand Mountain#3 CastSpell Kird Ape#1 CastSpell Kird Ape#2 CastSpell Lightning Bolt#4 CastSpell Lightning Bolt#6 CastSpell Lightning Bolt#7 CastSpell Lightning Bolt#9 PassPriority] Casting Lightning Bolt#6 targeting Kird Ape#33 Kird Ape#33 now at -2 life Available actions: [PlayLand Mountain#3 PassPriority] Playing Mountain#3 Available actions: [CastSpell Kird Ape#1 CastSpell Kird Ape#2 CastSpell Lightning Bolt#4 CastSpell Lightning Bolt#7 CastSpell Lightning Bolt#9 PassPriority] Casting Kird Ape#1 Phase: Combat Kird Ape#1 attacks
Write a command called mtg_test that plays a game of magic the gathering from the terminal and prints out the steps. I’d like it to print out who’s turn it is and the current phase as the phases change. Players should have a deck of 30 cards a combination of lighting bolts and mountains.
the players should pick randomly from the list of available actions and then perform those actions (ex. cast spell or play land).
Players lives start at 9 each. Play until 1 player loses or any player has played more than 5 turns.
see mtg/ subdirectory for rules and game engine implementation. Also look at test files.
write a function AvailableActions that returns a list of actions a player can take at the current state of the game. For example when it’s a players main phase and they haven’t played a land that turn yet and they have land cards in their hand it should return 1 or more “Play $LAND” actions (1 for each card in hand). See mtg/README.md for how the mtg rules engine works.
- Monthly status updates
- r/shandalar
- shandalar discord
- pig squad?
- blog
- Retro Game Submission form
- Ebiten splash logo
- Spanish version? https://www.clasicosbasicos.org/juegos/juegos-de-mesa/magic-the-gathering
In the MiniMap screen, assets.MiniMapTerrSpr_png (file assets/art/sprites/mini_map/Ttsprite.spr.png) contains 5 rows of images, each row has a different size.
The rows are
- 13×13: 48 images (gems/diamonds in various colors)
- 13×20: 6 images (taller UI elements)
- 15×15: 1 image
- 16×16: 10 images (medium sprites)
- 23×23: 10 images (larger sprites)
Update game/ui/imageutil/loader.go LoadSpriteSheet() to allows the caller to specify an image size for each row. Or write a new function that is like LoadSpriteSheet() but takes image sizes as arguments.
Then update games/screens/minimap.go to use the new function to load assets.MiniMapTerrSpr_png and display the correct city
I’m building a 2d isometric game that has a world you walk around in. That world can have different types of tiles (plains, forest, water, etc). These tiles are often grouped together in various shapes (ex. water tiles forming a lake or a river). I want you to describe an algorithm that could be used to detect transition points when the tiles change (ex. from a plains to a river tile) and suggest those connections.
Here’s what I’m thinking:
For each tile, look at it’s orthogonal neighbors and for each different type, note what type it is and what type of transition is needed. A transition could be required for a single edge or for multiple edges. Also, the type of transition may continue into the next neighbor so you would need to mark the transition as “open” vs “closed”.
Ex.
W P1 W P2
Using P1 as the tile, you would need a W transition on the West side and because P2’s West side is also W, the P2 transition South should be “open” so the transition can continue into P2.
Ex.
W P1 P P
In this example we P1 needs a W transition on the West side, but it should be closed to the South side.
This means the algorithm will only need to look at a tiles neighbors and neighbor’s neighbors to decide what transition to make.
<><> <><>
Build a tile map that describes how to join the transition sprites together.
Analyze the assets/art/sprites/world/land/Cstline1.spr.png file and look at the first 28 sprites (see cmd/tile_transitions). They should all be water sprites. Now find out which pieces connect together. You can do that by analyzing the lines of blue pixels on each of the corners. Ex. the first sprite has a blue edge that runs from 0,27 to 14,33 (could be off by a few pixels). That looks like it should match up with sprite in row 5 column 4, it has a blue edge that runs from 357,204 to 372,212.
Write a script that will find these edges that are similar and print them out. These are the pieces that can connect to each other.
Edges must be less that 50 (euclidean distance).
Also corners can only connect in certain ways. ex. 3->2 means starts at corner 3, edge is heading towards 2
| P1 | P2 |
|---|---|
| 3->2 | 0->1 |
| 1->2 | 0->3 |
| 2->1 | 3->0 |
| 1->0 | 2->3 |
Ok, lets simplify this. The tiles can only have an edge on a corner. The pixel points of each tile subimage corner are:
| Corner | Point (X,Y) |
|---|---|
| 0 | 51,0 |
| 1 | 102,25 |
| 2 | 51,51 |
| 3 | 0,25 |
Edges can only be on the line between 1 corner and it’s preceding and next corner. Ex from corner 0, edges can only be 0->1 or 0->3 an edge is not from 0->2. This should simplify edge finding a lot. Now instead of counting blue pixels you can count the number of non-transparent pixels.
For the tile at 0,0 in the sprite you should have 2 edges. 1 ~50 pixels long from 3->0 and 1 ~25 pixels long from 3->2.
For the third tile in the row, you should have 3 edges 1 ~25pixels long from 0->1, 1 ~50pixels long from 0->3 and 1 ~25 pixels long from 3->2.
Build a tile map that describes how to join the transition sprites together.
Analyze the assets/art/sprites/world/land/Cstline1.spr.png file and look at the first 28 sprites (see cmd/tile_transitions for sprite size). They should all be water sprites. Now find out which pieces connect together. You can do that by analyzing the lines of blue pixels on each of the corners. Ex. the first sprite has a blue edge that runs from 0,27 to 14,33 (could be off by a few pixels). That looks like it should match up with sprite in row 5 column 4, it has a blue edge that runs from 357,204 to 372,212.
Write a script that will find these edges that are similar and print them out. These are the pieces that can connect to each other.
Build a tile map that describes how to join the transition sprites together.
Cstline1.spr.png is a sprite image with 4 columns and 21 rows. For now we only care about the first 7 rows.
Look at cmd/tile_transition/edge_connections.txt to see how the tiles connect to each other. We need to store that info as a tile map that can be used by the game to build tile transitions between land tiles in the game.
We also need to identify the full-length edges. These will be used to place them correctly against the tile type (ex. the first 28 images are water transitions, so we want the long edge to be up against a water tile)
See cmd/tile_transitions/analyze_edges.py for how to identiy edges. It currently is set to look for connection edges (ednges < 40). Now we want to find edges that are at least 40
Suggest a tile map format that can be used to store this metadata about transition tiles.
Write a separate program that will generate a smaller world and overlay the transition tiles. I want to see some examples of tile transitions.
Each tiles has four corners in a diamond shape, starting from the top 0-3 representing N,E,S,W directions. When different tile types meet (ex. Plains/Forest) there is an additional threshold tile added. These are the Cstline_png and Cstline2_png sprites.
CstlineMap_txt is 7 rows with 4 columns per row. Cstline1_png has 3 sets of tiles, 1 for Water, Marsh and Forest tiles. Cstline2_png has 2 sets of tiles: Sand and Ice.
Each set of tiles is the same, just for different types of terrain.
The map file CstlineMap_txt describes what corners the threshold merges. There are 4 images per line and are separate by the ‘|’ character in the map file.
Ex:
0,1|0,3 means there are 2 images, the first covers the 0,1 corners and the second covers the 0,3 corners.
After placing tiles when generating the world, go back through the map and for each tile look at the surrounding tiles. If there is a change from one type to another, add a threshold tile overlay to visually transition the tiles.
Ex:
legend: (W = Water, P = Plains)
Tiles are offset because they’re diamond shaped
W W P1 W P2
Each tile requires 2 transition pieces to complete the length of the tile.
P1 has 2 transition pieces T1 and T2. Both have only a single full edge from 0->3 or 3->0. T1 must have T2 in it’s list of connect edges.
T1’s 0 corner starts at P1’s 0 corner. T2’s 0 corner starts halfway between P1 0 and 3 corner.
P2 has 2 transition pieces T3 and T4. Both have a single full edge from 0->3 or 3->0. T2 must have T3 in it’s list of connect edges and T3 must have T4 in it’s list of connect edges. T3’s 0 corner starts at P2’s 0 corner. T4’s 0 corner starts halfway between P2’s 0 and 3 corner.
Read Cstline_map.json to see the map of tiles. the map defines a set of transitions. Each tile type (Water, Forest, Marsh, Sand, Ice) has the same set of transitions, just styled to match the tile type. The full tileset is 28 tiles (7 rows, 4 cols). Cstline1.spr.png has (in order) Water, Marsh and Forest. Cstline2.spr.png has Sand and Ice.
To start build a map of the above example tile layout and write a solution that only places the water transitions.
Modify cmd/tile_transitions to do this.
TRANSITION_CORNER_POSITIONS = { 0: (51, 0), # Top 1: (101, 25), # Right 2: (51, 51), # Bottom 3: (0, 25), # Left }
TILE_CORNER_POSITIONS = { 0: (103,0), 1: (205,50), 2: (103,100), 3: (0, 50) }
EDGE_DIRECTIONS = [ (0, 1), # Top to Right (1, 0), # Right to Top (1, 2), # Right to Bottom (2, 1), # Bottom to Right (2, 3), # Bottom to Left (3, 2), # Left to Bottom (3, 0), # Left to Top (0, 3), # Top to Left ]
I want to refactor how decks and card collections are managed. Currently a player has a card collection but all Characters have a deck. Characters consist of the player and rogues (enemies).
Instead of player having a collection, characters should have a card collection, where each collection item has additional metadata showing which deck a card is part of. A player can have multiple decks, but enemies only have a single deck.
type CollectionItem struct { Card Count int - number of cards in the collection DeckCounts []int - index of deck that has the count of the Card in the deck }
To determine if a card is in a deck you have to iterate over the list of collectionitems. When a player loses a duel, it decrements the cards Count and iterates over DeckCounts and sets the value in each array to the min of Count and the value of DeckCounts.
Instead of “Sideboard” cards, rogues will now have all their cards in their “collection”. By default all cards (DeckRaw and SideboardRaw) and the Rogue will only have a single deck. It will contain all cards listed in DeckRaw, but not in SideboardRaw.
Card art is 150px wide
- drawing text onto card frame would make it clearer as it’s scaled up
- Magnified size is 250-300px wide
- Using blank-card.png would make card art 236px wide
Modify download_card_images.py to use the
Is there a way to tell if my game is running in wasm/browser vs on a regular OS? If so, I’d like to modify the CardImage() function to make https requests to https://throwingbones.com/ben/cardimages.zip instead of reading it from the embedded file system.Write a package that can read a zip file over http that uses the range operator to download the zip file directory and then download the individual file in the compressed .zip file using the range operator.
Cache the directory in memory so it doesn’t require re-requesting the zip file directory.
The library zip reader will reread the directory when opening a file (see `findBodyOffset()`). Instead make a custom version of `findBodyOffset` and `Open()` that use a local copy of the zip file directory.
See game/utils/httpzip/reader.go for a partial implementation
Need a way to save and load the game. Includes building menus for saving/loading and a way to load/continue the game at start.
Come up with a way to save the game state. You’ll want to save the generated world (tiles and cities), the location of all current enements and the player object. Then implement a save function and write some tests.User provides prompt file and worktree name
Write a script that will perform the same steps as the new Google AntiGravity editor.
agcli.py –prompt -p Prompt filename –worktree -w Name of git worktree to create
The script itself should do the following steps:
- Make git worktree
- change to worktree directory
- Write an implementation plan based on prompt [claude_yolo.sh]
- Write code based on plan [claude_yolo.sh]
- Run tests [claude_yolo.sh]
- Do code review [claude_yolo.sh]
- Make suggested changes [claude_yolo.sh]
- Commit changes [claude_yolo?]
For each step that requires claude_yolo.sh - write a prompt that will for the action. These prompts will be embedded into the script itself so the user can change them if needed.
claude_yolo.sh - a version of claude code that runs in a container and doesn’t prompt for user input.
- make test fails because it can’t compile /usr/local/go/pkg/tool/linux_amd64/link: running gcc failed: exit status 1 /usr/bin/gcc -m64 -s -o $WORK/b272/httpzip.test -rdynamic /tmp/go-link-3891896473/go.o /tmp/go-link-3891896473/000000.o /tmp/go-link-3891896473/000001.o /tmp/go-link-3891896473/000002.o /tmp/go-link-3891896473/000003.o /tmp/go-link-3891896473/000004.o /tmp/go-link-3891896473/000005.o /tmp/go-link-3891896473/000006.o /tmp/go-link-3891896473/000007.o /tmp/go-link-3891896473/000008.o /tmp/go-link-3891896473/000009.o /tmp/go-link-3891896473/000010.o /tmp/go-link-3891896473/000011.o /tmp/go-link-3891896473/000012.o /tmp/go-link-3891896473/000013.o /tmp/go-link-3891896473/000014.o /tmp/go-link-3891896473/000015.o /tmp/go-link-3891896473/000016.o /tmp/go-link-3891896473/000017.o /tmp/go-link-3891896473/000018.o /tmp/go-link-3891896473/000019.o -O2 -g -lresolv -O2 -g -lpthread /usr/bin/ld: cannot find /lib/x86_64-linux-gnu/libc.so.6: No such file or directory collect2: error: ld returned 1 exit status
- go build fails becuase cardimages.zip can’t be found assets/cardimages_native.go:9:13: pattern art/cardimages.zip: no matching files found
- gofmt works
- git command failure:
Bash(git add game/screens/edit_deck.go game/ui/elements/scrollable_list.go .agcli/IMPLEMENTATION_PLAN.md) -> Error: fatal: pathspec ‘game/screens/edit_deck.go’ did not match any files
- failure message: Error: fatal: pathspec ‘game/screens/edit_deck.go’ did not match any files
Implement colored amulets. These are amulets the player can collect either by fighting enemies or completing quests in a town. There is one amulet for each color of magic (Red, White, Blue, Green and Black). Each city/town has an amulet color associated with it. This will affect other aspects of the game, so store the city color when creating a new city.
Display amulets on the world_frame screen. Add Amsprite.spr.png to assets/embed.go and use that to display the amulets. The colors in the sprite are (in order):- White
- Blue
- Black
- Red
- Green
Display them in the world frame at 125,628 and 250,628 and 375,628 and 500,628 and 625,628. Immediately next to the image of the amulet display the player’s count of that color of amulet, similar to how gold and food count are displayed.
How would I implement a click-and-drag function? In edit_deck.go I’d like to be able click and drag cards to/from the player’s deck area.
Seems like I need a “droppable areas” where draggable objects can be dropped?
Also, need to be able to designate things as “draggable”. Then when the left button is clicked and held the draggable item should follow the mouse cursor.
Add a new UI effect that can designate an element as draggable and another effect that implements droppable. Maybe this can be done through an interface?
Make the items in the scrollable list “draggable”
Cities have 3 tiers:
- Hamlet
- Town
- Capital
Update game/domain/city.go to use a enum for the tiers and update all calls to city tier to use the enum.
World magics
Each world magic is assigned to a random Capital (tier 3) city as the world is being generated. A player can buy the world magic at the city that has that world magic. Also, the world magics a player has purchased should be tracked on the player object.
List of world magics and gold cost:
- Sword of Resistance - 400
- Quickening - 300
- Leap of Fate - 300
- Ring of the Guardian - 500
- Haggler’s Coin - 250
- Tome of Enlightenment - 300
- Sleight of Hand - 300
- Staff of Thunder - 100
- Conjurer’s Will - 300
- Dwarven Pick - 125
- Amulet of Swampwalk - 125
- Fruit of Sustenance - 50
- after completing a dungeon
- after buying a world magic
- after defeating a Planeswalker
- After defeating an enemy select cards based on rarity. Common and uncommon cards should be given as rewards more often and rare cards should be less frequent. Reserved cards (Black Lotus, Moxen, etc) should never be given as rewards.
In the original Shandalar there were dungeons hidden across the map/level, similar to random encounters, but there are a fixed number of dungeons for the game.
Players can gain information about dungeons from creatures, Wise Men, or from Lairs. Three clues are given for each dungeon, the position on the map, what size/color of creatures it contains, and what effects are in play for the given dungeon.
Dungeons contain the most powerful restricted cards as rewards.
On entering the dungeon the game changed from an openworld game to a turn-based movement game where the enemies are static and the player walks in narrow hallways either being forced to fight the enemy or walk down a different hallway. At the end of some of these hallways would be rewards. Rewards could be a restricted card, or some gems and money
With each higher level dungeons/castles are more populated.
There are four major events in dungeons/castles.
- Creatures The dungeon will give you a clue as to what the dungeon/castle
is populated by, this can be either large or small creatures of a given color. The higher levels have more creatures. You do NOT get ante from creatures in dungeons, and if you lose a duel in the dungeon you lose your ante card AND get booted from the dungeon/castle. If you are in a dungeon, and you did not get all the Restricted cards from the dungeon, then the dungeon moves to a random location on the map.
- Treasure chests Treasure chests either hold Restricted cards or gold+amulets. In
castles there are NO Restricted cards.
- Dice These dice are give either advantages or disadvantages for the
next duel ONLY in the current dungeon/castle. If you get an advantage and then leave, you lose the advantage. On the apprentance level you can never get a disadvantage from dice. On higher levels you might get a disadvantage.
There are two types of advantages
- +X lives. This is additive(?) you get to keep this live
while in the dungeon/castle. In dungeons where life carries over, this is necessary. the easier levels give you around 3-4, while the harder ones give 1-2.
- Start with a card. This can be anything from creatures, to
mana sources. On easier levels the cards given are usually powerful creatures, while the harder levels give weaker creatures or moxes/mana creatures.
There is one type of disadvantage
- -X lives.
NOTE: if you leave the dungeon/castle any -vantanges you have are GONE. You can bring -vantages into a dungeon or a castle just fine.
- Scrolls Scrolls are question given based on the cards in the game. If
the question is wrong, a creature appears in its place. On the easier levels the questions have 5 possible choices, while harder ones usually have more choices. NOTE: question take into account ONLY the starting abilities of a creature. Goblin Balloon Brigade does not have flying for the purposes of the scroll.
Examples of questions
- which of these creatures has flying ability?
- What special ability does this creature have?
- what is the casting cost/power/toughness of this creature?
If you leave a dungeon/castle, advantages given to you from the dice in a dungeon/castle are GONE. You can bring advantages into a dungeon or a castle just fine.
Each dungeon/castle will have one card in play when you enter a duel. This card is an enchantment of the color of the dungeon/castle or non-creature artifact without upkeep. There will also be an additional effect on high levels that forbid you from bringing in certain types of cards.
Note: If a dungeon forbids a certain type of card (like fast effects, or white) then it forbids you from drawing that type of card from your deck. BUT if that brings your deck below the minimum required for your level it does not fill your deck up to the minimum with basic lands.
Also, if you do not get all the cards from the dungeon, then it moves to a random location on the map. To find it again, you have to get another dungeon clue for that dungeon.
- make harder, fix life loss bug
- magic/chess puzzles? (Ex win this turn)
- no reserved cards
- game loss is permadeath (hardcore mode)
- increased enemy strength (life, starting cards, etc)
- mana link
- free card
- amulet
- quest timer
- destination city
When the player is in a city and talks to the wiseman, sometimes he can give the player quests. The reward is known at the time the quest is shown and the player can refuse the quest. A player can only have 1 active quest at a time.
These quests are one of several types:
- Go to a nearby city (Deliver a Letter or other task)
- Rewards: Mana Link (player life total goes up by 1. Once a city has been linked, it can’t be linked again unless it has been unlinked by an enemy)
- Rewards: Amulet - 1-3 amulets of that cities amulet color
- Rewards: free card - the player is given a card that includes that cities amulet color
- Defeat an enemy - Wiseman says something like “A Goblin Warlord has been menacing our village”
- Rewards: Mana Link (player life total goes up by 1. Once a city has been linked, it can’t be linked again unless it has been unlinked by an enemy)
- Rewards: Amulet - 1-3 amulets of that cities amulet color
- Rewards: free card - the player is shown a list of cards that include that cities amulet color and can choose one of them
Each quest has a “quest timer” and if not completed in that time, that city will no longer give the player new quests.
see game/domain/city.go for city details and game/domain/player.go for player details
see also assets/text/Advstrings.txt for some sample text for quests (@QUESTSTATUS). Modify as needed.
While walking around the overland (see game/world/level.go) there are random encounters the player can interact with. The sprites for them are in assets/art/sprites/world/land/Locatn07.spr.png, similar to Cities1_png, but is 5 cols 2 rows, with the 2nd row being the ground/shadows.
These encounters should appear on a tile as the player walks around the world, similar to how enemies spawn, but less frequently than enemies.
Once a player touches the encounter take them to a new screen that just says “Random Encounter” and the player can press Escape key to exit.
Build the random encounters logic drawing/updating (in a file named random_encounters.go) and build the default random encounter screen (in game/screens/).
After a player leaves the random encounter, it is removed from the map and can’t be revisited.
On the city screen a player can click on “Begin Quest” to talk to the sage of the city. Go to a new screen that with the Wiseman_png image (in embed.go) as the backgroud (no border, scale wiseman image to screen size (1024x768)). Use the assets/text/Advblocks.txt file to display the story the wiseman has to tell.
Display the text in the rectangle {0,0} to {290, 768}. If the text is too large to fit in that rectangle add ellipses and use click/touch/spacebar to advance to the next block of text.
At the final line, pressing click/touch/spacebar takes you back to the city screen.
Use the Just functions (JustPressed, etc) to make sure it doesn’t accidentally scroll 2 screens at once.
Initial version will allow player to bride or “play game”. If play game is chosen, randomly determine winner and give wins/losses.- [X] Clean up Rogue/Enemy refactor
- [X] Implement bribes (static 20 gold)
- [X] Implement duel (75% chance of winning, win 3 cards or lose ante)
- [X] show winning screen
- [X] show losing screen
- [X] Give player starting deck
- [X] Add enemy “level” to .toml files (determines life and bribe cost) wiki-src
- [X] Add color for each enemy (to choose background)
- or do it at runtime by counting cards/mana symbols
| Tier | Enemy Name | Bribe | Life |
|---|---|---|---|
| Weak | Cleric | 40 | 10 |
| Weak | Seer | 40 | 10 |
| Weak | Witch | 40 | 10 |
| Weak | Sorceress | 40 | 10 |
| Weak | Druid | 40 | 10 |
| Aggro | Priestess | 40 | 12 |
| Aggro | Merfolk Shaman | 40 | 12 |
| Aggro | Undead Knight | 40 | 12 |
| Aggro | Sorcerer | 40 | 12 |
| Aggro | Elvish Magi | 40 | 12 |
| Typical | Crusader | 60 | 14 |
| Typical | Conjurer | 60 | 14 |
| Typical | Warlock | 60 | 14 |
| Typical | Troll Shaman | 60 | 14 |
| Typical | Enchantress | 60 | 14 |
| Two-color Lesser | Tusk Guardian | 70 | 16 |
| Two-color Lesser | Mind Stealer | 70 | 16 |
| Two-color Lesser | Sedge Beast | 70 | 16 |
| Two-color Lesser | Elementalist | 70 | 16 |
| Two-color Lesser | Fungus Master | 70 | 16 |
| Intermediate | Paladin | 80 | 18 |
| Intermediate | Sea Dragon | 80 | 18 |
| Intermediate | Vampire Lord | 80 | 18 |
| Intermediate | Goblin Lord | 80 | 18 |
| Intermediate | Forest Dragon | 80 | 18 |
| Genies | Saltrem Tor | 80 | 19 |
| Genies | Aga Galneer | 80 | 19 |
| Genies | Queltosh | 80 | 19 |
| Genies | Alt-a-Kesh | 80 | 19 |
| Two-color Greater | Lord of Fate | 100 | 20 |
| Two-color Greater | Winged Stallion | 100 | 20 |
| Two-color Greater | Centaur Shaman | 100 | 20 |
| Two-color Greater | Centaur Warchief | 100 | 20 |
| Two-color Greater | Ape Lord | 100 | 20 |
| Strong | Archangel | 100 | 22 |
| Strong | Shapeshifter | 100 | 22 |
| Strong | Nether Fiend | 100 | 22 |
| Strong | Hydra | 100 | 22 |
| Strong | Beastmaster | 100 | 22 |
| Dragons | Kiska-Ra | 110 | 24 |
| Dragons | Whim | 110 | 24 |
| Dragons | Mandurang | 110 | 24 |
| Dragons | Dracur | 110 | 24 |
| Dragons | Prismat | 110 | 24 |
| Henchmen | High Priest | - | Up to 27 |
| Henchmen | Thought Invoker | - | Up to 27 |
| Henchmen | Necromancer | - | Up to 27 |
| Henchmen | War Mage | - | Up to 27 |
| Henchmen | Summoner | - | Up to 27 |
| Guildlords | Sainted One | - | 35-45 |
| Guildlords | Astral Visionary | - | 35-45 |
| Guildlords | Greater Lich (Azaar) | - | 35-45 |
| Guildlords | Dragon Lord (Kzzy’n) | - | 35-45 |
| Guildlords | Great Druid (Morgane) | - | 35-45 |
| Final Boss | Arzakon | - | 400 |
Major UI elements needed:
- [X] scrollable bottom section, always scroll by 1 full card?
- [X] card hover
- [X] deck layout
- selecting buttons to filter library
- search box for filtering library
- drag-and-drop cards into/out of deck
- right-click to sell? Or multi-select then sell button?
- deck group by card
- [X] card image for art/name only
- [ ] Show cards
- [ ] Sell cards
- [ ] Add Decks to player, including active deck
When the last card in a collection list is added to a deck, it should be removed from the collection list.
When you’re in a town you can sell cards. Sale price is default 50% of the buy price. 60% at tier 2 cities and 75% at tier 3 cities (capitals). All cities have an amulet color and if the color identify of the card includes that amulet color it increases the price an additional 10%.
Add a function in the Card object that accepts a City as an argument and returns the sale price.
Then in the deck editor if you hover over a card, pressing the S key will sell the card. Also, when hovering over a card display the sales price above the card magnifier.
In Deck Edit screen add card counts to the collection, similar to how it works in the deck edit area.Hovering over a card in the deck area should trigger the magnifier.
In the draggable section of edit_deck.go I also want to display the player’s deck. Use the card art images for the cards and display them in there. Also if there are multiples of the same cards only show a single card, but add teh count to the lower right hand corner of the card.
Group the cards by name, even if they’re different printings (ex. group all mountain cards together).
Also the draggable area should be the rectangle (x,y) and should be call the deck area: 300x0 - 1024x588
- [ ] constraints on a player’s deck
- should only allow cards in a player’s collection (can check when adding a card to a deck)
- selling a card/losing a card to ante should remove the card from the card collection and all player decks
Every 30 ticks if a player hasn’t interacted with an enemy in the last 100 ticks spawn a new enemy.
Also, make the enemies have a little movement jitter towards the player instead of running directly at the player. Also, make the enemies occasionally stop and wait until the player gets closer and then run towards them.
Should I store card art in the repo?
- No, this bloats the repo. Instead have tools to download images from scryfall as needed.
Options:
- Store individual card art on a server in the format and compression I want. Make http requests when loading art.
- Con: Now I’m responsible for keeping carddart up or the game breaks.
- Con: Could make calls to scryfall directly, but again it requires some server to stay running and a way to get card URLs
- Store full zip files.
- Could be in embed.FS
- Con: 2GB file limit for binaries
- Con: large filesize makes download slow
- Could be distributed with game, like .WAD files for doom/quake
- Pro: people can distribute their own .zip files with their mods for custom card lists
- Con: Doesn’t work with web version, there is no .zip file to reference
- Could be in embed.FS
Image art should be 190x150 card back images are 228 x 325
Would also require text for:
- mana cost
- set symbol
- card text
- flavor text
I’ve opted out of writing a cat decoder since I can get card art online: https://scryfall.com/docs/api/images
Medart.cat is 31MB
That’s 300 cards at 100k/card, which seems possible.
- burgeoning.jpg is 160k
- burgeoning-art.jpg is 130k
Try converting them to .png and running:
- pngquant –quality=60-80
Example art image for burgeoning, size is 190x140
-rw-r–r– 1 ben ben 59K Jul 23 23:19 c16-143-burgeoning-art.png -rw-rw-r– 1 ben ben 25K Jul 23 23:20 c16-143-burgeoning-art-fs8.png
stripped down is 25k! That’s pretty good! For all 25k cards, that’s only 625MB
For 1k cards that’s only 25MB, much better than Medart! It could be even better if I could separate the palette from the image, but .png doesn’t support that and ChatGPT doesn’t give me any good standard image formats that do this.
Google suggest GIF format, LZW compressed and can have separate palette
Also look into AVIF format (https://en.wikipedia.org/wiki/AVIF)
convert input.png -resize 300x output.png
convert input.png -resize x200 output.png
convert input.png -resize 300x200 output.png
convert input.png -resize 50% output.png
Can use MTG Oracle ID for card id, and keep updated with scryfall
- images: https://scryfall.com/docs/api/images
- cards.json: https://scryfall.com/docs/api/bulk-data
Converted buttons that had black background to transparent background:
convert assets/art/sprites/city/Smbuybttn.png -transparent black assets/art/sprites/city/Smbuybttn.png
Note: why does web version scale up?
Some options for handling scaling
## 1. **Percentage-Based Positioning** Instead of hardcoded pixel positions, use percentages of screen dimensions:
“`go // Instead of: buttonX := 100 buttonY := 50
// Use: buttonX := int(float64(screenW) * 0.1) // 10% from left buttonY := int(float64(screenH) * 0.065) // 6.5% from top “`
## 2. **Anchor-Based Layout System** Create an anchor system for UI elements:
“`go type Anchor int const ( TopLeft Anchor = iota TopCenter TopRight CenterLeft Center CenterRight BottomLeft BottomCenter BottomRight )
type UIElement struct { Anchor Anchor OffsetX, OffsetY int // Offset from anchor point }
func (ui *UIElement) GetPosition(screenW, screenH int) (int, int) { switch ui.Anchor { case TopLeft: return ui.OffsetX, ui.OffsetY case TopCenter: return screenW/2 + ui.OffsetX, ui.OffsetY case Center: return screenW/2 + ui.OffsetX, screenH/2 + ui.OffsetY // … other cases } } “`
## 3. **Responsive Layout Manager** Create a layout manager that handles different screen sizes:
“`go type LayoutManager struct { baseWidth, baseHeight int // Your original 1024x768 }
func (lm *LayoutManager) ScalePosition(x, y, currentW, currentH int) (int, int) { scaleX := float64(currentW) / float64(lm.baseWidth) scaleY := float64(currentH) / float64(lm.baseHeight)
return int(float64(x) * scaleX), int(float64(y) * scaleY) } “`
## 4. **Constraint-Based Layout** Define relationships between UI elements:
“`go type Constraint struct { Element *UIElement RelativeTo *UIElement // nil for screen edges Spacing int Direction Direction // Above, Below, LeftOf, RightOf } “`
## 5. **Grid System** Divide the screen into a grid and position elements within grid cells:
“`go type GridLayout struct { Rows, Cols int }
func (g *GridLayout) GetCellPosition(row, col, screenW, screenH int) (int, int) { cellW := screenW / g.Cols cellH := screenH / g.Rows return col * cellW, row * cellH } “`
## **Recommended Approach for Your Game:**
Given your current setup, I’d suggest starting with **option #2 (Anchor-Based)** combined with **option #3 (Layout Manager)**:
- **Modify your Button struct** to include anchor information
- **Update your screen classes** (CityScreen, BuyCardsScreen, etc.) to use anchored positioning
- **Create a helper function** that converts your current hardcoded positions to anchored ones
This approach:
- Requires minimal changes to existing code
- Provides good flexibility for different screen sizes
- Is easy to understand and maintain
- Works well with your existing UI element structure
Would you like me to show you how to implement the anchor-based system for your specific button and UI setup? Tokens: 9.8k sent, 738 received. Cost: $0.04 message, $0.17 session.
game/game.go game/world/level.go architect> /ask I’d like some abstract representation of pixels that I can use so that I don’t have to pass scale around everywhere, what would that look like? and what are the shortcomings of that approach?
Here’s an abstract pixel representation approach:
## **Virtual Pixel System**
“`go // Virtual pixel unit - always represents the same logical size type VPixel int type VPoint struct { X, Y VPixel } type VRect struct { X, Y, W, H VPixel }
// Global viewport that handles conversion type Viewport struct { baseWidth, baseHeight int // Your reference resolution (1024x768) currentWidth, currentHeight int scale float64 }
var GlobalViewport *Viewport
func (vp *Viewport) ToScreen(v VPixel) int { return int(float64(v) * vp.scale) }
func (vp *Viewport) ToScreenPoint(p VPoint) (int, int) { return vp.ToScreen(p.X), vp.ToScreen(p.Y) }
func (vp *Viewport) FromScreen(pixels int) VPixel { return VPixel(float64(pixels) / vp.scale) }
func (vp *Viewport) UpdateScale(newWidth, newHeight int) { vp.currentWidth = newWidth vp.currentHeight = newHeight vp.scale = math.Min( float64(newWidth)/float64(vp.baseWidth), float64(newHeight)/float64(vp.baseHeight), ) } “`
## **Usage Example**
“`go // Instead of: buttonX := 100 buttonY := 50
// Use: buttonPos := VPoint{X: 100, Y: 50}
// In your draw function: screenX, screenY := GlobalViewport.ToScreenPoint(buttonPos) drawButton(screen, screenX, screenY) “`
## **Integration with Your Button System**
“`go type Button struct { // … existing fields Position VPoint // Instead of X, Y int Size VPoint // Instead of separate width/height }
func (b *Button) Draw(screen *ebiten.Image) { x, y := GlobalViewport.ToScreenPoint(b.Position) w, h := GlobalViewport.ToScreenPoint(b.Size)
opts := &ebiten.DrawImageOptions{} opts.GeoM.Scale(GlobalViewport.scale, GlobalViewport.scale) opts.GeoM.Translate(float64(x), float64(y)) screen.DrawImage(b.Normal, opts) } “`
## **Shortcomings of This Approach**
### **1. Global State Dependency**
- Creates coupling between all UI code and the global viewport
- Makes testing harder (need to initialize global state)
- Can cause issues in multi-threaded scenarios
### **2. Type Safety Issues**
- Easy to accidentally mix VPixel and regular int values
- Compiler won’t catch when you forget to convert
- Can lead to subtle bugs when values get passed around
### **3. Performance Overhead**
- Every pixel access requires a multiplication
- Frequent conversions can add up, especially for complex UI
- Cache misses from extra indirection
### **4. Debugging Complexity**
- Harder to debug layout issues (need to convert values mentally)
- Print statements show virtual pixels, not actual screen positions
- Tools/debuggers show actual pixels, creating disconnect
### **5. Third-Party Library Integration**
- Ebiten and other libraries expect real pixels
- Need conversion at every boundary
- Easy to forget conversions when using library functions
### **6. Precision Loss**
- Repeated conversions can accumulate floating-point errors
- Small VPixel values might round to 0 at small scales
- Can cause alignment issues with fractional scaling
## **Alternative: Context-Based Approach**
“`go type RenderContext struct { scale float64 screenW, screenH int }
func (rc *RenderContext) ScaleX(x int) int { return int(float64(x) * rc.scale) }
// Pass context explicitly instead of global state func (b *Button) Draw(screen *ebiten.Image, ctx *RenderContext) { x := ctx.ScaleX(b.X) y := ctx.ScaleY(b.Y) // … } “`
This avoids global state but requires passing the context around, which you wanted to avoid.
## **Recommendation**
For your game, I’d suggest a **hybrid approach**:
- Use the virtual pixel system for **UI layout and positioning**
- Keep the global viewport for convenience
- Add helper methods to your existing structs to handle conversion
- Use real pixels for performance-critical rendering code
This gives you the convenience without the full overhead everywhere. Tokens: 12k sent, 937 received. Cost: $0.05 message, $0.22 session.