Skip to content
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

Add support for one-sided alliances #1680

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

Conversation

kubaau
Copy link
Contributor

@kubaau kubaau commented Jul 23, 2024

Adds supported for one-sided alliances exactly (I hope) how they worked in S2. This makes it possible to achieve the same alliance logic as in the campaigns designed for S2 (tested with the Roman campaign and the FANpaign). FANpaign in particular makes almost no sense to play without such alliances being possible. See video1 and video2 for a short demonstration.

Adds an addon for catapults to attack allies, which was the case in S2 (defaults to disabled in RttR). This also makes old campaigns more viable since many scenarios rely on a slow catapult creep without starting a war.

These should probably be split into 2 PR's, but I am modifying campaign scripts in both so I worked on one branch and the addon does not make much sense without modifying campaign alliances.

Related to issues #743 and #1177

@Spikeone
Copy link
Member

Great work! Just two questions: Does this work in multiplayer? Is it possible to break such pact via diplomacy window as well?

@kubaau
Copy link
Contributor Author

kubaau commented Jul 26, 2024

Great work! Just two questions: Does this work in multiplayer? Is it possible to break such pact via diplomacy window as well?

This type of pact does not appear in the diplomacy window, so you cannot suggest or break such an alliance this way.

I was designing this with only single player in mind, thus the human can always attack (even their allies), as it was in S2. As it stands the pact does not make sense in multiplayer unless AIs are also involved and it can only be made through scripts. I think it could be made to work with multiplayer by adding one additional if statement in bool GamePlayer::CanAttack(const unsigned char otherPlayerId) const, I just didn't think much about multiplayer other than disallowing suggesting such a pact.

Copy link
Member

@Flamefire Flamefire left a comment

Choose a reason for hiding this comment

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

This might be a bit harder than it looks. Especially the definition of "allied to" isn't that clear any more. We might need to check where and how this is used.

/// Test whether a player is attackable or not (alliances, etc)
bool IsPlayerAttackable(unsigned char playerID) const { return player_.IsAttackable(playerID); }
/// Can the current player (AI) attack the other player?
bool CanAttack(unsigned char otherPlayerId) const { return gwb.GetPlayer(GetPlayerId()).CanAttack(otherPlayerId); }
Copy link
Member

Choose a reason for hiding this comment

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

Why the extra call?

Suggested change
bool CanAttack(unsigned char otherPlayerId) const { return gwb.GetPlayer(GetPlayerId()).CanAttack(otherPlayerId); }
bool CanAttack(unsigned char otherPlayerId) const { return player_.CanAttack(otherPlayerId); }

@@ -121,7 +121,7 @@ class nobBaseMilitary : public noBuilding
bool IsUnderAttack() const { return !aggressors.empty(); };

/// Return whether this building can be attacked by the given player.
virtual bool IsAttackable(unsigned playerIdx) const;
virtual bool CanBeAttackedBy(unsigned playerIdx) const;
Copy link
Member

Choose a reason for hiding this comment

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

I like having such getters start with get or is. Is IsAttackableBy better? Avoids another "fill-word" there.
I do like the addition of "by" though.

@@ -673,7 +673,7 @@ void nofAttacker::OrderAggressiveDefender()
continue;
// We only send a defender if we are allied with the attacked player and can attack the attacker (no pact etc)
GamePlayer& bldOwner = world->GetPlayer(bldOwnerId);
if(bldOwner.IsAlly(attacked_goal->GetPlayer()) && bldOwner.IsAttackable(player))
if(bldOwner.IsAlly(attacked_goal->GetPlayer()) && world->GetPlayer(bldOwnerId).CanAttack(player))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if(bldOwner.IsAlly(attacked_goal->GetPlayer()) && world->GetPlayer(bldOwnerId).CanAttack(player))
if(bldOwner.IsAlly(attacked_goal->GetPlayer()) && bldOwner.CanAttack(player))

- P1 cannot attack P2
- if P1 attacks P2 `or` if P1 breaks their alliance towards P2, `then`
- P2 breaks their alliance towards P1 `and`
- all players who are allied to P2 `and` to whom P2 is allied to break their alliance towards P1
Copy link
Member

Choose a reason for hiding this comment

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

Currently only one-sided alliances are broken. What is the intention?

Comment on lines +1192 to +1198
if(!IsAlly(i))
continue;

auto& player = world.GetPlayer(i);

// skip if they are not allied to us
if(!player.IsAlly(thisPlayerId))
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I understand that. In which case can we be allied to the other player but not the other way round?

maybe instead of commenting on the lines (e.g. "skip ourselves" is obvious from the code, similar for others) extend the comment above the loop. It looks like: "Break one-sided alliances from all our allies to the attacking player"

And maybe the loop can be even simpler: if(allied) -> reset one-sided pact to attacker
--> No need to check for self or do it above the loop as we are allied to ourselves, hence also removing any "continue"

Comment on lines +858 to +860
{
GetLua().EventAttack(player_attacker, attacked_building.GetPlayer(), counter);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{
GetLua().EventAttack(player_attacker, attacked_building.GetPlayer(), counter);
}
GetLua().EventAttack(player_attacker, attacked_building.GetPlayer(), counter);

Comment on lines +277 to +312
BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_AICanAttackAnyoneByDefault, EmptyWorldFixture2P)
{
auto& p0 = world.GetPlayer(0);
auto& p1 = world.GetPlayer(1);
p0.ps = PlayerState::AI;
p1.ps = PlayerState::AI;

BOOST_TEST_REQUIRE(p0.CanAttack(1) == true);
BOOST_TEST_REQUIRE(p1.CanAttack(0) == true);
}

BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_AICannotAttackAllies, EmptyWorldFixture2P)
{
auto& p0 = world.GetPlayer(0);
auto& p1 = world.GetPlayer(1);
p0.ps = PlayerState::AI;
p1.ps = PlayerState::AI;

p0.MakeOneSidedAllianceTo(1);

BOOST_TEST_REQUIRE(p0.CanAttack(1) == false);
BOOST_TEST_REQUIRE(p1.CanAttack(0) == true);
}

BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_HumansCanAttackAnyone, EmptyWorldFixture2P)
{
auto& p0 = world.GetPlayer(0);
auto& p1 = world.GetPlayer(1);
p1.ps = PlayerState::AI;

p0.MakeOneSidedAllianceTo(1);
p1.MakeOneSidedAllianceTo(0);

BOOST_TEST_REQUIRE(p0.CanAttack(1) == true);
BOOST_TEST_REQUIRE(p1.CanAttack(0) == false);
}
Copy link
Member

Choose a reason for hiding this comment

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

Those 3 can be combined, i.e.

  • AI can attack
  • after one-sided alliance it cannot
  • If it was a human it can attack AI (and humans), but not the other way round

This would more clearly highlight the differences

BOOST_TEST_REQUIRE(p0.CanAttack(1) == true);
}

BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_AllianceIsBrokenBothWaysWhenAttacked, EmptyWorldFixture2P)
Copy link
Member

Choose a reason for hiding this comment

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

This includes the previous test, doesn't it? So OneSidedAlliances_AllianceIsBrokenWhenAttacked is not needed IMO

BOOST_TEST_REQUIRE(p2.CanAttack(1) == true);
}

BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_BeingAttacked_DoesNotAlsoBreakAlliancesForPlayers_WhoAreNotOurAlliedToUs,
Copy link
Member

Choose a reason for hiding this comment

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

Similar here: It includes the former, so maybe rename to OneSidedAlliances_BeingAttacked_BreaksAlliancesForAlliedPlayers and extend the comments why which alliance is broken/not broken at the check

BOOST_TEST_REQUIRE(p2.CanAttack(1) == true);
}

BOOST_FIXTURE_TEST_CASE(OneSidedAlliances_BeingAttacked_DoesNotAlsoBreakAlliancesForPlayers_WhoWeAreNotAlliedTo,
Copy link
Member

Choose a reason for hiding this comment

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

I guess this needs resolving the definition of "allied to".

Say player X attacks player Y. Then of course player Y breaks the pact to player X.

And I guess every player Z allied to player Y should also break the pact to player X. Only question is whether this Z->Y should include one-sided pacts and if so which way: Z->Y, Y->Z, both or either. I would argue only Z->Y should be checked as Z "doesn't know" about Y->Z so why should it care about Y being attacked in that case?

Might also be worth merging with the above test case to focus on the differences and add the "DoesNotAlsoBreakAlliancesForPlayers_WhoWeAreNotAlliedTo" as a comment instead

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

Successfully merging this pull request may close these issues.

3 participants