AI: fetch the land colour it is actually short of - #11504
Conversation
|
might be some interesting ideas here but too messy logic should be shared/consolidated around |
|
Consolidated around
Fetching and playing differ in exactly two explicit things: fetching can be for another player, and the pool is a library rather than a hand. Building it that way turned up three bugs in my own first version, all fixed in this push and covered by tests:
Measured on a 40-permanent board: Known limits: a multi-colour source counts once per colour though it makes one mana; |
|
Thank you for taking a look at it! All five, in one push — they turned out to share a cause.
On The two scans are one: the candidate can only add to what's already on the battlefield, so the second set of counts is a clone plus that one card. And One thing worth flagging from doing this. Those
|
| if (ab.getApi() == ApiType.ManaReflected) { | ||
| colors.addAll(CardUtil.getReflectableManaColors(ab)); | ||
| } else { | ||
| colors = CardUtil.canProduce(6, ab, colors); |
There was a problem hiding this comment.
shouldn't this loop offer early exit in case colors is full?
There was a problem hiding this comment.
Added, and in the end in both places.
Within one card it is safe to break because canProduce(6, …) and getReflectableManaColors both draw from COLORS_AND_COLORLESS, so the set cannot exceed six. It earns very little there though — of 1,870 mana-producing cards in the pool, only Plaza of Heroes and White Lotus Hideout still have an ability left to walk once the set is full.
Across sources it is worth much more, but it needed a fix first. getAvailableManaColors was collecting the raw Produced$ string, so its set held Any and Combo ColorIdentity alongside W and there was no size at which it was full. Worse, since every caller runs it through ColorSet.fromNames, which keeps only colour names, an Any source was contributing nothing at all — three City of Brass read as no colours available, and canBePaidWithAvailable then disagreed with ComputerUtilMana.canPayManaCost about a plain {W}.
It now asks getProducibleColors, which resolves those and makes the set bounded, so the break there is both correct and fires on any five-colour board. Thanks for the nudge — I would not have looked at that method otherwise.
49bc361 to
fe85a22
Compare
fe85a22 to
1ff5144
Compare
tool4ever
left a comment
There was a problem hiding this comment.
After further consideration I feel like complicating the logic this way isn't the right path forward since including all the available mana costs still says nothing about if AI would even want to pay them :/
Therefore it could be highly situational if building the mana base simply around missing colors doesn't lead to a better outcome.
Additionally this overlaps with my same argument in #11373 (though arguably a wrong result here isn't as bad as an untap-ramp into nothing) so a clean approach would benefit both.
These PR were still helpful for letting me think about the bigger picture, even if it's sometimes painful to untangle the AI code :P
I might try to cherry pick some of the cleanup done here later...
1ff5144 to
d4bc44e
Compare
|
Rebased onto current master, and pushed a change that I hope answers the objection rather than restating the PR. You were right that counting what the board can produce says nothing about whether the AI would want to pay it. What the counting also cannot say is whether a land lets anything be cast this turn. With one Forest out and Doom Blade
It also takes extra generic mana, because +210 lines over the previous state, four files. Confined to land decisions — Worth being straight about the other half of that: I can measure what this costs but not what it wins. Seeded AI-vs-AI games measure noise at about the same magnitude, so whether better land choice is worth it is not something this harness can settle. The cleanup you mentioned cherry-picking is still here and still independent of all of this. |
|
My PoC: #11673 Further improvements later and in smaller steps. |
Land searches picked by list order, so a fetch that had settled on the right colour still chose its second colour arbitrarily, and the land drop from hand counted mana pips inline. Rank both by one shared measure: what the colours a land adds would let the AI pay for, plus depth in the colours it is thin on. The play path and the search path now ask the same question, and UntapAi's TODO for a colour-aware form of it is answered by the same helper. Rebased onto master with Card-Forge#11673. Its shard bias reads UNPAID_COSTS for the fetching player; basicManaFixing now distinguishes decider from owner, and the lookup follows the owner, since the land ends up with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Card-Forge#11673 biases the basic-land fetch by the shards of costs the AI failed to pay. canPlaySa runs before canPayCost, so a spell with no legal target never reaches payManaCost and never records one. Cover that gap: black cards in hand with nothing to target, and no Plains or Swamps owned, so neither the existing count nor the shard bias can separate the two and the tie falls to BASIC_LANDS order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d4bc44e to
31e8a81
Compare
|
Re-tested on current master with #11673 in. It rebases with one conflict, in the basic-selection loop we both touch, and the two compose — your shard division still runs, this PR only changes which lands reach the loop. One thing needs your call: #11673 reads On overlap, I had assumed #11673 covered the basic-land case and this PR only added non-basics. Measuring it says otherwise. That leaves the two mechanisms complementary rather than overlapping. Yours answers "I tried and could not pay"; this one answers "I can see what I am short of before I try". Added Happy to still cut this down — the non-basic ranking and the land drop from hand are the parts #11673 does not reach, and I can drop the rest. Say which shape you want. Suite 370/0/6, checkstyle clean. |
|
I guess broadly speaking we'd have three main fetch factors to consider: I'd prefer some reasoning first for how these should be connected by default - is it more priority-like based on above order where only ties get attempted to resolve with another heuristic? |
|
Non-AI here: I think the answer is greedy-to hand and then balanced to deck, unless we have longer-term AI plans/needs that we lack:
AI: Only 3 is a genuine tiebreak. 1 and 2 are commensurable (both count spells made payable), so I'd keep them as weights in one scale rather than a strict lexicographic override — otherwise a land unlocking one trivial spell beats a land fixing three colours. For 3, |
Land searches picked by list order.
basicManaFixingchose the basic type the player had fewest of, then tooklist.get(0)from whatever survived that filter;getBestLandAIended inAggregates.random. Neither asked which colours were actually blocking anything.Every fetchland comes through here —
areAllBasics("Plains,Island")is true — and a "Plains" search matches every dual carrying the Plains type. Over 12 seeded AI-vs-AI games (deck 260613, three seeds)basicManaFixingfires 46 times, 44 with a real choice. One observedminType=Islanddecision offered:All carry Island; all differ beside it. It took Tundra because Tundra was first.
The measure
ComputerUtilCard.getColorFixingValue(player, land)is the single number every caller ranks by: how many missing colour sources that land supplies across the player's hand and the activatable abilities on their permanents, plus depth in the colours they are thin on. Counting sources rather than colours is what credits a second Swamp towardsBB, which a colour mask calls payable off a single one.The parts are
countMissingSources,countSourcesFixedandevaluateSpareSources.Colour need is asked before the basic-type count, because that count cannot tell a colour that is missing from one that is merely uncommon: with three Islands and a hand wanting black it concluded it needed Plains — having none — and fetched a Plains-Island.
An "Any" source counted for nothing
Found while answering review here, and folded in because it is the same code path.
getAvailableManaColorscollected the rawProduced$string, and every caller runs that throughColorSet.fromNames, which keeps only colour names — soAnywas dropped entirely:{W}in handChecked against
ComputerUtilMana.canPayManaCostas ground truth: before, the any-colour boards disagreed with it; after, every row agrees and the negatives stay negative. It now asksgetProducibleColors, which resolves the colours and makes the set bounded, so it can also stop once every colour is present.Testing
mvn -pl forge-gui-desktop -am test: 358 tests, 0 failures.Seven tests, sized by mutation rather than by count: removing the depth term, its diminishing returns, the hand scan, the permanent-cost exclusion, the per-colour pip counting, the search narrowing, the tapped-source invariance or the any-colour fix each turns at least one of them red.
One caveat on the numbers above: the 46/44 counts describe the old behaviour and still stand, but the share of picks that change was measured against the first version of the metric and has not been re-run since the scoring changed.
🤖 Implemented with the assistance of Claude Code (Opus 5).