Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
9e65d29
Add experimental yield system for reduced multiplayer micromanagement
MostCromulent Jan 28, 2026
b47d81a
Add preferences GUI toggle for experimental yield options
MostCromulent Jan 28, 2026
5f4d7c8
Fix experimental yield system bugs and improve UX
MostCromulent Jan 28, 2026
284c10a
Fix multiplayer yield issues and simplify yield logic
MostCromulent Jan 28, 2026
f5f7287
Add PR documentation for experimental yield system
MostCromulent Jan 28, 2026
17b8822
Improve yield system integration and fix End Turn behavior
MostCromulent Jan 28, 2026
35a7c3f
Add authorship section to PR documentation
MostCromulent Jan 28, 2026
34fc365
Add new yield modes and F-key hotkeys for yield system
MostCromulent Jan 29, 2026
9595faf
Fix yield timing for End Step and Your Turn buttons
MostCromulent Jan 29, 2026
2eb6bc0
Add auto-suppress for declined suggestions and bug fixes
MostCromulent Jan 29, 2026
43b3898
Add mass removal interrupt option for yield system
MostCromulent Jan 29, 2026
59086a6
Add Yield Until Next Phase mode with dynamic hotkey display
MostCromulent Jan 29, 2026
57466cd
Comprehensively update DOCUMENTATION.md for accuracy and completeness
claude Jan 29, 2026
b100b93
Merge pull request #12 from MostCromulent/claude/update-documentation…
MostCromulent Jan 29, 2026
aeed733
Remove redundant PR documentation file
MostCromulent Jan 30, 2026
e1104e6
Fix targeting interrupt not detecting sub-ability targets
MostCromulent Jan 30, 2026
85dbd6a
Remove duplicate hasAvailableActions function
MostCromulent Jan 30, 2026
9211d97
Fix yield system for multiplayer non-host players
MostCromulent Jan 30, 2026
1f9e92e
Network-safe smart suggestions and yield button fixes
MostCromulent Jan 30, 2026
c21416e
Fix yield sync recursion and smart suggestion mana checking
MostCromulent Jan 31, 2026
6ddc7f6
Fix yield panel disappearing on layout refresh and improve 2-player l…
MostCromulent Jan 31, 2026
4b8c1b1
Add Expanded Yield Options wiki documentation
MostCromulent Jan 31, 2026
98f458b
Refactor PlayerView lookup to reuse existing method
MostCromulent Jan 31, 2026
b7442b0
Shift yield hotkeys from F1-F6 to F2-F7 to avoid F1=Help conflict
MostCromulent Jan 31, 2026
47947e3
Disable smart yield suggestions on mobile GUI
MostCromulent Jan 31, 2026
b586166
Add toggle behavior for yield buttons
MostCromulent Jan 31, 2026
bf9910b
Consolidate yield state tracking into YieldState class
MostCromulent Jan 31, 2026
cbcd373
Make hasAvailableActions computation conditional on experimental yields
MostCromulent Feb 1, 2026
9db3049
Add toggles for suggestion suppression behavior
MostCromulent Feb 1, 2026
925c37c
Remove DOCUMENTATION.md (moved to NetworkPlay/dev)
MostCromulent Feb 1, 2026
8cdeacd
Move yield computation to PlayerView and query controller for prefere…
MostCromulent Feb 1, 2026
c5b154d
Add preference guard to updateWillLoseManaAtEndOfPhase
MostCromulent Feb 1, 2026
11e15be
Remove right-click yield menu and simplify yield button layout
MostCromulent Feb 7, 2026
8057365
Clean up yield rework: remove dead code and fix shortcut filter
MostCromulent Feb 7, 2026
c7074ed
Merge branch 'master' into YieldRework
MostCromulent Feb 9, 2026
2b3ec77
Merge branch 'master' into YieldRework
tool4ever Feb 12, 2026
87262c2
Avoid single use utility method that only grows the code
Feb 12, 2026
5384ed4
Avoid single use utility method that only grows the code
Feb 12, 2026
b8d2591
Clean style
Feb 12, 2026
83cc646
Merge branch 'master' into YieldRework
tool4ever Feb 12, 2026
9b41c5d
Address PR review feedback: use passPriority() and remove getPlayerCo…
MostCromulent Feb 12, 2026
0846e01
Apply experimental yield options preference immediately without restart
MostCromulent Feb 14, 2026
a089269
Merge upstream/master into YieldRework
MostCromulent Feb 18, 2026
c1375c9
Remove dynamic keyboard shortcut text from yield UI
MostCromulent Feb 18, 2026
64af30b
Simplify yield system: remove over-engineering from IGuiGame
MostCromulent Feb 19, 2026
974102a
Add "interrupt on triggered abilities" yield setting
MostCromulent Feb 27, 2026
af263d7
Merge remote-tracking branch 'upstream/master' into YieldRework
MostCromulent Feb 27, 2026
813fe83
Make Yield Options menu always visible with Ctrl+Y toggle
MostCromulent Feb 27, 2026
84996a7
Add auto-pass when no available actions (F8 toggle)
MostCromulent Feb 27, 2026
f63f2e3
Improve available actions heuristic for auto-pass
MostCromulent Feb 27, 2026
3e5ea56
Add network transparency for yield mode host/client mismatch
MostCromulent Feb 27, 2026
62114a4
Merge remote-tracking branch 'upstream/master' into YieldRework
MostCromulent Feb 27, 2026
213cb44
Add VYieldSettings dialog with per-suggestion scope dropdowns
MostCromulent Mar 1, 2026
c5f4cd8
Reorder yield shortcuts to match panel layout, update wiki docs
MostCromulent Mar 1, 2026
bd8343b
Add network sync for auto-yield and trigger accept/decline
MostCromulent Mar 2, 2026
f8cdfb6
Fix multiple yield system bugs
MostCromulent Apr 8, 2026
519701a
Revert yield "network-safe" refactor; immutable YieldState
MostCromulent Apr 9, 2026
b9fcc20
Trim IGuiGame yield interface surface
MostCromulent Apr 9, 2026
1be8ec2
Sync remote-player yield prefs to host
MostCromulent Apr 9, 2026
4846a37
Replace magic-int trigger choice with TriggerChoice enum
MostCromulent Apr 9, 2026
4f65f84
Polish: consolidate yield F-key shortcuts and clean up stale notes
MostCromulent Apr 9, 2026
409c955
Redesign yield panel layout; add Before Your Turn mode; fix auto-pass
MostCromulent Apr 9, 2026
464ad27
Merge origin/master into YieldRework
MostCromulent Apr 9, 2026
db9c4e9
Address yield feedback: APINA attackers fix, remove redundant interru…
MostCromulent Apr 9, 2026
b0c1fa1
Merge master into YieldRework, drop obsolete TriggerChoice
MostCromulent Apr 17, 2026
2ca8907
Refactor yield prefs to controller-owns-prefs (PR #10355 pattern)
MostCromulent Apr 17, 2026
b9e7cb7
Extract auto-pass heuristic to forge-ai/AvailableActions; sync result
MostCromulent Apr 17, 2026
1a08dea
Update documentation
MostCromulent Apr 17, 2026
e9cae64
Improve heuristic performance and accuracy
MostCromulent Apr 18, 2026
70cb7e7
AvailableActions: use ComputerUtilAbility.isFullyTargetable
MostCromulent Apr 20, 2026
8b30bca
Reset YieldController state between games
MostCromulent Apr 25, 2026
7cfc89f
Yield modes never clear at their stop point when APINA is on
MostCromulent Apr 25, 2026
5676429
Merge remote-tracking branch 'origin/master' into YieldRework
MostCromulent Apr 25, 2026
3a3fc16
Tighten available-actions scan gating
MostCromulent Apr 26, 2026
9a1b18c
Right-click phase indicator to set a yield-until-phase marker
MostCromulent Apr 27, 2026
2a9bc58
Yield to entire stack from stack item context menu
MostCromulent Apr 27, 2026
8a0eb27
Auto-pass-no-actions ignores interrupts by default
MostCromulent Apr 27, 2026
70e2b11
Merge remote-tracking branch 'upstream/master' into YieldRework
MostCromulent Apr 27, 2026
fa7766f
Restore declareAttackers legality check; import ForgePreferences
MostCromulent Apr 28, 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
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
148 changes: 148 additions & 0 deletions docs/advanced-yield-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Advanced Yield Options
The standard priority system in Forge can involve dozens of priority passes every turn. This can cause frustration, particularly in multiplayer Magic games like Commander, where one player's delay responding to priority can slow down the game for everybody else.

**Advanced Yield Options** is an experimental feature that significantly expands the legacy Forge auto-pass system through:

- enabling players to automatically yield when there is no available action they can take.
- giving players the ability to yield until a specific phase is reached, without responding to priority passes in the meantime.
- configurable yield interrupt conditions, so you'll always get control back when something important happens (e.g. you are attacked or targeted by a spell).
- smart suggestions for you 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.

**Note:** This feature is disabled by default and must be explicitly enabled in preferences.

## How to Enable

- **Anywhere:** open Gameplay Settings > Preferences > **Enable Advanced Yield Options**.
- **In a match (desktop):** open the Game menu > **Yield Options** > **Enable Advanced Yield Options**, or press the hotkey (default Ctrl+Y).
- **In a match (mobile):** open the in-match Game menu and toggle the same option from there.

The change takes effect immediately — no restart required.

## Once Enabled

- The **Yield Options** panel (desktop) exposes the persistent **Auto-Pass** toggle and a **Settings** button that opens the Yield Settings dialog.
- On **mobile**, two equivalent entries appear in the in-match Game menu (below the existing **Auto-Yields** entry): **Yield Options** (opens the dialog) and **Auto-Pass: ON / OFF** (toggle).
- **Right-click** any phase indicator (desktop) or **long-press** it (mobile) to set a yield marker on that phase — see [Setting Yield Markers](#setting-yield-markers) below.
- Smart suggestions begin appearing in the prompt area (see [Automatic Yield Suggestions](#automatic-yield-suggestions)).

## Auto-Pass

**Auto-Pass** is a persistent toggle (F2 on desktop, or the Auto-Pass button) 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.

**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.
- The button label reflects the state (`Auto-Pass: ON` / `Auto-Pass: OFF`).

**Interaction with interrupts:** Auto-Pass respects the interrupt settings in the Yield Settings dialog. Even if you have no actions, you will still be prompted when an interrupt condition fires — for example, when creatures attack you or when a mass-removal spell is cast.

**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.

**Setting a marker passes current priority** and starts auto-passing toward the marked phase. If you didn't want to pass priority, cancel with right-click/long-press or ESC.

## Hotkeys (desktop)

| Hotkey | Action |
|--------|--------|
| **F2** | Toggle Auto-Pass |
| **ESC** | Cancel any active yield marker or stack yield |
| **Ctrl+Y** | Toggle the **Advanced Yield Options** feature flag |

All hotkeys can be modified from the in-game hotkeys menu (press H by default). Mobile uses Game-menu entries instead of hotkeys.

## Yield Settings Menu

The **Yield Settings** menu is the central configuration UI for advanced yield behavior. It's accessible from:
- **Desktop:** the Settings button on the Yield Options panel, or Game menu > **Yield Options** > **Yield Settings**.
- **Mobile:** Game menu > **Yield Options**.

The dialog has four sections:

### Yield Interrupt Settings

Yield markers and stack-yield automatically cancel when important game events occur. Each interrupt can be individually toggled:

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

**Multiplayer note:** The attackers interrupt is scoped to you specifically. If Player A attacks Player B, your yield will not be interrupted.

### Automatic Yield Suggestions

When the system detects situations where you likely cannot take action, it prompts you with a yield suggestion in the prompt area, with Accept/Decline buttons. 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 | Stack yield (auto-pass until stack empties) | Never / Always / Once per stack (default) / Once per turn |
| **No actions available** | No playable cards or activatable abilities (not your turn, stack empty) | Yield to your next turn | Never / Always / Once per turn (default) |

**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.

### Suppression Options

- **Suppress on own turn:** by default, suggestions are suppressed on your own turn since you typically want to take actions during your turn. 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:** by default, suggestions are suppressed 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

- **Auto-pass calculation timeout:** The amount of time in milliseconds the AI has to calculate whether you have any available actions and whether you should auto-pass. If the timeout is reached auto-pass will return false and hand you priority as a safeguard. The default is **Dynamic** — the budget scales with the number of playable cards (approximately 50ms per card, clamped between 50ms and 1500ms).
- **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
- Verify **Advanced Yield Options** is enabled in preferences.
- 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.
- Suggestions only appear when Advanced Yield Options are enabled.

## Network Play

- The host must have **Advanced Yield Options** enabled for clients to use them. If the host does not have the option enabled, a warning is posted in the chat window and the client's yield controls are disabled.
- Each player controls their own yield preferences. Your yield marker, stack-yield state, and interrupt settings apply to you only and take effect across the network — they do not affect other players.
83 changes: 83 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,83 @@
package forge.ai;

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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

// 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 static final Comparator<Card> BY_CMC_ASC = Comparator.comparingInt(Card::getCMC);

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) {
Iterable<Card> cards = player.getCardsIn(zone);
List<Card> copy = new ArrayList<>();
cards.forEach(copy::add);
if (copy.size() < 2) return copy;
copy.sort(BY_CMC_ASC);
return copy;
}

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;
}
}
28 changes: 28 additions & 0 deletions forge-game/src/main/java/forge/game/player/PlayerView.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.GameEntityView;
import forge.game.GameView;
import forge.game.card.Card;
import forge.game.card.CardView;
import forge.game.card.CounterType;
Expand Down Expand Up @@ -44,6 +45,24 @@ public static TrackableCollection<PlayerView> getCollection(Iterable<Player> pla
return collection;
}

/**
* Look up a PlayerView by ID from the given GameView's player list. Used for
* network play where deserialized PlayerViews have different trackers than
* the host's GameView. Falls back to the input PlayerView if no match is
* found, or if the GameView is null.
*/
public static PlayerView findById(GameView gv, PlayerView player) {
if (player == null) return null;
if (gv == null) return player;
int id = player.getId();
for (PlayerView pv : gv.getPlayers()) {
if (pv.getId() == id) {
return pv;
}
}
return player;
}

public PlayerView(final int id0, final Tracker tracker) {
super(id0, tracker);

Expand Down Expand Up @@ -522,6 +541,15 @@ void updateMana(Player p) {
set(TrackableProperty.Mana, mana);
}

public boolean hasAvailableActions() {
return get(TrackableProperty.HasAvailableActions);
}

// Per-SA "playable" markers can be added as sibling properties without changing this signature.
public void setHasAvailableActions(boolean value) {
set(TrackableProperty.HasAvailableActions, value);
}

private List<String> getDetailsList() {
final List<String> details = Lists.newArrayListWithCapacity(8);
details.add(Localizer.getInstance().getMessage("lblLifeHas", getLife()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ public enum TrackableProperty {
HasPriority(TrackableTypes.BooleanType, FreezeMode.IgnoresFreeze),
AvatarLifeDifference(TrackableTypes.IntegerType, FreezeMode.IgnoresFreeze),
HasLost(TrackableTypes.BooleanType),
HasAvailableActions(TrackableTypes.BooleanType),

//SpellAbility
HostCard(TrackableTypes.CardViewType),
Expand Down
Loading
Loading