Skip to content

Commit 316efde

Browse files
MostCromulentclaude
andcommitted
Merge branch 'master' into network-draft-sealed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents 21d1168 + 3395c61 commit 316efde

176 files changed

Lines changed: 3983 additions & 1516 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/Development/ownership.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ Please also try to check back a bit after your PR gets merged in case it causes
55
## DevOps
66
| Concept | Owners | Ancestors | Example tasks |
77
| - | - | - | - |
8-
| PC Releases | friarsol | Agetian | - update Maven dependencies |
8+
| PC Releases | friarsol | Agetian | - update Maven dependencies<br>- maintain CI files |
99
| Android Releases | | kevlahnota | |
10-
| Sentry | JaminCollins | | - watch for rare/unusual crashes |
10+
| Sentry | JaminCollins | | - watch trends for rare/unusual crashes |
1111

1212
## Ingame Engine
1313
| Concept | Owners | Ancestors | Example tasks |
@@ -26,7 +26,7 @@ Please also try to check back a bit after your PR gets merged in case it causes
2626
## User Interface
2727
| Concept | Owners | Ancestors | Example tasks |
2828
| - | - | - | - |
29-
| Desktop | | | |
29+
| Desktop | | | - performance profiling |
3030
| Android | | DrDev, kevlahnota | - test new libGDX versions |
3131
| Localization | | Alumi | - update card translation files<br>- update engine text (native speaker not required) |
3232
| Sound effects | | | |
@@ -41,7 +41,7 @@ Please also try to check back a bit after your PR gets merged in case it causes
4141
| Concept | Owners | Ancestors | Example tasks |
4242
| - | - | - | - |
4343
| Quest | friarsol | | |
44-
| [Network Play](../network-play.md) | | JaminCollins | |
44+
| [Network Play](../network-play.md) | MostCromulent | JaminCollins | |
4545
| Gauntlet | | | |
4646
| Draft | | | |
4747
| Planar Conquest | | DrDev | |
@@ -61,4 +61,4 @@ Please also try to check back a bit after your PR gets merged in case it causes
6161
## Miscellaneous
6262
| Concept | Owners | Ancestors | Example tasks |
6363
| - | - | - | - |
64-
| Documentation | | | - update Wiki |
64+
| Documentation | TRT | | - update Wiki |

docs/Network-Play.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,22 @@
4545
4646
1. **Configure network** — Host must configure network settings to enable external connections (see [Network Configuration](#network-configuration) below).
4747
2. **Verify versions** — Confirm all devices are running the same Forge version (see [Version Compatibility](#version-compatibility) below).
48-
3. **Launch Forge** on all devices.
48+
3. **Launch Forge** on all devices and navigate to the online play screen.
4949
- Mobile: Choose "Classic Mode", then "Play Online"
50-
- Desktop: "Online Multiplayer" > "Lobby" > "Connect to Server"
51-
4. **Host** leaves the server address field **empty** and clicks OK.
50+
- Desktop: "Online Multiplayer" > "Lobby"
51+
4. **Host** clicks **"Host a Game"** to start the server.
5252
- On first host, Forge will ask whether to **automatically open the port via UPnP** (see [UPnP](#upnp-automatic-port-forwarding) below). If your router supports UPnP, choosing "Just Once" or "Always" can skip manual port forwarding entirely.
5353
5. **Host** determines address to share with clients:
5454
- **Local play:** Use the **Copy Local URL** button in the lobby — this copies the address in the correct format. Forge displays the host's IP (typically `192.168.x.x`). Verify against the device's network settings. Ignore any suggestion to use `localhost`.
5555
- **Remote play:** Use the **Copy External URL** button in the lobby — if necessary verify the external IP at [canyouseeme.org](http://canyouseeme.org).
56-
7. **Client** enters the host's IP address in the connection dialog and clicks OK.
56+
6. **Client** clicks **"Join a Game"** and enters the host's address.
5757
- The address format is **`IP:port`** — for example: `192.168.1.50:36743` (local) or `203.0.113.45:36743` (remote).
5858
- If the port is omitted, Forge defaults to 36743 (=FORGE on older phone keypads).
59-
8. **Configure the match:**
59+
7. **Configure the match:**
6060
- Host selects match type, teams, and game settings.
6161
- All players select decks, sleeves, and avatars.
6262
- Each player toggles their **Ready** switch.
63-
9. **Host starts the match** once all players are ready.
63+
8. **Host starts the match** once all players are ready.
6464

6565
---
6666

docs/User-Guide.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
- [Shift Key helper](#shift-key-helper)
2121
- [Full Control](#full-control)
2222
- [Repeatable Sequences (Macros)](#repeatable-sequences-macros)
23-
- [User Interface](#user-interface)
23+
- [Desktop User Interface](#desktop-user-interface)
2424
- [Layout](#layout)
2525
- [Viewing Cards in Different Zones](#viewing-cards-in-different-zones)
2626
- [Auto-Sort Multiplayer Fields](#auto-sort-multiplayer-fields)
@@ -34,8 +34,8 @@
3434

3535
[***CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)***](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
3636

37-
* For desktop, grab the installer file that ends in .jar
38-
* For android, grab the android file that ends in .apk
37+
* For Desktop, grab the installer file that ends in .jar
38+
* For Android, grab the app file that ends in .apk
3939
&dash; Watch the screen recording if one of following steps isn't clear for you
4040

4141
<https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8>

forge-ai/src/main/java/forge/ai/AiController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ private CardCollection filterLandsToPlay(CardCollection landList) {
435435
}
436436
}
437437

438+
landList = ComputerUtilCard.dedupeCards(landList);
439+
438440
landList = CardLists.filter(landList, c -> {
439441
String name = c.getName();
440442
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
@@ -473,8 +475,6 @@ private Card chooseBestLandToPlay(CardCollection landList) {
473475
return null;
474476
}
475477

476-
landList = ComputerUtilCard.dedupeCards(landList);
477-
478478
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
479479

480480
// Some considerations for Momir/MoJhoSto
@@ -2167,9 +2167,9 @@ public boolean chooseEvenOdd(SpellAbility sa) {
21672167
String aiLogic = sa.getParamOrDefault("AILogic", "");
21682168

21692169
if (aiLogic.equals("AlwaysEven")) {
2170-
return false; // false is Even
2170+
return false;
21712171
} else if (aiLogic.equals("AlwaysOdd")) {
2172-
return true; // true is Odd
2172+
return true;
21732173
} else if (aiLogic.equals("Random")) {
21742174
return MyRandom.getRandom().nextBoolean();
21752175
} else if (aiLogic.equals("CMCInHand")) {

forge-ai/src/main/java/forge/ai/ComputerUtil.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package forge.ai;
1919

2020
import com.google.common.collect.*;
21+
2122
import forge.ai.AiCardMemory.MemorySet;
2223
import forge.ai.ability.ProtectAi;
2324
import forge.ai.ability.TokenAi;
@@ -412,7 +413,7 @@ else if (pref.contains("DiscardCost")) { // search for permanents with DiscardMe
412413
int mana = ComputerUtilMana.getAvailableManaEstimate(ai, false);
413414

414415
boolean cantAffordSoon = activate.getCMC() > mana + 1;
415-
boolean wrongColor = !activate.getColor().hasNoColorsExcept(ColorSet.fromNames(ComputerUtilCost.getAvailableManaColors(ai, ImmutableList.of())).getColor());
416+
boolean wrongColor = !activate.getColor().hasNoColorsExcept(ColorSet.fromNames(ComputerUtilCost.getAvailableManaColors(ai, List.of())).getColor());
416417

417418
// Only do this for spells, not activated abilities
418419
// We can't pay for this spell even if we play another land, or have wrong colors
@@ -2340,7 +2341,7 @@ public static String chooseSomeType(Player ai, String kindOfType, SpellAbility s
23402341
final String logic = sa.getParam("AILogic");
23412342

23422343
if (validTypes == null) {
2343-
validTypes = ImmutableList.of();
2344+
validTypes = List.of();
23442345
}
23452346

23462347
final Game game = ai.getGame();

forge-ai/src/main/java/forge/ai/ComputerUtilCard.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,9 +2107,8 @@ public static CardCollection prioritizeCreaturesWorthRemovingNow(final Player ai
21072107

21082108
public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) {
21092109
Game game = card.getGame();
2110-
boolean isRightSplit = sa != null && sa.getCardState().getStateName() == CardStateName.RightSplit;
2111-
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
2112-
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
2110+
String needsToPlayName = "NeedsToPlay";
2111+
String needsToPlayVarName = "NeedsToPlayVar";
21132112

21142113
// TODO: if there are ever split cards with Evoke or Kicker, factor in the right split option above
21152114
if (sa != null) {

forge-ai/src/main/java/forge/ai/PlayerControllerAi.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -931,10 +931,9 @@ public boolean chooseFlipResult(SpellAbility sa, Player flipper, boolean call) {
931931
if (call) {
932932
// Win if possible
933933
return true;
934-
} else {
935-
// heads or tails, AI doesn't know which is better now
936-
return MyRandom.getRandom().nextBoolean();
937934
}
935+
// heads or tails, AI doesn't know which is better now
936+
return MyRandom.getRandom().nextBoolean();
938937
}
939938

940939
@Override
@@ -993,7 +992,10 @@ public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType k
993992
}
994993
return defaultVal != null && defaultVal;
995994
case LeftOrRight: return brains.chooseDirection(sa);
996-
case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even
995+
case OddsOrEvens: return brains.chooseEvenOdd(sa);
996+
case HeadsOrTails:
997+
// this is the result if AI gets to choose after
998+
return true;
997999
default:
9981000
return MyRandom.getRandom().nextBoolean();
9991001
}

forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -286,22 +286,22 @@ public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<
286286
}
287287

288288
return ComputerUtil.isNegativeCounter(type, tgt);
289-
} else {
290-
if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) {
291-
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
292-
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
289+
}
293290

294-
if (maritEmpty) {
295-
return false;
296-
}
297-
} else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
298-
return false;
299-
} else if (type.is(CounterEnumType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
291+
if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) {
292+
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
293+
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
294+
295+
if (maritEmpty) {
300296
return false;
301297
}
302-
303-
return !ComputerUtil.isNegativeCounter(type, tgt);
298+
} else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
299+
return false;
300+
} else if (type.is(CounterEnumType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
301+
return false;
304302
}
303+
304+
return !ComputerUtil.isNegativeCounter(type, tgt);
305305
}
306306
return super.chooseBinary(kindOfChoice, sa, params);
307307
}

forge-ai/src/main/java/forge/ai/ability/UntapAi.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ private static boolean untapPrefTargeting(final Player ai, final SpellAbility sa
167167
if (combat == null) {
168168
return false;
169169
}
170-
list = CardLists.filter(list, c -> isAttackingAi(c, ai, combat));
170+
list = CardLists.filter(list, c -> combat.isAttacking(c, ai));
171171
if (list.isEmpty()) {
172172
return false;
173173
}
@@ -379,7 +379,7 @@ private boolean doPreventCombatDamageLogic(final Player ai, final SpellAbility s
379379
}
380380

381381
CardCollection list = CardLists.getTargetableCards(activeCombat.getAttackers(), sa);
382-
list = CardLists.filter(list, c -> isAttackingAi(c, ai, activeCombat));
382+
list = CardLists.filter(list, c -> activeCombat.isAttacking(c, ai));
383383

384384
if (list.isEmpty()) {
385385
return false;
@@ -398,10 +398,6 @@ private boolean doPreventCombatDamageLogic(final Player ai, final SpellAbility s
398398
return false;
399399
}
400400

401-
private static boolean isAttackingAi(final Card card, final Player ai, final Combat combat) {
402-
return ai.equals(combat.getDefenderPlayerByAttacker(card));
403-
}
404-
405401
private static boolean alreadyAssignedTarget(final SpellAbility sa) {
406402
if (sa.hasParam("AILogic")) {
407403
String aiLogic = sa.getParam("AILogic");

forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,8 @@ private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, fina
5050
return null;
5151
}
5252

53-
Game gameCopy;
5453
GameCopier copier = new GameCopier(evalGame);
55-
56-
if (evalGame.EXPERIMENTAL_RESTORE_SNAPSHOT) {
57-
gameCopy = copier.makeCopy();
58-
} else {
59-
gameCopy = copier.makeCopy(null, aiPlayer);
60-
}
54+
Game gameCopy = copier.makeCopy(null, aiPlayer);
6155

6256
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, () -> GameSimulator.resolveStack(gameCopy, aiPlayer.getWeakestOpponent()));
6357
CombatSimResult result = new CombatSimResult();

0 commit comments

Comments
 (0)