Skip to content

Commit fc919f1

Browse files
authored
Merge pull request #6 from olarozenfeld/fix-empath-mayor
Couple of bugfixes
2 parents 747c074 + da3e525 commit fc919f1

File tree

7 files changed

+151
-33
lines changed

7 files changed

+151
-33
lines changed

.bazelversion

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5.3.0

src/game_log.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ message Event {
296296
StorytellerInteraction storyteller_interaction = 3;
297297
Nomination nomination = 4;
298298
Vote vote = 5;
299+
// Use the empty string to indicate no execution in the rare case where you
300+
// explicitly want to note that nobody was executed in final 3, and yet
301+
// a Mayor win DID NOT occur, and you still want to solve at that point, and
302+
// not after the game continued.
299303
string execution = 6;
300304
// Storyteller announces that the player is dead. This can be due to
301305
// execution, Slayer shot, Tinker, Witch, or any other day-time effect.

src/game_sat_solver.cc

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ void GameSatSolver::AddChefConstraints(int chef, int chef_number) {
127127
if (role_claims_[i][0] == RECLUSE) {
128128
evil_options_i.push_back(
129129
model_.NewEquivalentVarAnd(
130-
{RoleVar(i, RECLUSE, night1), Not(PoisonerPickVar(i, night1))},
130+
{RoleVar(i, RECLUSE, night1), Not(PoisonedVar(i, night1))},
131131
absl::StrFormat("healthy_recluse_%s_%s", g_.PlayerName(i),
132132
night1)));
133133
}
@@ -151,7 +151,7 @@ void GameSatSolver::AddChefConstraints(int chef, int chef_number) {
151151
evil_pairs, chef_number, absl::StrFormat(
152152
"chef_%s_number_%d", g_.PlayerName(chef), chef_number));
153153
model_.AddOr(
154-
{Not(RoleVar(chef, CHEF, night1)), PoisonerPickVar(chef, night1), correct});
154+
{Not(RoleVar(chef, CHEF, night1)), PoisonedVar(chef, night1), correct});
155155
}
156156

157157
void GameSatSolver::AddEmpathConstraints() {
@@ -167,17 +167,19 @@ void GameSatSolver::AddEmpathConstraints() {
167167
}
168168

169169
void GameSatSolver::AddEmpathConstraints(
170-
int player, int number, const Time& time) {
170+
int player, int number, const Time& night_time) {
171+
// Empath goes after Evils, so the time of all registration checks needs
172+
// to be the daytime:
173+
const Time time = night_time + 1;
171174
vector<int> alive_neighbors = g_.AliveNeighbors(player, time); // Size 2.
172175
const int ping1 = alive_neighbors[0], ping2 = alive_neighbors[1];
173-
// We assume a Spy will not be poisoned. But Empath goes after the Imp, so
174-
// need to check day role.
176+
// We assume a Spy will not be poisoned.
175177
BoolVar ping1_regs_good = model_.NewEquivalentVarOr(
176-
{Not(StartingEvilVar(ping1)), RoleVar(ping1, SPY, time + 1)},
178+
{Not(StartingEvilVar(ping1)), RoleVar(ping1, SPY, time)},
177179
absl::StrFormat("empath_%s_registers_%s_good_%s", g_.PlayerName(player),
178180
g_.PlayerName(ping1), time));
179181
BoolVar ping2_regs_good = model_.NewEquivalentVarOr(
180-
{Not(StartingEvilVar(ping2)), RoleVar(ping2, SPY, time + 1)},
182+
{Not(StartingEvilVar(ping2)), RoleVar(ping2, SPY, time)},
181183
absl::StrFormat("empath_%s_registers_%s_good_%s", g_.PlayerName(player),
182184
g_.PlayerName(ping2), time));
183185
vector<BoolVar> ping1_regs_evil_cases({StartingEvilVar(ping1)});
@@ -252,11 +254,13 @@ void GameSatSolver::AddFortuneTellerConstraints() {
252254
}
253255

254256
void GameSatSolver::AddFortuneTellerConstraints(
255-
int player, int pick1, int pick2, bool yes, const Time& time) {
257+
int player, int pick1, int pick2, bool yes, const Time& night_time) {
258+
// Fortune Teller goes after Evils, so time of all registration checks needs
259+
// to be the daytime:
260+
const Time time = night_time + 1;
256261
vector<BoolVar> yes_options({
257-
// Fortune Teller goes after the Imp, so we need to check day roles.
258-
RoleVar(pick1, IMP, time + 1), RedHerringVar(pick1),
259-
RoleVar(pick2, IMP, time + 1), RedHerringVar(pick2)});
262+
RoleVar(pick1, IMP, time), RedHerringVar(pick1),
263+
RoleVar(pick2, IMP, time), RedHerringVar(pick2)});
260264
BoolVar poisoned_recluse;
261265
if (yes) { // We can only infer Recluse possibilities from Yes answer.
262266
for (int pick : {pick1, pick2}) {
@@ -427,16 +431,20 @@ BoolVar GameSatSolver::AliveRoleVar(Role role, const Time& time) {
427431
absl::StrFormat("alive_%s_%s", Role_Name(role), time));
428432
}
429433

434+
// This works for everyone except the Imp. The Imp is poisoned iff they were
435+
// picked by the Poisoner. Everyone else also need to account for the
436+
// Poisoner being alive on the *day after* the pick.
430437
BoolVar GameSatSolver::PoisonedVar(int player, const Time& time) {
431438
Time night = time.is_day ? time - 1 : time;
432-
BoolVar picked = PoisonerPickVar(player, time);
439+
BoolVar picked = PoisonerPickVar(player, night);
433440
const auto night_deaths = g_.Deaths(night);
434441
if (night_deaths.empty()) {
435442
return picked;
436443
}
437-
// At most one night death in TB:
444+
// This accounts for both a starpass to the Poisoner and Imp killing the
445+
// Poisoner in the night.
438446
return model_.NewEquivalentVarAnd(
439-
{Not(RoleVar(night_deaths[0], POISONER, night)), picked},
447+
{AliveRoleVar(POISONER, night + 1), picked},
440448
absl::StrFormat("poisoned_%s_%s", g_.PlayerName(player), night));
441449
}
442450

@@ -591,7 +599,8 @@ void GameSatSolver::AddNoVictoryConstraints(const Time& time) {
591599
vector<BoolVar> cases;
592600
// No Mayor win.
593601
if (num_alive - g_.Deaths(time).size() == 3 &&
594-
g_.Execution(time) == kNoPlayer) {
602+
g_.Execution(time) == kNoPlayer &&
603+
(time < g_.CurrentTime() || g_.NoExecutionsDeclared())) {
595604
// Mayor was not alive or poisoned.
596605
for (int mayor : AliveRolePossibilities(MAYOR, time)) {
597606
cases.push_back(model_.NewEquivalentVarOr(
@@ -971,7 +980,7 @@ void GameSatSolver::AddImpStarpassConstraints(const Time& time) {
971980
const Role role_claim = role_claims_[i][time.count - 1];
972981
BoolVar healthy_recluse_i = role_claim == IMP ?
973982
model_.NewEquivalentVarAnd(
974-
{RoleVar(i, RECLUSE, time), Not(PoisonerPickVar(i, time))},
983+
{RoleVar(i, RECLUSE, time), Not(PoisonedVar(i, time))},
975984
absl::StrFormat("healthy_recluse_%s_%s", g_.PlayerName(i), time)) :
976985
model_.FalseVar();
977986
for (Role role : {POISONER, SPY, SCARLET_WOMAN, BARON, RECLUSE}) {
@@ -1017,12 +1026,12 @@ void GameSatSolver::AddImpActionConstraints(const internal::RoleAction& ra) {
10171026
// Target was not a healthy Soldier
10181027
if (IsRolePossible(imp_kill, SOLDIER, time)) {
10191028
model_.AddOr({Not(RoleVar(imp_kill, SOLDIER, time)),
1020-
PoisonerPickVar(imp_kill, time)});
1029+
PoisonedVar(imp_kill, time)});
10211030
}
10221031
// Target was not healthy Monk protected
10231032
for (int monk : PossibleMonkProtecting(imp_kill, time)) {
10241033
model_.AddOr({Not(RoleVar(monk, MONK, time)),
1025-
PoisonerPickVar(monk, time)});
1034+
PoisonedVar(monk, time)});
10261035
}
10271036
return;
10281037
}
@@ -1034,25 +1043,25 @@ void GameSatSolver::AddImpActionConstraints(const internal::RoleAction& ra) {
10341043
if (IsRolePossible(target, SOLDIER, time)) {
10351044
cases.push_back(model_.NewEquivalentVarAnd(
10361045
{RoleVar(target, SOLDIER, time),
1037-
Not(PoisonerPickVar(target, time))},
1046+
Not(PoisonedVar(target, time))},
10381047
absl::StrFormat("healthy_SOLDIER_%s_%s", g_.PlayerName(target),
10391048
time)));
10401049
}
10411050
// Target was healthy Monk protected
10421051
for (int monk : PossibleMonkProtecting(target, time)) {
10431052
cases.push_back(model_.NewEquivalentVarAnd(
10441053
{RoleVar(monk, MONK, time),
1045-
Not(PoisonerPickVar(monk, time))},
1054+
Not(PoisonedVar(monk, time))},
10461055
absl::StrFormat("healthy_MONK_%s_%s", g_.PlayerName(monk), time)));
10471056
}
10481057
// Target was a healthy Mayor, not healthy Monk protected, and bounced to no
10491058
// kill.
10501059
if (IsRolePossible(target, MAYOR, time)) {
10511060
vector<BoolVar> mayor_bounce_no_kill(
1052-
{RoleVar(target, MAYOR, time), Not(PoisonerPickVar(target, time))});
1061+
{RoleVar(target, MAYOR, time), Not(PoisonedVar(target, time))});
10531062
for (int monk : PossibleMonkProtecting(target, time)) {
10541063
mayor_bounce_no_kill.push_back(model_.NewEquivalentVarOr(
1055-
{Not(RoleVar(monk, MONK, time)), PoisonerPickVar(monk, time)},
1064+
{Not(RoleVar(monk, MONK, time)), PoisonedVar(monk, time)},
10561065
absl::StrFormat("not_healthy_MONK_%s_%s", g_.PlayerName(monk),
10571066
time)));
10581067
}
@@ -1063,13 +1072,13 @@ void GameSatSolver::AddImpActionConstraints(const internal::RoleAction& ra) {
10631072
// Healthy Soldier.
10641073
if (IsRolePossible(i, SOLDIER, time)) {
10651074
no_kill_cases.push_back(model_.NewEquivalentVarAnd(
1066-
{RoleVar(i, SOLDIER, time), Not(PoisonerPickVar(i, time))},
1075+
{RoleVar(i, SOLDIER, time), Not(PoisonedVar(i, time))},
10671076
absl::StrFormat("healthy_SOLDIER_%s_%s", g_.PlayerName(i),
10681077
time)));
10691078
}
10701079
for (int monk : PossibleMonkProtecting(i, time)) {
10711080
no_kill_cases.push_back(model_.NewEquivalentVarAnd(
1072-
{RoleVar(monk, MONK, time), Not(PoisonerPickVar(monk, time))},
1081+
{RoleVar(monk, MONK, time), Not(PoisonedVar(monk, time))},
10731082
absl::StrFormat("healthy_MONK_%s_%s", g_.PlayerName(monk),
10741083
time)));
10751084
}
@@ -1088,10 +1097,10 @@ void GameSatSolver::AddImpActionConstraints(const internal::RoleAction& ra) {
10881097
// Kill bounces. Target is a healthy Mayor, not healthy Monk protected.
10891098
if (IsRolePossible(target, MAYOR, time)) {
10901099
model_.AddEquality(RoleVar(target, MAYOR, time), true);
1091-
model_.AddEquality(PoisonerPickVar(target, time), false);
1100+
model_.AddEquality(PoisonedVar(target, time), false);
10921101
for (int monk : PossibleMonkProtecting(target, time)) {
10931102
model_.AddOr({Not(RoleVar(monk, MONK, time)),
1094-
PoisonerPickVar(monk, time)});
1103+
PoisonedVar(monk, time)});
10951104
}
10961105
return;
10971106
}
@@ -1125,12 +1134,12 @@ void GameSatSolver::AddImpConstraints(
11251134
// Target was not a healthy Soldier
11261135
if (IsRolePossible(imp_kill, SOLDIER, time)) {
11271136
model_.AddOr({Not(RoleVar(imp_kill, SOLDIER, time)),
1128-
PoisonerPickVar(imp_kill, time)});
1137+
PoisonedVar(imp_kill, time)});
11291138
}
11301139
// Target was not healthy Monk protected
11311140
for (int monk : PossibleMonkProtecting(imp_kill, time)) {
11321141
model_.AddOr({Not(RoleVar(monk, MONK, time)),
1133-
PoisonerPickVar(monk, time)});
1142+
PoisonedVar(monk, time)});
11341143
}
11351144
return;
11361145
}
@@ -1147,7 +1156,7 @@ void GameSatSolver::AddImpConstraints(
11471156
for (int i : AliveRolePossibilities(role, time)) {
11481157
cases.push_back(model_.NewEquivalentVarAnd(
11491158
{RoleVar(i, role, time),
1150-
Not(PoisonerPickVar(i, time))},
1159+
Not(PoisonedVar(i, time))},
11511160
absl::StrFormat("healthy_%s_%s_%s", Role_Name(role),
11521161
g_.PlayerName(i), time)));
11531162
}

src/game_sat_solver_test.cc

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <filesystem>
1515
#include <map>
16+
#include <memory>
1617

1718
#include "src/game_sat_solver.h"
1819
#include "src/game_state.h"
@@ -806,6 +807,32 @@ TEST(Starpass, PoisonerPerspectiveCatch) {
806807
EXPECT_WORLDS_EQ(Solve(g), expected_worlds);
807808
}
808809

810+
TEST(Starpass, BaronPerspectiveCatch) {
811+
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(7));
812+
g.AddNight(1);
813+
g.AddShownToken("P7", BARON);
814+
g.AddDay(1);
815+
g.AddRoleClaims(
816+
{SOLDIER, EMPATH, MAYOR, RECLUSE, RAVENKEEPER, BUTLER, LIBRARIAN}, "P1");
817+
g.AddClaimRoleAction("P2", g.NewEmpathInfo(0));
818+
g.AddClaimRoleAction("P6", g.NewButlerAction("P2"));
819+
g.AddClaimRoleAction("P7", g.NewLibrarianInfo("P2", "P3", DRUNK));
820+
g.AddNominationVoteExecution("P2", "P4");
821+
g.AddDeath("P4");
822+
g.AddNight(2);
823+
g.AddDay(2);
824+
g.AddNightDeath("P2");
825+
g.AddClaimRoleAction("P6", g.NewButlerAction("P2"));
826+
g.AddNominationVoteExecution("P1", "P6");
827+
g.AddDeath("P6");
828+
g.AddNight(3);
829+
g.AddShownToken("P7", IMP);
830+
g.AddDay(3);
831+
g.AddNightDeath("P5");
832+
g.AddClaimRoleAction("P5", g.NewRavenkeeperAction("P3", IMP));
833+
EXPECT_TRUE(IsValidWorld(g));
834+
}
835+
809836
TEST(Starpass, InvalidPoisonerPerspectiveCatch) {
810837
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(5));
811838
g.AddNight(1);
@@ -1004,6 +1031,26 @@ TEST(FortuneTeller, LearnsTrueInfo) {
10041031
EXPECT_TRUE(IsValidWorld(g));
10051032
}
10061033

1034+
TEST(FortuneTeller, UnpoisonedAfterStarpass) {
1035+
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(5));
1036+
g.AddNight(1);
1037+
g.AddShownToken("P1", POISONER);
1038+
g.AddRoleAction("P1", g.NewPoisonerAction("P4"));
1039+
g.AddDay(1);
1040+
g.AddRoleClaims({MAYOR, FORTUNE_TELLER, SLAYER, VIRGIN, SOLDIER}, "P1");
1041+
g.AddClaimRoleAction("P2", g.NewFortuneTellerAction("P3", "P4", true));
1042+
g.AddNominationVoteExecution("P1", "P4");
1043+
g.AddDeath("P4");
1044+
g.AddNight(2);
1045+
g.AddRoleAction("P1", g.NewPoisonerAction("P2"));
1046+
g.AddShownToken("P1", IMP);
1047+
g.AddDay(2);
1048+
g.AddNightDeath("P3");
1049+
g.AddClaimRoleAction("P2", g.NewFortuneTellerAction("P1", "P3", false));
1050+
// We know there is no way for the Fortune Teller to be wrong here.
1051+
EXPECT_FALSE(IsValidWorld(g));
1052+
}
1053+
10071054
TEST(Empath, LearnsTrueInfo) {
10081055
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(10));
10091056
g.AddNight(1);
@@ -1048,6 +1095,33 @@ TEST(Empath, LearnsTrueInfo) {
10481095
EXPECT_WORLDS_EQ(Solve(g, r), {roles});
10491096
}
10501097

1098+
TEST(Empath, GainsInfoOnNewNeighbor) {
1099+
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(7));
1100+
g.AddNight(1);
1101+
g.AddShownToken("P1", EMPATH);
1102+
g.AddRoleAction("P1", g.NewEmpathInfo(0));
1103+
g.AddDay(1);
1104+
g.AddRoleClaims(
1105+
{EMPATH, SLAYER, SLAYER, RAVENKEEPER, SOLDIER, CHEF, MONK}, "P1");
1106+
g.AddClaimRoleAction("P1", g.NewEmpathInfo(0));
1107+
g.AddClaimRoleAction("P6", g.NewChefInfo(0));
1108+
// P3 double-claims the Slayer and gets Slayer-shot in the face:
1109+
g.AddRoleAction("P2", g.NewSlayerAction("P3"));
1110+
g.AddDeath("P3"); // Scarlet Woman confirmed!
1111+
g.AddNominationVoteExecution("P1", "P4");
1112+
g.AddDeath("P4");
1113+
g.AddNight(2);
1114+
g.AddRoleAction("P1", g.NewEmpathInfo(1));
1115+
g.AddDay(2);
1116+
g.AddNightDeath("P2");
1117+
g.AddClaimRoleAction("P1", g.NewEmpathInfo(1));
1118+
g.AddClaimRoleAction("P7", g.NewMonkAction("P1"));
1119+
vector<unordered_map<string, Role>> expected_worlds({
1120+
{{"P1", EMPATH}, {"P2", SLAYER}, {"P3", IMP}, {"P4", RAVENKEEPER},
1121+
{"P5", IMP}, {"P6", CHEF}, {"P7", MONK}}});
1122+
EXPECT_WORLDS_EQ(Solve(g), expected_worlds);
1123+
}
1124+
10511125
TEST(Slayer, ImpDeducesDrunkSlayer) {
10521126
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(7));
10531127
g.AddNight(1);
@@ -1255,6 +1329,27 @@ TEST(GameEndConditions, PoisonedMayorNoWin) {
12551329
EXPECT_FALSE(IsValidWorld(g, r));
12561330
}
12571331

1332+
TEST(GameEndConditions, MayorMightBeDroisoned) {
1333+
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(5));
1334+
g.AddNight(1);
1335+
g.AddShownToken("P1", MAYOR);
1336+
g.AddDay(1);
1337+
g.AddRoleClaims({MAYOR, SLAYER, VIRGIN, RECLUSE, SAINT}, "P1");
1338+
g.AddNominationVoteExecution("P1", "P3");
1339+
g.AddDeath("P3");
1340+
g.AddNight(2);
1341+
g.AddDay(2);
1342+
g.AddNightDeath("P2"); // Final 3.
1343+
SolverRequest r = SolverRequestBuilder::FromCurrentRoles("P1", DRUNK);
1344+
EXPECT_TRUE(IsValidWorld(g, r));
1345+
r = SolverRequestBuilder()
1346+
.AddCurrentRoles("P1", MAYOR).AddHealthy("P1", 2).Build();
1347+
EXPECT_TRUE(IsValidWorld(g, r));
1348+
r = SolverRequestBuilder()
1349+
.AddCurrentRoles("P1", MAYOR).AddPoisoned("P1", 2).Build();
1350+
EXPECT_TRUE(IsValidWorld(g, r));
1351+
}
1352+
12581353
TEST(Spy, SpyPerspective) {
12591354
GameState g(PLAYER, TROUBLE_BREWING, MakePlayers(13));
12601355
g.AddNight(1);
@@ -1306,7 +1401,7 @@ TEST(Examples, ExamplesWork) {
13061401
map<path, int> world_counts_by_game({
13071402
{"tb/monk.pbtxt", 3},
13081403
{"tb/teensy_observer.pbtxt", 3},
1309-
{"tb/virgin.pbtxt", 8},
1404+
{"tb/virgin.pbtxt", 9},
13101405
});
13111406
string dir = "src/examples/";
13121407
for (const auto& it : world_counts_by_game) {

src/game_state.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ GameState& GameState::AddDay(int count) {
364364
log_.add_events()->set_day(count);
365365
++cur_time_;
366366
on_the_block_ = kNoPlayer;
367+
declare_no_executions_ = false;
367368
executions_.push_back(kNoPlayer);
368369
execution_deaths_.push_back(kNoPlayer);
369370
night_deaths_.push_back(kNoPlayer);
@@ -516,7 +517,12 @@ GameState& GameState::AddVote(absl::Span<const string> votes,
516517
GameState& GameState::AddExecution(const string& name) {
517518
log_.add_events()->set_execution(name);
518519
CHECK(cur_time_.is_day) << "Executions can only occur during the day.";
519-
const int executee = PlayerIndex(name);
520+
CHECK(!declare_no_executions_) << "No executions was already declared.";
521+
const int executee = PlayerIndex(name, true);
522+
if (executee == kNoPlayer) {
523+
declare_no_executions_ = true;
524+
return *this;
525+
}
520526
CHECK_EQ(executions_.back(), kNoPlayer)
521527
<< "More than one execution attempted.";
522528
CHECK(!nominations_.empty()) << "Execution must have a preceding nomination.";
@@ -705,7 +711,7 @@ GameState& GameState::AddAllShownTokens(absl::Span<const Role> roles) {
705711
return *this;
706712
}
707713

708-
// Returns the deaths chronilogically. Execution is always the last death.
714+
// Returns the deaths chronologically. Execution is always the last death.
709715
vector<int> GameState::Deaths(const Time& time) const {
710716
vector<int> result;
711717
if (time.is_day) {

0 commit comments

Comments
 (0)