Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
86c2e37
YieldReworkV2 — configurable interrupts, APINA, smart suggestions, se…
MostCromulent May 5, 2026
304456d
Drop APINA state-scanner; route interrupts through yield event handlers
MostCromulent May 5, 2026
f8436a9
Move auto-pass and yield settings to the dock panel
MostCromulent May 6, 2026
d355680
Split stack-yield into interruptible and non-interruptible variants
MostCromulent May 6, 2026
32036f3
Merge branch 'master' into YieldReworkV2
tool4ever May 6, 2026
415d0cb
Fix merge
May 6, 2026
8fa44c9
Clean up start
May 6, 2026
d4ac70c
Clean up start
May 6, 2026
949837c
Clean up start
May 6, 2026
3925c20
Clean up start
May 6, 2026
f53f714
Route end-turn through the SetAutoPassUntilEndOfTurn envelope
MostCromulent May 6, 2026
b3bf969
Seed proxy with full pref set; drop redundant local override stores
MostCromulent May 6, 2026
d50617b
Skip AvailableActions compute in tryAutoPassNow when yielding.
MostCromulent May 7, 2026
4249262
Clean up
May 7, 2026
6ee9452
Fix import
May 7, 2026
4049989
Minor clean up
May 7, 2026
1e7f792
Merge remote-tracking branch 'origin/master' into YieldReworkV2
MostCromulent May 7, 2026
9fc5034
User Guide: split Auto-Yield section; add sidebar entry
MostCromulent May 7, 2026
4b3f169
Don't let APINA skip declare-attackers; treat forced prompt as yield …
MostCromulent May 8, 2026
10c706c
Unify yield-pref wire envelope; gate APINA recompute
MostCromulent May 8, 2026
723a2b4
Remove getters
May 8, 2026
dab3fe3
Clean up obsolete refactor
May 8, 2026
928f870
Unify ESC yield-cancel through Input layer
MostCromulent May 8, 2026
0c23bee
Split yield shortcuts: F2 stop-all, F3 APINA toggle
MostCromulent May 9, 2026
5f34180
Unify yield shortcut: P toggles Auto-Pass or cancels active yields
MostCromulent May 9, 2026
b6f104a
Translate updated lblSHORTCUT_ENDTURN into other locales
MostCromulent May 9, 2026
64b26fc
Refinements
May 9, 2026
8acf9a7
Remove redundancy
May 9, 2026
415c34c
Fix redundant label
May 9, 2026
f34b8ab
Fix redundant label
May 9, 2026
cb74bc8
Clean up
May 9, 2026
b03264c
Drop unused YieldController.resetForNewGame; fix yield threading comment
MostCromulent May 9, 2026
66c72d1
Merge branch 'master' into YieldReworkV2
tool4ever May 10, 2026
52f775e
Merge branch 'master' into YieldReworkV2
tool4ever May 11, 2026
e6cf292
Clean up
May 11, 2026
c3922a0
Fix missing labels
May 11, 2026
f8da52f
Clean up
May 11, 2026
8d35da9
Merge branch 'master' into YieldReworkV2
MostCromulent May 13, 2026
9a8db62
Clear host UI when auto-passing through chooseSpellAbilityToPlay
MostCromulent May 13, 2026
c951dae
Merge branch 'master' into YieldReworkV2
tool4ever May 13, 2026
4608f04
Clean up
May 13, 2026
9a07de0
Add Enable auto-pass checkbox to desktop Game menu
MostCromulent May 13, 2026
484afc5
Merge branch 'master' into YieldReworkV2
tool4ever May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions docs/User-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
- [Easier creature type selection](#easier-creature-type-selection)
- [Auto-Target](#auto-target)
- [Auto-Pay](#auto-pay)
- [Auto-Yield](#auto-yield)
- [Auto-Pass and Yield Options](#auto-pass-and-yield-options)
- [Auto-Yield and Trigger Decisions](#auto-yield-and-trigger-decisions)
- [Shift Key helper](#shift-key-helper)
- [Full Control](#full-control)
- [Repeatable Sequences (Macros)](#repeatable-sequences-macros)
Expand Down Expand Up @@ -189,16 +190,32 @@ When paying mana costs, you can press Enter/Spacebar or click the Auto button in
- You can still manually pay the cost by clicking mana sources in play (e.g. lands) or clicking symbols in your mana pool, which might be a good idea if you want to save specific mana sources for a later play that turn.
- you'll still be prompted when paying Sunburst or cards that care what colors are spent to cast it (ex. Firespout).

## Auto-Yield
- When a spell or an ability appears on the stack and it says "(OPTIONAL)" you can right-click it to decide if you want to always accept or to decline it.
## Auto-Pass and Yield Options
**Yielding** lets you hand priority to Forge so it passes on your behalf instead of you needing to click through every priority pass. This helps you get to your desired phase of action quickly.

It is possible to specify the granularity level for auto-yields: the difference is that, for example, when choosing per ability if you auto-yield to Hellrider's triggered ability once, all triggers from other Hellrider cards will be automatically yielded to as well. When choosing per card, you will need to auto-yield to each Hellrider separately.
Forge offers several yield options depending on how long you want to skip prompts:

Note that in when auto-yielding per ability, yields will NOT be automatically cleared between games in a match, which should speed the game up. When auto-yielding per card, yields WILL be automatically cleared between games because they are dependent on card IDs which change from game to game, thus you will need to auto-yield to each card again in each game of the match.
- **Auto-Pass** — a persistent toggle that automatically yields priority when you have no playable actions. Available on **Desktop** via the Auto-Pass dock icon and the **P** hotkey, or on **Mobile** from the in-match Game menu.
- **End Turn** — auto-pass through the rest of the current turn, bypassing any phase stops. Triggered by the End Turn dock button.
- **Yield markers** — auto-pass until a specific phase is reached. Right-click (or long-press) a phase indicator to set one; a fast-forward symbol marks the active cell. Each (player, phase) cell is independent, so in multiplayer you can yield to a specific opponent's end step.
- **Yield to stack / Resolve entire stack** — auto-pass while the stack resolves. Right-click a stack item to choose: **Yield to stack** auto-passes until the stack empties or an interrupt fires (for example, an opponent casts another spell); **Resolve entire stack** keeps auto-passing until the whole stack is empty even if opponents cast more spells.

- Pressing "End Turn" skips your attack phase and doesn't get cancelled automatically if a spell or ability is put on the stack. You'll still be given a chance to declare blockers if your opponent attacks, but after that the rest of your opponent's turn will then progress without you receiving priority.
> [!NOTE]
> For more information and configuration options — including interrupt conditions, automatic yield suggestions, and speed settings — see [Advanced Yield Options](advanced-yield-options.md).

## Individual Yields and Trigger Decisions
When a spell or an ability appears on the stack you can right-click it to decide if you want to always accept (Always Yes) or always decline it (Always No). For abilities marked "(OPTIONAL)" the same right-click lets you set an auto-yield so you don't get prompted on subsequent activations.

The granularity and lifetime of these decisions are controlled by the **Auto Yield/Trigger Mode** setting under Settings → Preferences:

- **Per Card (Each Game)** — decisions are tied to a specific card instance and cleared at the end of each game. You'll need to set them again in the next game of the match. (For example, auto-yielding one Hellrider does not affect another copy of Hellrider.)
- **Per Ability (Each Match)** — decisions apply to every copy of the ability and persist across games within the current match, then clear when you start a new match.
- **Per Ability (Each Session)** — decisions persist across matches until you close Forge.
- **Per Ability (Each Install)** — decisions are saved to disk and persist across Forge restarts.

Pick a longer-lived scope when you want recurring triggers (e.g. routine ETBs, upkeep optional triggers) to stay yielded across many games; pick a shorter scope when you want a clean slate each game.

To alleviate pressing this accidentally, as long as you're passing this way, you'll be able to press Escape or the Cancel button to be given the chance to act again. Phases with stops and spells/abilities resolving will be given a slight delay to allow you to see what's going on.
The current list of active auto-yields and Always Yes / Always No trigger decisions is visible from Game → Auto-Yields and Triggers, where individual entries can be cleared.

## Shift Key helper
* When you mouse over a flip, transform or Morph (controlled by you) card in battlefield, hold SHIFT to see other state of that card at the side panel that displays card picture and details.
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [AI](ai.md)
- [Network Play](network-play.md)
- [Advanced search](Advanced-Search.md)
- [Advanced Yield Options](advanced-yield-options.md)

- Adventure Mode

Expand Down
125 changes: 125 additions & 0 deletions docs/advanced-yield-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
## Contents
- [How to Access](#how-to-access)
Comment thread
tool4ever marked this conversation as resolved.
- [Auto-Pass](#auto-pass)
- [Yield markers](#yield-markers)
- [Yield Settings Menu](#yield-settings-menu)
- [Yield Interrupt Settings](#yield-interrupt-settings)
- [Automatic Yield Suggestions](#automatic-yield-suggestions)
- [Speed Options](#speed-options)
- [Troubleshooting](#troubleshooting)
- [Network Play](#network-play)

# Advanced Yield Options
The standard priority system in Forge can involve dozens of priority passes every turn. This can can slow down the game, particularly in multiplayer games like Commander, where one player's delay responding to priority halts the game for everybody else.

Forge offers a range of **Advanced Yield Options** to:

- enable players to automatically yield when there is no available action they can take.
- give players the ability to yield until a specific phase is reached, without responding to priority passes in the meantime.
- configure yield interrupt conditions, so you'll always get control back when something important happens (e.g. you are attacked or targeted by a spell).
- provide smart suggestions to enable yield if there are no useful actions you can take (e.g. it is another player's turn and you have no mana or playable cards).

These features are highly configurable through the **Yield Settings** dialog, and can be set up to suit your own gameplay preferences.

## Auto-Pass

**Auto-Pass** is a persistent toggle (**P** on desktop, or the Auto-Pass icon on the dock) that automatically passes priority whenever you have no playable actions available. It's the simplest way to speed up games where you often have nothing to do — enable it once and Forge stops asking for input you'd only use to pass. The same **P** key also doubles as a "stop everything" shortcut: if any yield is currently active (transient yield or Auto-Pass itself), pressing P clears them all in one shot without dismissing any prompt that's up.

**How it works:**
- When enabled, Forge scans your hand, battlefield, and external zones (graveyard, exile, command) for castable spells, playable lands, and activatable abilities.
- If you have any available action, you keep priority as usual.
- If you have no available action, Forge passes priority on your behalf without prompting.
- On desktop, the Auto-Pass dock icon's background lights up gold while active. On mobile, the menu entry text reads `Auto-Pass: ON` / `Auto-Pass: OFF`.

**Interaction with interrupts:** By default, Auto-Pass ignores your interrupt settings — it keeps passing as long as you have no actions, regardless of attackers, opponent spells, mass-removal, etc. Enable **Auto-pass respects interrupts** in the Yield Interrupt Settings section if you want interrupts to break Auto-Pass too.

**Persistence:** Unlike yield markers, Auto-Pass does not end on a game event. It stays active until you toggle it off.

**Performance and timeout:** The action-availability scan can be expensive in complex board states. The scan is subject to the **Auto-pass calculation timeout** setting in the Yield Settings dialog. On timeout the system prompts you instead of auto-passing, so a false positive means an extra prompt rather than a long stall. The default is **Dynamic** — the budget scales with the number of playable cards (approximately 50ms per card, clamped between 50ms and 1500ms). Set your own value in the Yield Settings dialog to override.

> [!NOTE]
> **The Auto-pass AI is not perfect.** It is designed to avoid false negatives (passing priority when there is action you can take) as much as possible. There may be times it produces a false positive (giving you priority when there is nothing you can do). Use with appropriate caution.

## Yield markers

A **yield marker** tells Forge to auto-pass priority until a specific phase is reached. Markers are set directly on the phase indicator strip in the match UI.

**Setting a marker:**
- **Desktop:** right-click the phase indicator cell for the phase you want to yield to.
- **Mobile:** long-press the phase indicator cell.

A fast-forward symbol will appear on the targeted cell to show the marker is active. The prompt area also describes what phase you are yielding to. Forge then auto-passes priority on your behalf until that phase is reached, at which point the marker clears automatically and you regain priority.

**Per-(player, phase) precision:** Each phase indicator cell is distinct per player. Right-clicking your own End Step yields to *your* End Step; right-clicking an opponent's End Step yields to *that opponent's* End Step. In multiplayer (e.g. four-player Commander) this lets you express things like "yield until that opponent's End Step" without affecting how you respond to the other opponents' end steps.

**Cancelling:**
- Right-click (or long-press) the marker again to cancel it.
- Press **ESC** (desktop) to cancel any active marker.
- An enabled interrupt firing (see [Yield Interrupt Settings](#yield-interrupt-settings)) cancels the marker and hands priority back to you.

**Re-targeting:** Right-clicking (or long-pressing) a different phase indicator while a marker is active moves the marker to the new cell. Only one marker is active at a time.

## Yield Settings Menu

The **Yield Settings** dialog is the central configuration UI for yield behavior. It's accessible from:
- **Desktop:** the cog button on the dock, the Game menu > **Yield Settings** entry, or Ctrl+Y.
- **Mobile:** Game menu > **Yield Options**.

The dialog has three sections:

### Yield Interrupt Settings

Yield markers, end-of-turn yield, and the interruptible **Yield to stack** automatically cancel when important game events occur. The non-interruptible **Resolve entire stack** is exempt — its purpose is to watch the stack resolve to completion, so opponent spells hitting the stack do not cancel it.

You can decide which game events interrupt a yield:

| Interrupt | Default | Description |
|-----------|---------|-------------|
| **Attackers declared against you** | ON | Triggers when creatures attack you specifically (not when other players are attacked). |
| **Opponent casts any spell** | ON | Triggers on opponent spells and activated abilities (not triggered abilities). |
| **You or your permanents targeted** | OFF | Triggers when a spell/ability targets you or something you control. |
| **Mass removal spell cast** | OFF | Triggers when an opponent casts a board wipe (DestroyAll / DamageAll / SacrificeAll / ChangeZoneAll spell). |
| **Triggered abilities on stack** | OFF | Triggers when triggered abilities are on the stack. |
| **Cards revealed or choices made** | OFF | Triggers when reveal dialogs / non-trivial value notifications fire. |

### Automatic Yield Suggestions

When the system detects situations where you likely cannot take action, it can prompt you with a yield suggestion. Each suggestion type has a dropdown controlling its decline behavior:

| Suggestion | When it appears | Suggested action | Decline scope options |
|------------|-----------------|------------------|-----------------------|
| **Can't respond to stack** | You have no instant-speed responses available | Yield to stack (interruptible — auto-pass until stack empties or an interrupt fires) | Never (default) / Always / Once per stack / Once per turn |
| **No actions available** | No playable cards or activatable abilities (not your turn, stack empty) | Yield to your next turn | Never (default) / Always / Once per turn |

**Decline scope options:**
- **Never:** suggestion is disabled entirely (never shown).
- **Always:** suggestion re-appears on the next priority pass, even if just declined.
- **Once per stack:** declining suppresses the suggestion until the current stack resolves. A new stack will re-prompt. (Only available for "Can't respond to stack".)
- **Once per turn:** declining suppresses the suggestion for the rest of the current turn.

In addition to the per-suggestion dropdowns, this section contains two global suppression toggles:

- **Suppress on own turn** (default ON): suppress suggestions during your own turn, when you typically want to take actions. Suggestions are always suppressed on your first turn regardless of this setting, since you won't have any lands or mana yet.
- **Suppress immediately after yield ends** (default ON): suppress suggestions for one priority pass when a yield expires or is interrupted, giving you time to assess the game state before deciding whether to re-yield.

### Speed Options

- **Skip delay between phases:** skip Forge's default 200ms delay between each phase resolving.
- **Skip delay when stack resolves:** skip Forge's default 400ms delay between items on the stack resolving.

## Troubleshooting

### Yield marker doesn't appear when right-clicking / long-pressing a phase indicator
- Markers cannot be set during pre-game, mulligan, or cleanup phases.

### Yield clears unexpectedly
- Check interrupt settings in the Yield Settings dialog.
- A marker also clears automatically the moment its target phase is reached.

### Smart suggestions not appearing
- Verify the suggestion's decline scope is not set to "Never" in the Yield Settings dialog.
- Suggestions don't appear if you're already yielding.
- If you declined a suggestion, check the decline scope to understand when it will re-appear.

## Network Play
- Each player controls their own yield preferences. Your yield marker, stack-yield state, interrupt settings, and decline-scope choices apply to you only and propagate across the network — they do not affect other players. The host does not impose its own preferences on connected clients.
3 changes: 1 addition & 2 deletions forge-ai/src/main/java/forge/ai/AiCostDecision.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ public PaymentDecision visit(CostExile cost) {
CardCollection valid = CardLists.getValidCards(player.getGame().getCardsIn(cost.getFrom().get(0)), typeCleaned, player, source, ability);
CardCollection chosen = new CardCollection();

CardLists.sortByCmcDesc(valid);
Collections.reverse(valid);
valid.sort(CardLists.CmcComparator);

int totalCMC = 0;
for (Card card : valid) {
Expand Down
75 changes: 75 additions & 0 deletions forge-ai/src/main/java/forge/ai/AvailableActions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package forge.ai;

import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import org.tinylog.Logger;

import java.util.stream.Collectors;

// Heuristic: does the player have any playable action this priority window?
// Bounded by timeoutMs; returns true on expiry (false-positive — player is prompted).
public final class AvailableActions {

private AvailableActions() {}

public static boolean compute(Player player, long timeoutMs) {
long deadlineNanos = System.nanoTime() + timeoutMs * 1_000_000L;

for (Card card : sortedCardsIn(player, ZoneType.Hand)) {
for (SpellAbility sa : card.getAllPossibleAbilities(player, true)) {
if (checkTimeout(deadlineNanos, timeoutMs)) return true;
if (sa.isSpell()) {
if (canAfford(sa, player) && ComputerUtilAbility.isFullyTargetable(sa)) {
return true;
}
} else if (sa.isLandAbility()) {
return true;
}
}
}

// Not sorted: activation costs are per-ability, not the permanent's CMC.
for (Card card : player.getCardsIn(ZoneType.Battlefield)) {
for (SpellAbility sa : card.getAllPossibleAbilities(player, true)) {
if (checkTimeout(deadlineNanos, timeoutMs)) return true;
if (!sa.isManaAbility() && canAfford(sa, player) && ComputerUtilAbility.isFullyTargetable(sa)) {
return true;
}
}
}

for (Card card : sortedCardsIn(player, ZoneType.Flashback)) {
for (SpellAbility sa : card.getAllPossibleAbilities(player, true)) {
if (checkTimeout(deadlineNanos, timeoutMs)) return true;
if (!sa.isManaAbility() && canAfford(sa, player) && ComputerUtilAbility.isFullyTargetable(sa)) {
return true;
}
}
}

return false;
}

// Sort cheap cards first so cheap-to-validate matches early-exit
private static Iterable<Card> sortedCardsIn(Player player, ZoneType zone) {
return player.getCardsIn(zone).stream().sorted(CardLists.CmcComparator).collect(Collectors.toList());
}

private static boolean canAfford(SpellAbility sa, Player player) {
if (sa.getPayCosts() == null || !sa.getPayCosts().hasManaCost()) {
return true;
}
return ComputerUtilMana.canPayManaCost(sa, player, 0, false);
}

private static boolean checkTimeout(long deadlineNanos, long timeoutMs) {
if (System.nanoTime() < deadlineNanos) {
return false;
}
Logger.warn("AvailableActions: heuristic timed out after {}ms; returning true.", timeoutMs);
return true;
}
}
4 changes: 1 addition & 3 deletions forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,7 @@ public static CardCollection chooseCollectEvidence(final Player ai, CostCollectE

if (CardLists.getTotalCMC(typeList) < amount) return null;

// FIXME: This is suboptimal, maybe implement a single comparator that'll take care of all of this?
CardLists.sortByCmcDesc(typeList);
Collections.reverse(typeList);
typeList.sort(CardLists.CmcComparator);

// TODO AI needs some improvements here
// What's the best way to choose evidence to collect?
Expand Down
Loading
Loading