Skip to content

Implement rooms (Bottomless Pool // Locker Room) #13786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: master
Choose a base branch
from

Conversation

oscscull
Copy link

@oscscull oscscull commented Jun 26, 2025

Had a go at building room card functionality. There were some difficult parts, though the name issue wasn't really that bad (it seems the existing work around for split cards is basically fine).
It seems to work.

Part of #12534

  • Added Bottomless Pool // Locker Room
  • Added Surgical Suite // Hospital Room

Copy link
Member

@JayDi85 JayDi85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code style looks good, but it can be fully reviewed after unit tests added to make sure it's support all rule parts (the most important part -- copy and zone changing logic, also name searching/compare).


boolean unlockLeftHalf(Game game, Ability source);

boolean unlockRightHalf(Game game, Ability source);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to use "room/door" prefixes in methods and fields

shot_250626_204706
shot_250626_204720

this.leftHalfUnlocked = true;

// Fire generic door unlock event
game.fireEvent(new GameEvent(GameEvent.EventType.UNLOCK_DOOR, getId(), source, source.getControllerId()));
Copy link
Member

@JayDi85 JayDi85 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Unlock" naming for replacement and "unlocked" naming for passed events.

@@ -378,7 +388,11 @@ private static boolean maybeRemoveFromSourceZone(ZoneChangeInfo info, Game game,
Permanent permanent;
if (card instanceof MeldCard) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meld cards are badly tested by unit tests, so it's better to write room related tests for blinks, hand/stack/battlefield copy, etc.

* @author oscscull
* Checks if a Permanent's left half is LOCKED (i.e., NOT unlocked).
*/
public enum RoomLeftHalfLockedCondition implements Condition {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to use card hint to show rooms status in popup hints, e.g. "green icon left door unlocked", "red icon right door locked", etc

@JayDi85 JayDi85 requested review from theelk801 June 26, 2025 17:06
// Add abilities to remove locked door abilities - keep triggers or they won't trigger
if (leftAbility != null && !(leftAbility instanceof UnlockThisDoorTriggeredAbility)) {
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
new LoseAbilitySourceEffect(leftAbility, Duration.WhileOnBattlefield),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's smells bad. Why you need to use such workaround with lose abilities effects? Use static trigger with conditional instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly was trying to follow the word of

"709.5. Some split cards are permanent cards with a single shared type line. A shared type line on such an object represents two static abilities that function on the battlefield. These are “As long as this permanent doesn’t have the ‘left half unlocked’ designation, it doesn’t have the name, mana cost, or rules text of this object’s left half” and “As long as this permanent doesn’t have the ‘right half unlocked’ designation, it doesn’t have the name, mana cost, or rules text of this object’s right half.” These abilities, as well as which half of that permanent a characteristic is in, are part of that object’s copiable values."

So, as my understanding of the above, the room loses the ability when it's not unlocked. That approach is probably simpler though.

Copy link
Author

@oscscull oscscull Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly it doesn't work cleanly (in the way I did it) because if the ability is lost, and the ability triggers on the unlock, the code that does the unlocking (including the event dispatch) happens in the wrong order. (The unlock happens, the event is fired, then the event checks for triggers, then the permanent gets the triggered ability back and misses the event)

@@ -50,7 +50,7 @@ public PermanentCard(Card card, UUID controllerId, Game game) {
goodForBattlefield = false;
} else if (card instanceof SplitCard) {
// fused spells allowed (it uses main card)
if (card.getSpellAbility() != null && !card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED)) {
if (card.getSpellAbility() != null && !card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED) && !(card instanceof RoomCard)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure all places with SPLIT_FUSED usage checked, same for SPLIT_LEFT and SPLIT_RIGHT:
shot_250626_212008

@Grath
Copy link
Contributor

Grath commented Jun 26, 2025

Edge case for room names which I do not believe is handled by the current hacky workaround for name:

  • Play a room with one side unlocked
  • Play [[Opalescence]] to turn it into a creature
  • Give it a non-mana activated ability (such as [[Diviner's Wand]]'s {4}: Draw a Card)
  • [[Pithing Needle]] the name of the locked side (may or may not be possible as currently coded?)
  • Validate that the room can still activate the gained ability
  • Unlock the other side, so it gains the name of the other side
  • Validate that you can no longer activate the ability.

Copy link

Opalescence - (Gatherer) (Scryfall) (EDHREC)

{2}{W}{W}
Enchantment
Each other non-Aura enchantment is a creature in addition to its other types and has base power and base toughness each equal to its mana value.

Diviner's Wand - (Gatherer) (Scryfall) (EDHREC)

{3}
Kindred Artifact — Wizard Equipment
Equipped creature has "Whenever you draw a card, this creature gets +1/+1 and gains flying until end of turn" and "{4}: Draw a card."
Whenever a Wizard creature enters, you may attach this Equipment to it.
Equip {3}

Pithing Needle - (Gatherer) (Scryfall) (EDHREC)

{1}
Artifact
As this artifact enters, choose a card name.
Activated abilities of sources with the chosen name can't be activated unless they're mana abilities.

@Grath
Copy link
Contributor

Grath commented Jun 26, 2025

(Also speaking of Opalescence - make sure that the mana value of the room reflects the mana value of the unlocked sides; a non-cast Room should have zero mana value and die with Opalesence in play, a half-unlocked room should have the mana value of that unlocked side, and a fully unlocked room should have the total mana value.)

@JayDi85
Copy link
Member

JayDi85 commented Jun 27, 2025

Also check other room cards — some of it can contain unique logic and can require additional changes to support it.

@oscscull
Copy link
Author

oscscull commented Jul 3, 2025

Edge case for room names which I do not believe is handled by the current hacky workaround for name:

  • Play a room with one side unlocked
  • Play [[Opalescence]] to turn it into a creature
  • Give it a non-mana activated ability (such as [[Diviner's Wand]]'s {4}: Draw a Card)
  • [[Pithing Needle]] the name of the locked side (may or may not be possible as currently coded?)
  • Validate that the room can still activate the gained ability
  • Unlock the other side, so it gains the name of the other side
  • Validate that you can no longer activate the ability.

I will check these specifically. I have done a lot of changes to the code with opalescence + bile blight for local testing so that works for sure. Also testing mirage mirror and similar copy effects have a lot of edge cases. I asked the judge reddit for what happens when a room loses all its abilities + opalescence + muraganda petroglyphs but, got no definite answer.

@oscscull
Copy link
Author

oscscull commented Jul 3, 2025

Thanks to all for the extensive reviews. Apologies, I've got limited time to work on this so I expect I will proceed in bursts when I get a chance. I'll address all of these and add unit tests this week if possible.

@oscscull
Copy link
Author

oscscull commented Jul 3, 2025

Also check other room cards — some of it can contain unique logic and can require additional changes to support it.

this is a good point, I'll probably pull in several more to this PR.

@github-actions github-actions bot added the tests label Jul 6, 2025
@@ -433,6 +434,15 @@ public boolean hasObjectTargetNameOrAlias(MageObject object, String nameOrAlias)
return true;
}

if (object instanceof RoomCard && object.getName().contains(" // ")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's wrong. CardUtil.haveSameNames must do all the work with search.

shot_250706_191009

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

checkPlayableAbility("one land", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Bottomless Pool", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
addTarget(playerA, "Grizzly Bears");
execute();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must use setStrictChooseMode(true); to find miss commands in all tests. Also I recommend to use standard tests structure/format and comments:

Example with test_TriggerOnBigBoosted:

shot_250706_191247
shot_250706_191259

Copy link
Author

@oscscull oscscull Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll follow this template - I added some tests already.
There are a lot of cases that might have issues so i'll be testing
Flicker - done
copy on stack
clone (etb)
copy (on battlefield)
a permanent that copies one room, then later copies a different room (mirage mirror)

then name related
on the stack
on the battlefield
in the graveyard (is there such an effect? I'll have to check)

before this can come out of drafts. might be more.

@oscscull oscscull marked this pull request as ready for review July 21, 2025 16:14
@@ -56,8 +56,6 @@ public void test_NamesEquals() {
Assert.assertTrue(CardUtil.haveSameNames(splitCard1, "Armed // Dangerous", currentGame));
Assert.assertTrue(CardUtil.haveSameNames(splitCard1, splitCard1));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Other", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Other // Dangerous", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Armed // Other", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, splitCard2));
Copy link
Author

@oscscull oscscull Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: If this test is relevant to an actual case, I'll have to adjust my approach. I couldn't think of a scenario where it comes up. Please check and confirm!

@oscscull oscscull requested a review from JayDi85 July 21, 2025 16:29
@oscscull
Copy link
Author

Edge case for room names which I do not believe is handled by the current hacky workaround for name:

  • Play a room with one side unlocked
  • Play [[Opalescence]] to turn it into a creature
  • Give it a non-mana activated ability (such as [[Diviner's Wand]]'s {4}: Draw a Card)
  • [[Pithing Needle]] the name of the locked side (may or may not be possible as currently coded?)
  • Validate that the room can still activate the gained ability
  • Unlock the other side, so it gains the name of the other side
  • Validate that you can no longer activate the ability.

Specifically tested this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants