Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
AIMessageIngress.cpp
Go to the documentation of this file.
2#include "AISaveState.hpp"
3
4#include "LocalPlayers.hpp"
5#include "LegacyTextIds.hpp"
6#include "Messaging.hpp"
8
9#include <chrono>
10#include <cstdlib>
11
12namespace monopoly::ai
13{
14 namespace
15 {
16 using Clock = std::chrono::steady_clock;
17
18 trade::TradeIngressState tradeIngress{};
19 profile::RuntimeState profileRuntime{};
20 bool auctionOn{};
21 Clock::time_point gracePeriodForHumanActivity{};
22
23 struct EconomicRuntimeState
24 {
27 std::array<decision::EconomicActionPlan, rules::MaxPlayers> pendingControlAction{};
28 std::array<bool, rules::MaxPlayers> hasPendingControlAction{};
29 std::array<bool, rules::MaxPlayers> controlRequestInFlight{};
30 std::array<bool, rules::MaxPlayers> actionInFlight{};
31 std::array<bool, rules::MaxPlayers> controlRestartInFlight{};
32 std::array<bool, rules::MaxPlayers> controlReleaseInFlight{};
33 };
34
35 EconomicRuntimeState economicRuntime{};
36 std::array<bool, rules::MaxPlayers> jailDecisionInFlight{};
37 std::array<bool, rules::MaxPlayers> taxDecisionInFlight{};
38 std::array<bool, rules::MaxPlayers> buyDecisionInFlight{};
39 bool auctionBidInFlight{};
41 std::array<bool, rules::MaxPlayers> housingAuctionRequestInFlight{};
42 std::array<bool, rules::MaxPlayers> buildingPlacementInFlight{};
43 std::array<bool, rules::MaxPlayers> debtActionInFlight{};
44 std::array<std::int64_t, rules::MaxPlayers> debtMoneyOwed{};
45 std::array<bool, rules::MaxPlayers> freeUnmortgageInFlight{};
46 std::array<bool, rules::MaxPlayers> freeUnmortgageDoneInFlight{};
47 enum class PendingTurnPrompt : std::uint8_t { None = 0, RollDice, EndTurn };
48 std::array<PendingTurnPrompt, rules::MaxPlayers> pendingTurnPrompt{};
49 std::array<bool, rules::MaxPlayers> turnActionInFlight{};
50 std::array<bool, rules::MaxPlayers> turnPromptPaused{};
51 std::array<rules::board::SquareType, rules::MaxPlayers> lastBuildingAction{};
52 bool housingShortageOn{};
54 std::array<bool, rules::MaxPlayers> configurationAcceptInFlight{};
55 std::array<bool, rules::MaxPlayers> cardSeenInFlight{};
58
60 {
61 if (player >= rules::MaxPlayers) return 0;
62 // Ai.cpp uses num_actions != 0 for trade acceptance/counter preflight.
63 // Modern action owners keep separate flags, including a queued
64 // economic control action. Count their busy states for that predicate;
65 // this is not an attempt to reconstruct the retail message counter.
66 return static_cast<int>(economicRuntime.hasPendingControlAction[player]) +
67 economicRuntime.controlRequestInFlight[player] +
68 economicRuntime.actionInFlight[player] +
69 economicRuntime.controlRestartInFlight[player] +
70 economicRuntime.controlReleaseInFlight[player] +
72 buyDecisionInFlight[player] +
78 ((tradeIngress.pendingTradeAcceptPlayers & (1u << player)) != 0) +
79 (tradeIngress.counterRuntime.player == player &&
80 tradeIngress.counterRuntime.sending.state != trade::SendingTradeState::Nothing);
81 }
82
84 rules::PlayerNumber player) noexcept
85 {
86 if (player >= rules::MaxPlayers)
87 return;
88
89 economicRuntime.pendingControlAction[player] = {};
90 economicRuntime.hasPendingControlAction[player] = false;
91 economicRuntime.controlRequestInFlight[player] = false;
92 economicRuntime.actionInFlight[player] = false;
93 economicRuntime.controlRestartInFlight[player] = false;
94 economicRuntime.controlReleaseInFlight[player] = false;
95 jailDecisionInFlight[player] = false;
96 taxDecisionInFlight[player] = false;
97 buyDecisionInFlight[player] = false;
98 housingAuctionRequestInFlight[player] = false;
99 buildingPlacementInFlight[player] = false;
100 debtActionInFlight[player] = false;
101 debtMoneyOwed[player] = 0;
102 freeUnmortgageInFlight[player] = false;
103 freeUnmortgageDoneInFlight[player] = false;
104 pendingTurnPrompt[player] = PendingTurnPrompt::None;
105 turnActionInFlight[player] = false;
106 turnPromptPaused[player] = false;
108 if (economicRuntime.controlOwner == player)
109 {
111 economicRuntime.decompositionControl = false;
112 }
113 configurationAcceptInFlight[player] = false;
114 cardSeenInFlight[player] = false;
115
116 const auto bit = 1u << player;
117 tradeIngress.pendingTradeAcceptPlayers &= ~bit;
118 tradeIngress.deferredAcceptancePlayers &= ~bit;
119 tradeIngress.counterSessions[player] = {};
120 tradeIngress.turnState[player] = {};
121 if (tradeIngress.counterRuntime.player == player ||
122 tradeIngress.counterRuntime.proposedPlayer == player)
123 tradeIngress.counterRuntime = {};
124 if (tradeIngress.playerJustRejectedCountered == player)
125 {
126 tradeIngress.tradeJustRejectedCountered = false;
127 tradeIngress.playerJustRejectedCountered = rules::NobodyPlayer;
128 }
129 if (purchasingPlayer == player)
130 {
131 purchasingPlayer = rules::NobodyPlayer;
132 purchasingProperty = rules::board::SquareType::Count;
133 }
134 if (auctionBiddingPlayer == player)
135 {
136 auctionBidInFlight = false;
138 }
139 }
140
142 {
143 auctionBidInFlight = false;
145 auctionOn = false;
146 housingShortageOn = false;
147 buildingPlacementOn = false;
149 economicRuntime.decompositionControl = false;
150 economicRuntime.controlRestartInFlight.fill(false);
151 turnPromptPaused.fill(true);
152 purchasingPlayer = rules::NobodyPlayer;
153 purchasingProperty = rules::board::SquareType::Count;
154
155 tradeIngress.tradeJustRejectedCountered = false;
156 tradeIngress.playerJustRejectedCountered = rules::NobodyPlayer;
157 tradeIngress.proposedPlayer = rules::NobodyPlayer;
159 }
160
161 void updateRuntimeFlags(const actions::Message& message) noexcept
162 {
163 // A quick debt sale can enter decomposition without an underlying
164 // BSSM phase. Its next decision notification is the only public
165 // evidence that this temporary control has ended.
166 if (economicRuntime.decompositionControl &&
167 (message.action == actions::Type::NotifyPleasePay ||
168 message.action == actions::Type::NotifyPleaseRollDice ||
169 message.action == actions::Type::NotifyEndTurn ||
170 message.action == actions::Type::NotifyFreeUnmortgaging ||
171 message.action == actions::Type::NotifyPlaceBuilding ||
172 message.action == actions::Type::NotifyJailExitChoice ||
175 message.action == actions::Type::NotifyPickedUpCard ||
176 (message.action == actions::Type::NotifyActionCompleted &&
177 message.numberA == static_cast<std::int64_t>(actions::Type::NotifyPleasePay))))
178 {
179 if (economicRuntime.controlOwner < rules::MaxPlayers)
180 economicRuntime.controlRestartInFlight[economicRuntime.controlOwner] = false;
182 economicRuntime.decompositionControl = false;
183 }
184 // UI clients have no authoritative phase stack. Like Ai.cpp, track
185 // economic ownership and modal decisions from RULE notifications.
186 if ((message.action == actions::Type::NotifyPlayerBuySellMort ||
187 message.action == actions::Type::NotifyDecomposeSale) &&
188 message.numberA >= 0 &&
189 (message.numberA < rules::MaxPlayers || message.numberA == rules::NobodyPlayer))
190 {
191 economicRuntime.controlOwner =
192 static_cast<rules::PlayerNumber>(message.numberA);
193 economicRuntime.decompositionControl =
194 message.action == actions::Type::NotifyDecomposeSale;
195 if (economicRuntime.controlOwner < rules::MaxPlayers)
196 {
197 economicRuntime.controlRestartInFlight[economicRuntime.controlOwner] = false;
198 if (message.action == actions::Type::NotifyPlayerBuySellMort)
199 debtMoneyOwed[economicRuntime.controlOwner] =
200 message.numberC > 0 ? message.numberC : 0;
201 turnPromptPaused.fill(true);
202 }
203 }
204 if (message.action == actions::Type::NotifyPleasePay &&
205 message.numberA >= 0 && message.numberA < rules::MaxPlayers)
206 debtMoneyOwed[static_cast<rules::PlayerNumber>(message.numberA)] =
207 message.numberC > 0 ? message.numberC : 0;
208 if (message.action == actions::Type::NotifyHousingShortage)
209 housingShortageOn = message.numberD != 3;
210 if (message.action == actions::Type::NotifyAuctionGoing)
211 housingShortageOn = false;
212 if (message.action == actions::Type::NotifyPlaceBuilding)
213 buildingPlacementOn = true;
214 else if (message.action == actions::Type::NotifyActionCompleted &&
215 message.numberA == static_cast<std::int64_t>(actions::Type::BuyHouse))
216 buildingPlacementOn = false;
217 if (message.action == actions::Type::NotifyPleasePay ||
218 message.action == actions::Type::NotifyFreeUnmortgaging ||
219 message.action == actions::Type::NotifyPlaceBuilding ||
220 message.action == actions::Type::NotifyHousingShortage ||
221 message.action == actions::Type::NotifyTradeStarted ||
222 message.action == actions::Type::NotifyNewHighBid)
223 turnPromptPaused.fill(true);
224 if (message.action == actions::Type::NotifyNewHighBid)
225 auctionOn = true;
226 else if (message.action == actions::Type::NotifyAuctionGoing &&
227 message.numberD >= 3)
228 auctionOn = false;
229
230 if (message.action == actions::Type::NotifyPleasePay)
232 else if (message.action == actions::Type::NotifyTradeFinished ||
233 (message.action == actions::Type::NotifyPlayerBuySellMort &&
234 message.numberA == rules::NobodyPlayer))
235 gracePeriodForHumanActivity = Clock::now();
236 }
237
238 [[nodiscard]] profile::ConfigContext makeConfigContext() noexcept
239 {
240 profile::ConfigContext context{};
241 for (rules::PlayerNumber player = 0;
242 player < rules::MaxPlayers; ++player)
243 {
244 context.localAIPlayer[player] =
246 }
247 context.moneyOwed = debtMoneyOwed;
248 context.purchasingPlayer = purchasingPlayer;
249 context.purchasingProperty = purchasingProperty;
250 return context;
251 }
252
254 const rules::GameState& state,
255 const actions::Message& message) noexcept
256 {
257 if (message.action == actions::Type::NotifyPlayerDeleted)
258 {
259 if (message.numberA >= 0 &&
260 message.numberA < rules::MaxPlayers)
261 {
264 static_cast<rules::PlayerNumber>(message.numberA));
265 }
266 return;
267 }
268
269 if (message.action != actions::Type::NotifyNamePlayer ||
270 message.numberA < 0 ||
271 message.numberA >= rules::MaxPlayers)
272 return;
273 const auto player = static_cast<rules::PlayerNumber>(
274 message.numberA);
275
277 {
279 return;
280 }
281
282 const auto& playerState = state.players[player];
285 player,
286 playerState.token,
287 playerState.aiPlayerLevel);
288
289 if (!loaded)
290 {
291 // Retail Userifce.cpp removes a local AI immediately when
292 // AI_Load_AI() fails, then sends ACTION_NAME_PLAYER with an
293 // empty name so RULE releases the slot.
295 state, player);
296 }
297 }
298
300 const actions::Message& message) noexcept
301 {
302 if (message.action == actions::Type::NotifyBuyOrAuctionDecision &&
303 message.numberA >= 0 && message.numberA < rules::MaxPlayers &&
304 message.numberB >= 0 &&
305 message.numberB < static_cast<std::int64_t>(rules::board::SquareType::InJail))
306 {
307 purchasingPlayer = static_cast<rules::PlayerNumber>(message.numberA);
308 purchasingProperty = static_cast<rules::board::SquareType>(message.numberB);
309 return;
310 }
311
312 if (message.action == actions::Type::NotifySquareOwnership &&
313 purchasingPlayer != rules::NobodyPlayer &&
314 message.numberA == static_cast<std::int64_t>(purchasingProperty))
315 {
316 purchasingPlayer = rules::NobodyPlayer;
317 purchasingProperty = rules::board::SquareType::Count;
318 }
319 }
320
322 const rules::GameState& state,
323 const actions::Message& message) noexcept
324 {
325 if (message.action == actions::Type::NotifyActionCompleted &&
326 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
327 {
328 const auto player =
329 static_cast<rules::PlayerNumber>(message.numberC);
330 const auto completed =
331 static_cast<actions::Type>(message.numberA);
332 if (completed == actions::Type::AcceptConfiguration)
333 configurationAcceptInFlight[player] = false;
334 else if (completed == actions::Type::CardSeen)
335 cardSeenInFlight[player] = false;
336 return;
337 }
338
340 {
341 if (message.numberA <= 0)
342 return;
343 const auto requested = static_cast<std::uint32_t>(message.numberA);
344 for (rules::PlayerNumber player = 0;
345 player < state.numberOfPlayers && player < rules::MaxPlayers;
346 ++player)
347 {
348 if ((requested & (1u << player)) == 0 ||
350 !profileRuntime.playerLoaded[player])
351 continue;
352
353 save::State persisted{};
354 persisted.profile = profileRuntime.players[player];
355 persisted.timeLastTrade = tradeIngress.turnState[player].timeLastTrade;
356 actions::Message response{};
358 response.fromPlayer = player;
359 response.toPlayer = rules::BankPlayer;
360 if (save::encode(persisted, response.binaryDataA))
362 }
363 return;
364 }
365
366 if (message.action == actions::Type::NotifyAIParameters &&
367 message.numberA >= 0 && message.numberA < rules::MaxPlayers)
368 {
369 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
370 if (player >= state.numberOfPlayers ||
372 !profileRuntime.playerLoaded[player])
373 return;
374
375 save::State persisted{};
376 if (!save::decode(
377 std::span<const std::uint8_t>(message.binaryDataA),
378 persisted))
379 return;
380
381 profileRuntime.players[player] = std::move(persisted.profile);
382 tradeIngress.turnState[player].timeLastTrade = persisted.timeLastTrade;
383 return;
384 }
385
386 if (message.action == actions::Type::NotifyProposedConfiguration)
387 {
388 if (message.numberB <= 0)
389 return;
390 const auto requested =
391 static_cast<std::uint32_t>(message.numberB);
392 auto acceptedOptions = state.options;
393 if (message.numberC < 1)
394 {
395 acceptedOptions.futureRentTradingAllowed = false;
396 acceptedOptions.immunitiesTradingAllowed = false;
397 }
398
399 for (rules::PlayerNumber player = 0;
400 player < state.numberOfPlayers && player < rules::MaxPlayers;
401 ++player)
402 {
403 const auto bit = 1u << player;
404 if ((requested & bit) == 0 ||
407 continue;
408
409 actions::Message acceptance{};
411 acceptedOptions, player, false, acceptance) &&
413 {
414 configurationAcceptInFlight[player] = true;
415 }
416 }
417 return;
418 }
419
420 if (message.action == actions::Type::NotifyPickedUpCard &&
421 message.numberA >= 0 &&
422 message.numberA < rules::MaxPlayers)
423 {
424 const auto player =
425 static_cast<rules::PlayerNumber>(message.numberA);
426 if (player < state.numberOfPlayers &&
428 !cardSeenInFlight[player] &&
431 player, rules::BankPlayer))
432 {
433 cardSeenInFlight[player] = true;
434 }
435 return;
436 }
437
438 if (message.action == actions::Type::NotifyClientResyncInfo)
439 {
441 return;
442 }
443
444 if (message.action == actions::Type::NotifyPlayerDeleted &&
445 message.numberA >= 0 && message.numberA < rules::MaxPlayers)
446 {
448 static_cast<rules::PlayerNumber>(message.numberA));
449 return;
450 }
451
452 if (message.action == actions::Type::NotifyJumpToSquare &&
453 message.numberA == static_cast<std::int64_t>(
455 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
456 {
457 const auto player =
458 static_cast<rules::PlayerNumber>(message.numberC);
461 }
462 }
463
465 const rules::GameState& state,
466 const actions::Message& message) noexcept
467 {
468 if (message.action == actions::Type::NotifyStartTurn)
469 {
470 auto context = makeConfigContext();
471 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
472 context.localAIPlayer[player] = context.localAIPlayer[player] &&
473 profileRuntime.playerLoaded[player];
474 trade::advanceTradeTurn(state.numberOfPlayers, profileRuntime.players,
475 context.localAIPlayer, tradeIngress);
476 return;
477 }
478 auto proposer = tradeIngress.proposedPlayer;
479 std::int64_t respondingPlayer = -1;
480 bool accepted = false;
481 if (message.action == actions::Type::NotifyErrorMessage)
482 {
483 if (message.numberA == legacy_text::ErrorTradeChanging)
484 {
485 tradeIngress.proposedPlayer = tradeIngress.lastEditor;
486 return;
487 }
488 if (message.numberA != legacy_text::ErrorTradeAccepted &&
489 message.numberA != legacy_text::ErrorTradeRejected)
490 return;
491 accepted = message.numberA == legacy_text::ErrorTradeAccepted;
492 respondingPlayer = message.numberC;
493 }
494 else if (message.action == actions::Type::NotifyTradeEditor &&
495 tradeIngress.tradeOfferedForAcceptance)
496 {
497 proposer = tradeIngress.lastEditor;
498 respondingPlayer = message.numberA;
499 }
500 else
501 return;
502
504 return;
505 auto context = makeConfigContext();
506 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
507 context.localAIPlayer[player] = context.localAIPlayer[player] &&
508 profileRuntime.playerLoaded[player];
509 const auto player = static_cast<rules::PlayerNumber>(respondingPlayer);
512 profileRuntime.players, context);
514 profileRuntime.players[observer].playerAttitude[player] += changes[observer];
515 }
516
517 [[nodiscard]] trade::TradeAcceptanceConfig makeAcceptanceConfig(
518 rules::PlayerNumber player,
519 const profile::ConfigContext& context) noexcept
520 {
521 trade::TradeAcceptanceConfig config{};
523 profileRuntime.players, player, context);
524 const auto& strategy = profileRuntime.players[player];
525 config.numberTimesAllowPropertyTrade =
526 strategy.numberTimesAllowPropertyTrade;
527 config.minEvaluationThreshold = strategy.minEvaluationThreshold;
528 config.minEvaluationIfFedUp = strategy.minEvaluationIfFedUp;
529 return config;
530 }
531
533 {
534 return static_cast<double>(std::rand()) /
535 static_cast<double>(RAND_MAX);
536 }
537
538 [[nodiscard]] std::array<std::uint32_t, 8> retailRandomKeys() noexcept
539 {
540 std::array<std::uint32_t, 8> keys{};
541 for (auto& key : keys)
542 key = static_cast<std::uint32_t>(std::rand());
543 return keys;
544 }
545
546 [[nodiscard]] rules::PlayerNumber buySellMortgagePlayer(
547 const rules::GameState&) noexcept
548 {
549 return economicRuntime.controlOwner;
550 }
551
552 [[nodiscard]] bool economicBusy(rules::PlayerNumber player) noexcept
553 {
554 return player < rules::MaxPlayers &&
555 (economicRuntime.hasPendingControlAction[player] ||
556 economicRuntime.controlRequestInFlight[player] ||
557 economicRuntime.actionInFlight[player] ||
558 economicRuntime.controlRestartInFlight[player] ||
559 economicRuntime.controlReleaseInFlight[player]);
560 }
561
564 {
565 switch (kind)
566 {
575 return actions::Type::Tick;
576 }
577 return actions::Type::Tick;
578 }
579
581 rules::PlayerNumber player,
582 const decision::EconomicActionPlan& plan) noexcept
583 {
584 if (!plan.acted() || player >= rules::MaxPlayers)
585 return false;
586 const auto action = economicActionType(plan.kind);
587 if (action == actions::Type::Tick ||
589 action, player, rules::BankPlayer,
590 static_cast<std::int64_t>(plan.square)))
591 return false;
592 economicRuntime.actionInFlight[player] = true;
596 return true;
597 }
598
600 const rules::GameState& state,
601 rules::PlayerNumber player,
602 const decision::EconomicActionPlan& plan) noexcept
603 {
604 if (!plan.acted() || player >= state.numberOfPlayers || economicBusy(player))
605 return false;
606 const auto control = buySellMortgagePlayer(state);
607 if (control == player)
608 return sendEconomicAction(player, plan);
610 return false;
611
612 economicRuntime.pendingControlAction[player] = plan;
613 economicRuntime.hasPendingControlAction[player] = true;
616 player, rules::BankPlayer))
617 {
618 economicRuntime.pendingControlAction[player] = {};
619 economicRuntime.hasPendingControlAction[player] = false;
620 return false;
621 }
622 economicRuntime.controlRequestInFlight[player] = true;
623 return true;
624 }
625
627 const rules::GameState&, rules::PlayerNumber player) noexcept
628 {
630 }
631
632 struct EconomicDecision
633 {
634 decision::EconomicActionPlan plan{};
635 bool defer{};
636 };
637
638 [[nodiscard]] EconomicDecision nextEconomicDecision(
639 const rules::GameState& state,
640 rules::PlayerNumber player,
641 const profile::ConfigContext& context) noexcept
642 {
643 EconomicDecision result{};
644 if (player >= state.numberOfPlayers || !profileRuntime.playerLoaded[player])
645 return result;
646
647 const auto& strategy = profileRuntime.players[player];
648 const auto buy = decision::shouldBuyHouse(
649 state, player, strategy.cashStrategy, strategy.minCashOnHand,
650 strategy.housingPurchaseStrategy, context.moneyOwed[player]);
652 {
653 result.defer = true;
654 return result;
655 }
657 {
658 result.plan = decision::planBuyHouseAction(
659 state, player, strategy.cashStrategy, strategy.minCashOnHand,
660 strategy.housingPurchaseStrategy, context.moneyOwed[player]);
661 if (result.plan.acted())
662 return result;
663 }
664
666 state, player, strategy.cashStrategy, strategy.minCashOnHand,
667 context.moneyOwed[player]))
668 {
670 state, player, strategy.cashStrategy, strategy.minCashOnHand,
671 context.moneyOwed[player]);
672 }
673 return result;
674 }
675
677 rules::PlayerNumber player) noexcept
678 {
679 if (player >= rules::MaxPlayers ||
680 economicRuntime.controlReleaseInFlight[player])
681 return false;
684 player, rules::BankPlayer))
685 return false;
686 economicRuntime.controlReleaseInFlight[player] = true;
687 return true;
688 }
689
691 const rules::GameState& state,
692 rules::PlayerNumber player) noexcept
693 {
694 if (player >= state.numberOfPlayers ||
695 economicRuntime.actionInFlight[player] ||
696 economicRuntime.controlReleaseInFlight[player])
697 return;
698
699 if (auctionOn || state.tradeInProgress || playerPayingDebt(state, player) ||
700 tradeIngress.counterRuntime.sending.state !=
702 {
704 return;
705 }
706
707 auto context = makeConfigContext();
708 const auto next = nextEconomicDecision(state, player, context);
709 if (next.defer || !next.plan.acted())
710 {
712 return;
713 }
714 (void)sendEconomicAction(player, next.plan);
715 }
717 const rules::GameState& state,
718 const actions::Message& message) noexcept
719 {
720 if (message.action == actions::Type::NotifyActionCompleted &&
721 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
722 {
723 const auto player = static_cast<rules::PlayerNumber>(message.numberC);
724 const auto completed = static_cast<actions::Type>(message.numberA);
725 if (completed == actions::Type::PlayerBuySellMort)
726 {
727 economicRuntime.controlRequestInFlight[player] = false;
728 if (message.numberB == 0)
729 {
730 economicRuntime.pendingControlAction[player] = {};
731 economicRuntime.hasPendingControlAction[player] = false;
732 }
733 }
734 else if (completed == actions::Type::BuyHouse ||
735 completed == actions::Type::Mortgaging ||
736 completed == actions::Type::SellBuildings)
737 {
738 economicRuntime.actionInFlight[player] = false;
739 // Ai.cpp AI_Action_Completed resends the controlling player's
740 // phase after economic actions, including failed actions.
741 // The free-unmortgage owner below has its own restart path.
742 const auto owner = buySellMortgagePlayer(state);
744 owner < state.numberOfPlayers &&
745 !freeUnmortgageInFlight[player] &&
746 (completed != actions::Type::BuyHouse || !auctionOn) &&
747 !economicRuntime.controlRestartInFlight[owner])
748 {
749 economicRuntime.controlRestartInFlight[owner] =
752 }
753 }
754 else if (completed == actions::Type::PlayerDoneBuySellMort)
755 {
756 economicRuntime.controlReleaseInFlight[player] = false;
757 }
758 return;
759 }
760 if (message.action != actions::Type::NotifyPlayerBuySellMort)
761 return;
762
763 if (message.numberA == rules::NobodyPlayer)
764 {
765 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
766 {
767 economicRuntime.controlRestartInFlight[player] = false;
768 economicRuntime.controlReleaseInFlight[player] = false;
769 }
770 return;
771 }
772 if (message.numberA < 0 || message.numberA >= state.numberOfPlayers)
773 return;
774
775 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
776 economicRuntime.controlRestartInFlight[player] = false;
777 if (buySellMortgagePlayer(state) != player ||
779 !profileRuntime.playerLoaded[player])
780 return;
781
782 economicRuntime.controlRequestInFlight[player] = false;
783 if (economicRuntime.actionInFlight[player] ||
784 economicRuntime.controlReleaseInFlight[player])
785 return;
786
787 if (economicRuntime.hasPendingControlAction[player])
788 {
789 const auto pending = economicRuntime.pendingControlAction[player];
790 if (sendEconomicAction(player, pending))
791 {
792 economicRuntime.pendingControlAction[player] = {};
793 economicRuntime.hasPendingControlAction[player] = false;
794 }
795 return;
796 }
797
799 }
800
802 const rules::GameState& state,
803 const actions::Message& message) noexcept
804 {
805 if (message.action == actions::Type::NotifyActionCompleted &&
806 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
807 {
808 const auto player = static_cast<rules::PlayerNumber>(message.numberC);
809 const auto completed = static_cast<actions::Type>(message.numberA);
810 if (completed == actions::Type::Mortgaging &&
812 {
813 freeUnmortgageInFlight[player] = false;
816 return;
817 }
818 if (completed == actions::Type::FreeUnmortgageDone)
819 {
820 freeUnmortgageDoneInFlight[player] = false;
821 return;
822 }
823 }
824
825 if (message.action != actions::Type::NotifyFreeUnmortgaging ||
826 message.numberA < 0 || message.numberA >= state.numberOfPlayers ||
827 message.numberB == 0)
828 return;
829
830 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
832 !profileRuntime.playerLoaded[player] ||
834 return;
835
836 const auto& strategy = profileRuntime.players[player];
837 auto context = makeConfigContext();
839 state, player, strategy.cashStrategy, strategy.minCashOnHand,
840 context.moneyOwed[player]))
841 {
843 state, player, strategy.cashStrategy, strategy.minCashOnHand,
844 context.moneyOwed[player]);
845 if (plan.acted() && sendEconomicAction(player, plan))
846 {
847 freeUnmortgageInFlight[player] = true;
848 return;
849 }
850 }
851
854 player, rules::BankPlayer))
855 {
856 freeUnmortgageDoneInFlight[player] = true;
857 }
858 }
859
861 const rules::GameState& state,
862 const actions::Message& message) noexcept
863 {
864 if (message.action == actions::Type::NotifyActionCompleted &&
865 message.numberA == static_cast<std::int64_t>(actions::Type::BuyOrAuctionDecision) &&
866 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
867 {
868 buyDecisionInFlight[static_cast<rules::PlayerNumber>(message.numberC)] = false;
869 return;
870 }
871 if (message.action != actions::Type::NotifyBuyOrAuctionDecision ||
872 message.numberA < 0 || message.numberA >= state.numberOfPlayers ||
873 message.numberB < 0 ||
874 message.numberB >= static_cast<std::int64_t>(rules::board::SquareType::InJail))
875 return;
876
877 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
879 !profileRuntime.playerLoaded[player] || buyDecisionInFlight[player])
880 return;
881 const auto property = static_cast<rules::board::SquareType>(message.numberB);
882 const auto& strategy = profileRuntime.players[player];
883 const auto context = makeConfigContext();
884 const bool buy = decision::shouldBuyProperty(
885 state, player, property, strategy.cashStrategy,
886 strategy.minCashOnHand, context.moneyOwed);
889 player, rules::BankPlayer, buy ? 1 : 0))
890 buyDecisionInFlight[player] = true;
891 }
892
894 const rules::GameState& state,
895 const actions::Message& message) noexcept
896 {
897 if (message.action == actions::Type::NotifyActionCompleted &&
898 message.numberA == static_cast<std::int64_t>(actions::Type::Bid))
899 {
900 if (message.numberC == auctionBiddingPlayer)
901 {
902 auctionBidInFlight = false;
904 }
905 return;
906 }
907 if ((message.action != actions::Type::NotifyNewHighBid &&
908 message.action != actions::Type::NotifyAuctionGoing) ||
909 auctionBidInFlight || state.numberOfPlayers == 0 ||
910 state.numberOfPlayers > rules::MaxPlayers)
911 return;
912 if (message.action == actions::Type::NotifyAuctionGoing && message.numberD >= 3)
913 return;
914 if (message.numberB < 0 || message.numberC < 0 ||
915 message.numberC >= static_cast<std::int64_t>(rules::board::SquareType::Count))
916 return;
917
918 const auto currentBidder = message.numberA >= 0 &&
919 message.numberA < state.numberOfPlayers
920 ? static_cast<rules::PlayerNumber>(message.numberA)
921 : rules::NobodyPlayer;
922 const auto item = static_cast<rules::board::SquareType>(message.numberC);
923 const auto start = static_cast<rules::PlayerNumber>(
924 (static_cast<unsigned long long>(std::rand()) * state.numberOfPlayers) /
925 (static_cast<unsigned long long>(RAND_MAX) + 1ull));
926 const auto context = makeConfigContext();
927 for (std::size_t checked = 0; checked < state.numberOfPlayers; ++checked)
928 {
929 const auto player = static_cast<rules::PlayerNumber>(
930 (start + checked) % state.numberOfPlayers);
932 !profileRuntime.playerLoaded[player] ||
933 state.players[player].currentSquare == static_cast<std::uint8_t>(
935 continue;
936
937 const auto& strategy = profileRuntime.players[player];
938 decision::AuctionBidConfig config{};
940 profileRuntime.players, player, context);
941 config.housingPurchaseStrategy = strategy.housingPurchaseStrategy;
942 config.monopolySuicideFactor = strategy.monopolySuicideFactor;
943 const auto bid = decision::bidForAuctionItem(
944 state, player, item, message.numberB, currentBidder, config,
945 (std::rand() % 2) != 0);
946 if (bid == 0)
947 continue;
950 {
951 auctionBidInFlight = true;
952 auctionBiddingPlayer = player;
953 }
954 return;
955 }
956 }
957
959 const rules::GameState& state,
960 const actions::Message& message) noexcept
961 {
962 if (message.action == actions::Type::NotifyActionCompleted &&
963 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
964 {
965 const auto player = static_cast<rules::PlayerNumber>(message.numberC);
966 const auto completed = static_cast<actions::Type>(message.numberA);
967 if (completed == actions::Type::StartHousingAuction)
968 housingAuctionRequestInFlight[player] = false;
969 else if (completed == actions::Type::BuyHouse && buildingPlacementInFlight[player])
970 buildingPlacementInFlight[player] = false;
971 return;
972 }
973 if (message.action == actions::Type::NotifyPlaceBuilding)
974 {
975 if (message.numberA < 0 || message.numberA >= state.numberOfPlayers ||
976 message.numberC <= 0)
977 return;
978 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
980 !profileRuntime.playerLoaded[player] || buildingPlacementInFlight[player])
981 return;
982 const auto& strategy = profileRuntime.players[player];
983 const auto context = makeConfigContext();
984 const auto building = message.numberB > 0
987 const auto square = decision::chooseHousingAuctionSquare(
988 state, player, building, strategy.cashStrategy,
989 strategy.minCashOnHand,
990 static_cast<rules::board::PropertySet>(message.numberC), true,
991 context.moneyOwed[player]);
992 if (square && messaging::sendAction(
994 static_cast<std::int64_t>(*square)))
995 buildingPlacementInFlight[player] = true;
996 return;
997 }
998 if (message.action != actions::Type::NotifyHousingShortage ||
999 message.numberD >= 3 || message.numberE <= 0)
1000 return;
1001
1002 const auto allowed = static_cast<std::uint32_t>(message.numberE);
1003 const auto originalBuyer = message.numberA >= 0 &&
1004 message.numberA < state.numberOfPlayers
1005 ? static_cast<rules::PlayerNumber>(message.numberA)
1006 : rules::NobodyPlayer;
1007 const auto building = message.numberC > 0
1010 const auto context = makeConfigContext();
1011 for (rules::PlayerNumber player = 0; player < state.numberOfPlayers; ++player)
1012 {
1013 if ((allowed & (1u << player)) == 0 || player == originalBuyer ||
1015 !profileRuntime.playerLoaded[player] ||
1017 continue;
1018 const auto& strategy = profileRuntime.players[player];
1020 state, player, strategy.cashStrategy, strategy.minCashOnHand,
1021 strategy.housingPurchaseStrategy, context.moneyOwed[player]) ==
1023 continue;
1024 const auto square = decision::chooseHousingAuctionSquare(
1025 state, player, building, strategy.cashStrategy,
1026 strategy.minCashOnHand, 0, false, context.moneyOwed[player]);
1027 if (!square)
1028 continue;
1031 player, rules::BankPlayer))
1032 housingAuctionRequestInFlight[player] = true;
1033 return;
1034 }
1035 }
1036
1038 const rules::GameState& state,
1039 const actions::Message& message) noexcept
1040 {
1041 if (message.action == actions::Type::NotifyActionCompleted &&
1042 message.numberA == static_cast<std::int64_t>(actions::Type::TaxDecision) &&
1043 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
1044 {
1045 taxDecisionInFlight[static_cast<rules::PlayerNumber>(message.numberC)] = false;
1046 return;
1047 }
1048 if (message.action != actions::Type::NotifyFlatOrFractionTaxDecision ||
1049 message.numberA < 0 || message.numberA >= state.numberOfPlayers)
1050 return;
1051
1052 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1054 !profileRuntime.playerLoaded[player] || taxDecisionInFlight[player])
1055 return;
1056
1057 const auto percentage = decision::chooseFractionTax(state, player);
1058 if (!percentage)
1059 return;
1062 *percentage ? 1 : 0))
1063 {
1064 taxDecisionInFlight[player] = true;
1065 }
1066 }
1067
1069 const rules::GameState& state,
1070 const actions::Message& message) noexcept
1071 {
1072 if (message.action == actions::Type::NotifyActionCompleted &&
1073 message.numberA == static_cast<std::int64_t>(actions::Type::ExitJailDecision) &&
1074 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
1075 {
1076 jailDecisionInFlight[static_cast<rules::PlayerNumber>(message.numberC)] = false;
1077 return;
1078 }
1079 if (message.action != actions::Type::NotifyJailExitChoice ||
1080 message.numberA < 0 || message.numberA >= state.numberOfPlayers)
1081 return;
1082
1083 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1085 !profileRuntime.playerLoaded[player] || jailDecisionInFlight[player])
1086 return;
1087
1088 const auto choice = decision::chooseJailExitChoice(
1089 state, player,
1090 message.numberB != 0,
1091 message.numberC != 0,
1092 message.numberD != 0);
1093 if (!choice)
1094 return;
1097 player, rules::BankPlayer,
1098 static_cast<std::int64_t>(*choice)))
1099 {
1100 jailDecisionInFlight[player] = true;
1101 }
1102 }
1103
1105 const rules::GameState& state, rules::PlayerNumber player) noexcept
1106 {
1107 if (auctionOn || state.tradeInProgress || economicBusy(player) ||
1108 playerPayingDebt(state, player) ||
1109 tradeIngress.counterRuntime.sending.state !=
1111 state.players[player].currentSquare ==
1112 static_cast<std::uint8_t>(rules::board::SquareType::OffBoard))
1113 return true;
1114
1117 return true;
1118
1119 const auto controlPlayer = buySellMortgagePlayer(state);
1121 return true;
1122
1123 return state.options.aiTakesTimeToThink &&
1124 gracePeriodForHumanActivity != Clock::time_point{} &&
1125 Clock::now() - gracePeriodForHumanActivity < std::chrono::seconds(10);
1126 }
1127
1129 const rules::GameState& state,
1130 const actions::Message& message) noexcept
1131 {
1132 if (message.action != actions::Type::Tick &&
1133 !(message.action == actions::Type::NotifyStartTurn &&
1134 !state.options.aiTakesTimeToThink))
1135 return;
1136
1137 auto context = makeConfigContext();
1138 for (rules::PlayerNumber player = 0; player < state.numberOfPlayers; ++player)
1139 {
1141 !profileRuntime.playerLoaded[player] || economicBusy(player) ||
1142 pendingTurnPrompt[player] != PendingTurnPrompt::None ||
1144 continue;
1145
1146 const auto next = nextEconomicDecision(state, player, context);
1147 if (next.defer || !next.plan.acted())
1148 continue;
1149 if (queueEconomicAction(state, player, next.plan))
1150 return;
1151 }
1152 }
1153
1154 [[nodiscard]] decision::SemiImportantTradeInputs makeSemiImportantAttemptInputs(
1155 const rules::GameState& state, rules::PlayerNumber player,
1156 std::uint8_t importance, const profile::Profile& strategy,
1157 const profile::ConfigContext& context) noexcept
1158 {
1159 decision::SemiImportantTradeInputs inputs{};
1160 const auto assets = ai::liquidAssets(
1161 state, player, false, false, context.moneyOwed[player]);
1162 if ((importance & trade::TradeForCash) != 0 ||
1163 state.players[player].aiPlayerLevel != 3)
1164 {
1165 const auto keys = retailRandomKeys();
1168 }
1169 else
1170 {
1173 }
1174 inputs.deriveOfferedGroups = true;
1175 inputs.wantedPropertyRoll = static_cast<std::uint32_t>(std::rand());
1176 inputs.offeredPropertyRoll = static_cast<std::uint32_t>(std::rand());
1177 inputs.importance = importance;
1178 inputs.minimumNonmonopolyTradeAttitude =
1179 strategy.minimumNonmonopolyTradeAttitude;
1180 return inputs;
1181 }
1182
1183 struct SemiImportantPartnerOrder
1184 {
1185 std::array<rules::PlayerNumber, rules::MaxPlayers> players{};
1186 std::size_t count{};
1187 };
1188
1189 [[nodiscard]] SemiImportantPartnerOrder semiImportantPartnerOrder(
1190 const rules::GameState& state, rules::PlayerNumber player,
1191 std::span<const double> attitudes,
1192 rules::PlayerNumber excludedPlayer) noexcept
1193 {
1194 SemiImportantPartnerOrder result{};
1195 std::array<rules::PlayerNumber, rules::MaxPlayers> candidates{};
1196 std::size_t candidateCount{};
1197 double totalWeight{};
1199 candidate < state.numberOfPlayers; ++candidate)
1200 {
1201 if (candidate == player || candidate == excludedPlayer ||
1202 state.players[candidate].currentSquare ==
1203 static_cast<std::uint8_t>(rules::board::SquareType::OffBoard) ||
1204 candidate >= attitudes.size() || attitudes[candidate] < -1.0)
1205 continue;
1207 totalWeight += (attitudes[candidate] + 1.0) / 2.0;
1208 }
1209 if (candidateCount == 0 || totalWeight <= 0.0)
1210 return result;
1211
1212 double choice = retailCounterRoll();
1213 std::size_t start{};
1214 for (; start < candidateCount; ++start)
1215 {
1216 choice -= (attitudes[candidates[start]] + 1.0) / 2.0 / totalWeight;
1217 if (choice <= 0.0)
1218 break;
1219 }
1220 if (start >= candidateCount)
1221 start = candidateCount - 1;
1222 for (std::size_t offset = 0; offset < candidateCount; ++offset)
1223 result.players[result.count++] =
1224 candidates[(start + offset) % candidateCount];
1225 return result;
1226 }
1227
1229 const rules::GameState& state, rules::PlayerNumber player,
1230 std::uint8_t importance, rules::PlayerNumber excludedPlayer,
1231 const profile::Profile& strategy,
1232 const profile::ConfigContext& context,
1233 const trade::PropertySets& properties,
1234 const decision::MonopolyProposalConfig& config,
1235 trade::TradeProposalList& proposal) noexcept
1236 {
1237 const auto order = semiImportantPartnerOrder(
1238 state, player, strategy.playerAttitude, excludedPlayer);
1239 for (std::size_t index = 0; index < order.count; ++index)
1240 {
1242 state, player, importance, strategy, context);
1243 attempt.excludedPlayer = excludedPlayer;
1244 attempt.requiredTarget = order.players[index];
1245 attempt.partnerRoll = 0.0;
1246
1249 state, player, strategy.playerAttitude, properties,
1250 attempt, next, config) &&
1252 {
1253 proposal = next;
1254 return true;
1255 }
1256 }
1257 return false;
1258 }
1259
1261 const rules::GameState& state, rules::PlayerNumber player,
1262 const profile::ConfigContext& context) noexcept
1263 {
1264 if (player >= state.numberOfPlayers || !context.localAIPlayer[player] ||
1266 return false;
1267
1268 const auto& strategy = profileRuntime.players[player];
1270 state, player, strategy.cashStrategy, strategy.minCashOnHand,
1271 strategy.maxHousesPerSquareForGiveAway, context.moneyOwed);
1272
1273 trade::TradeCadenceInputs cadence{};
1274 cadence.playerSendingTrade = tradeIngress.counterRuntime.sending.state !=
1276 cadence.buySellMortgagePlayer = buySellMortgagePlayer(state);
1277 cadence.shouldGiveAwayMonopoly = shouldGiveAway;
1278 cadence.giveAwayProbability = strategy.proposeMonopolyGiveAwayProbability;
1279 cadence.monopolyProbability = strategy.monopolyTradeProbability;
1280 cadence.proposalProbability = strategy.proposeTradeProbability;
1281
1282 bool wantsTrade{};
1283 if (shouldGiveAway)
1284 {
1285 cadence.giveAwayRoll = retailCounterRoll();
1286 cadence.proposalRoll = 2.0;
1288 state, player, 0, tradeIngress.turnState[player].timeLastTrade,
1289 strategy.maxTrades, cadence);
1290 cadence.shouldGiveAwayMonopoly = false;
1291 }
1292 if (!wantsTrade)
1293 {
1294 cadence.proposalRoll = retailCounterRoll();
1296 state, player, 0, tradeIngress.turnState[player].timeLastTrade,
1297 strategy.maxTrades, cadence);
1298 }
1299 if (!wantsTrade)
1300 return false;
1301
1302 trade::PropertySets properties{};
1303 for (rules::PlayerNumber owner = 0; owner < state.numberOfPlayers; ++owner)
1304 properties[owner] = ai::propertiesOwnedByPlayer(state, owner);
1305
1306 decision::ProactiveTradeInputs inputs{};
1307 inputs.shouldGiveAwayMonopoly = shouldGiveAway;
1308 const auto assets = ai::liquidAssets(
1309 state, player, false, false, context.moneyOwed[player]);
1310 if (state.players[player].aiPlayerLevel == 3)
1311 inputs.monopolyGroups = trade::orderMonopolyImportance(
1313 else
1314 {
1315 const auto keys = retailRandomKeys();
1316 inputs.monopolyGroups = trade::orderMonopolyImportance(
1318 }
1319
1321 profileRuntime.players, player, context);
1322 auto proposal = decision::buildProactiveTrade(
1323 state, player, strategy.playerAttitude, properties, inputs, config);
1325 {
1328 state, player, 0, rules::NobodyPlayer, strategy, context,
1329 properties, config, semiProposal))
1330 return false;
1333 }
1334 if (proposal.kind == decision::ProactiveTradeKind::None)
1335 return false;
1337 state, player, proposal.proposal, strategy, tradeIngress);
1338 }
1339
1341 rules::PlayerNumber player,
1342 const decision::EconomicActionPlan& plan) noexcept
1343 {
1344 if (!plan.acted() || player >= rules::MaxPlayers ||
1345 debtActionInFlight[player])
1346 return false;
1347
1348 actions::Type action{};
1353 else
1354 return false;
1355
1357 action, player, rules::BankPlayer,
1358 static_cast<std::int64_t>(plan.square), 0, 0, 1))
1359 return false;
1360 debtActionInFlight[player] = true;
1362 lastBuildingAction[player] = plan.square;
1363 return true;
1364 }
1365
1367 const rules::GameState& state,
1368 rules::PlayerNumber player,
1370 std::uint8_t importance,
1371 const profile::ConfigContext& context) noexcept
1372 {
1373 if (player >= state.numberOfPlayers ||
1374 !profileRuntime.playerLoaded[player] ||
1375 state.tradeInProgress ||
1376 tradeIngress.counterRuntime.sending.state !=
1378 return false;
1379
1380 const auto& strategy = profileRuntime.players[player];
1381 if ((importance & trade::TradeDesperate) == 0)
1382 {
1383 trade::TradeCadenceInputs cadence{};
1384 cadence.buySellMortgagePlayer = buySellMortgagePlayer(state);
1385 if (!trade::shouldTrade(
1386 state, player, importance,
1387 tradeIngress.turnState[player].timeLastTrade,
1388 strategy.maxTrades, cadence))
1389 return false;
1390 }
1391
1392 trade::PropertySets properties{};
1393 for (rules::PlayerNumber owner = 0;
1394 owner < state.numberOfPlayers; ++owner)
1395 properties[owner] = ai::propertiesOwnedByPlayer(state, owner);
1396
1397 decision::ProactiveTradeInputs inputs{};
1398 inputs.importance = importance;
1399 inputs.excludedPlayer = creditor;
1400 const auto assets = ai::liquidAssets(
1401 state, player, false, false, context.moneyOwed[player]);
1402 if (state.players[player].aiPlayerLevel == 3)
1403 inputs.monopolyGroups = trade::orderMonopolyImportance(
1405 else
1406 {
1407 const auto keys = retailRandomKeys();
1408 inputs.monopolyGroups = trade::orderMonopolyImportance(
1410 }
1411
1413 profileRuntime.players, player, context);
1414 auto proposal = decision::buildProactiveTrade(
1415 state, player, strategy.playerAttitude, properties, inputs, config);
1417 {
1420 state, player, importance, creditor, strategy, context,
1421 properties, config, semiProposal))
1422 return false;
1425 }
1426 if (proposal.kind == decision::ProactiveTradeKind::None)
1427 return false;
1429 state, player, proposal.proposal, strategy, tradeIngress);
1430 }
1431
1432 void processDebtMessage(
1433 const rules::GameState& state,
1434 const actions::Message& message) noexcept
1435 {
1436 if (message.action == actions::Type::NotifyPlayerDeleted &&
1437 message.numberA >= 0 && message.numberA < rules::MaxPlayers)
1438 {
1439 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1440 debtActionInFlight[player] = false;
1441 debtMoneyOwed[player] = 0;
1442 return;
1443 }
1444
1445 if (message.action == actions::Type::NotifyActionCompleted &&
1446 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
1447 {
1448 const auto player = static_cast<rules::PlayerNumber>(message.numberC);
1449 const auto completed = static_cast<actions::Type>(message.numberA);
1450 if (completed == actions::Type::Mortgaging ||
1451 completed == actions::Type::SellBuildings)
1452 debtActionInFlight[player] = false;
1453
1454 if (completed == actions::Type::NotifyPleasePay ||
1455 completed == actions::Type::GoBankrupt)
1456 {
1457 debtActionInFlight[player] = false;
1458 debtMoneyOwed[player] = 0;
1459 }
1460 return;
1461 }
1462
1463 if (message.action == actions::Type::NotifyDecomposeSale)
1464 {
1465 if (message.numberA < 0 || message.numberA >= rules::MaxPlayers)
1466 return;
1467 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1469 !profileRuntime.playerLoaded[player] ||
1470 debtActionInFlight[player] || economicRuntime.actionInFlight[player])
1471 return;
1473 state, lastBuildingAction[player]);
1474 if (plan.acted())
1476 return;
1477 }
1478
1479 if (message.action != actions::Type::NotifyPleasePay ||
1480 message.numberA < 0 || message.numberA >= rules::MaxPlayers)
1481 return;
1482
1483 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1484 if (player >= state.numberOfPlayers ||
1486 !profileRuntime.playerLoaded[player] || debtActionInFlight[player] ||
1487 state.tradeInProgress ||
1488 tradeIngress.counterRuntime.sending.state !=
1490 return;
1491
1492 debtMoneyOwed[player] = message.numberC > 0 ? message.numberC : 0;
1493 auto context = makeConfigContext();
1494
1495 if (message.numberB < 0 ||
1496 message.numberB > static_cast<std::int64_t>(rules::BankPlayer))
1497 {
1498 debtMoneyOwed[player] = 0;
1499 return;
1500 }
1501 const auto creditor = static_cast<rules::PlayerNumber>(message.numberB);
1502 for (rules::PlayerNumber current = 0;
1504 context.localAIPlayer[current] = context.localAIPlayer[current] &&
1505 profileRuntime.playerLoaded[current];
1506
1508 state, player, false);
1509 if (first.acted())
1510 {
1512 return;
1513 }
1514
1516 state, player, creditor,
1518 return;
1519
1521 state, player, true);
1522 if (lastResort.acted())
1523 {
1525 return;
1526 }
1527
1529 state, player, creditor,
1531 return;
1532
1535 player, rules::BankPlayer))
1536 debtActionInFlight[player] = true;
1537 }
1538
1540 const rules::GameState& state, const actions::Message& message) noexcept
1541 {
1542 if (message.action != actions::Type::Tick &&
1543 !(message.action == actions::Type::NotifyStartTurn &&
1544 !state.options.aiTakesTimeToThink))
1545 return;
1546 auto context = makeConfigContext();
1548 context.localAIPlayer[current] = context.localAIPlayer[current] &&
1549 profileRuntime.playerLoaded[current];
1550 for (rules::PlayerNumber player = 0; player < state.numberOfPlayers; ++player)
1551 {
1552 if (pendingTurnPrompt[player] != PendingTurnPrompt::None ||
1553 turnActionInFlight[player])
1554 continue;
1555 if (tryProposeAutonomousTradeForPlayer(state, player, context))
1556 return;
1557 }
1558 }
1559
1561 rules::PlayerNumber player) noexcept
1562 {
1563 if (player >= rules::MaxPlayers || turnActionInFlight[player])
1564 return false;
1565 const auto pending = pendingTurnPrompt[player];
1566 if (pending == PendingTurnPrompt::None)
1567 return false;
1568 const auto action = pending == PendingTurnPrompt::RollDice
1570 if (!messaging::sendAction(action, player, rules::BankPlayer))
1571 return false;
1572 pendingTurnPrompt[player] = PendingTurnPrompt::None;
1573 turnActionInFlight[player] = true;
1574 return true;
1575 }
1576
1578 const rules::GameState& state, rules::PlayerNumber player) noexcept
1579 {
1580 if (player >= state.numberOfPlayers ||
1581 pendingTurnPrompt[player] == PendingTurnPrompt::None ||
1583 !profileRuntime.playerLoaded[player])
1584 return;
1585 if (economicBusy(player) ||
1586 tradeIngress.counterRuntime.sending.state != trade::SendingTradeState::Nothing)
1587 return;
1588 // A queued prompt belongs to the phase that emitted it. Modal
1589 // notifications suspend it until RULE emits a fresh turn prompt.
1590 if (turnPromptPaused[player] ||
1591 buySellMortgagePlayer(state) != rules::NobodyPlayer ||
1592 auctionOn || housingShortageOn || buildingPlacementOn ||
1593 state.tradeInProgress || playerPayingDebt(state, player))
1594 return;
1595 auto context = makeConfigContext();
1597 context.localAIPlayer[current] = context.localAIPlayer[current] &&
1598 profileRuntime.playerLoaded[current];
1599
1600 if (!autonomousTradeBlocked(state, player) &&
1601 tryProposeAutonomousTradeForPlayer(state, player, context))
1602 return;
1603
1604 if (!autonomousTradeBlocked(state, player))
1605 {
1606 const auto next = nextEconomicDecision(state, player, context);
1607 if (!next.defer && next.plan.acted())
1608 {
1609 if (queueEconomicAction(state, player, next.plan))
1610 return;
1611 return;
1612 }
1613 }
1614 (void)sendPendingTurnAction(player);
1615 }
1616
1618 const rules::GameState& state, const actions::Message& message) noexcept
1619 {
1620 if (message.action == actions::Type::NotifyActionCompleted &&
1621 message.numberC >= 0 && message.numberC < rules::MaxPlayers)
1622 {
1623 const auto completed = static_cast<actions::Type>(message.numberA);
1624 if (completed == actions::Type::RollDice || completed == actions::Type::EndTurn)
1625 {
1626 const auto player = static_cast<rules::PlayerNumber>(message.numberC);
1627 turnActionInFlight[player] = false;
1628 pendingTurnPrompt[player] = PendingTurnPrompt::None;
1629 }
1630 }
1631
1632 if (message.action == actions::Type::NotifyPleaseRollDice ||
1633 message.action == actions::Type::NotifyEndTurn)
1634 {
1635 if (message.numberA < 0 || message.numberA >= state.numberOfPlayers)
1636 return;
1637 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1638 if (turnActionInFlight[player])
1639 return;
1640 turnPromptPaused[player] = false;
1641 pendingTurnPrompt[player] = message.action == actions::Type::NotifyPleaseRollDice
1642 ? PendingTurnPrompt::RollDice : PendingTurnPrompt::EndTurn;
1644 return;
1645 }
1646
1647 if (message.action == actions::Type::NotifyTradeFinished ||
1648 (message.action == actions::Type::NotifyPlayerBuySellMort &&
1649 message.numberA == rules::NobodyPlayer) ||
1650 message.action == actions::Type::Tick)
1651 {
1652 for (rules::PlayerNumber player = 0; player < state.numberOfPlayers; ++player)
1654 }
1655 }
1656
1658 rules::PlayerNumber player, bool accept, std::int64_t status) noexcept
1659 {
1662 accept ? 1 : 0, status))
1663 return false;
1664 tradeIngress.pendingTradeAcceptPlayers |= 1u << player;
1665 return true;
1666 }
1667
1669 const rules::GameState& state,
1670 const actions::Message& message) noexcept
1671 {
1672 if (message.action != actions::Type::NotifyTradeAcceptanceDecision ||
1673 message.numberA < 0 || tradeIngress.tradeJustRejectedCountered)
1674 return;
1675
1676 const auto requested = static_cast<std::uint32_t>(message.numberA);
1677 const auto context = makeConfigContext();
1678 for (rules::PlayerNumber player = 0;
1679 player < state.numberOfPlayers; ++player)
1680 {
1681 const auto playerBit = 1u << player;
1682 if ((requested & playerBit) == 0 ||
1684 !profileRuntime.playerLoaded[player])
1685 continue;
1686
1687 // Ai.cpp:3095-3108 waits for every outstanding action of an
1688 // involved player. Uninvolved AIs may still accept immediately.
1689 if (trade::playerInvolvedInTrade(tradeIngress.currentTrade[player]) &&
1690 pendingActionsForPlayer(player) != 0)
1691 {
1692 if ((tradeIngress.pendingTradeAcceptPlayers & playerBit) == 0)
1693 tradeIngress.deferredAcceptancePlayers |= playerBit;
1694 return;
1695 }
1696
1697 const auto config = makeAcceptanceConfig(player, context);
1698 const auto decision = trade::evaluateCurrentTradeAcceptance(
1699 state, player, config, tradeIngress);
1700 switch (decision.status)
1701 {
1704 return;
1705
1707 if ((tradeIngress.pendingTradeAcceptPlayers & playerBit) == 0)
1708 tradeIngress.deferredAcceptancePlayers |= playerBit;
1709 return;
1710
1712 (void)sendTradeAcceptance(player, true, 3);
1713 continue;
1714
1717 (void)sendTradeAcceptance(player, false, 0);
1718 return;
1719
1721 (void)sendTradeAcceptance(player, true, 1);
1722 continue;
1723
1725 break;
1726 }
1727
1728 tradeIngress.tradeJustRejectedCountered = true;
1729 tradeIngress.playerJustRejectedCountered = player;
1731 profileRuntime.players, player, context);
1733 profileRuntime.players, player, context);
1735 state, player, true,
1736 tradeIngress.counterSessions[player].timesCounteredTrade == 0
1737 ? retailCounterRoll() : 0.0,
1738 pendingActionsForPlayer(player), auctionOn,
1740 if (!counter.acted())
1741 (void)sendTradeAcceptance(player, false, 0);
1742 return;
1743 }
1744 }
1745
1747 const actions::Message& message) noexcept
1748 {
1749 const auto restart = [](rules::PlayerNumber player)
1750 {
1751 const auto bit = 1u << player;
1752 if ((tradeIngress.deferredAcceptancePlayers & bit) == 0 ||
1753 pendingActionsForPlayer(player) != 0)
1754 return;
1757 tradeIngress.deferredAcceptancePlayers &= ~bit;
1758 };
1759 // Retry a full outbound queue without losing the deferred request.
1760 if (message.action == actions::Type::Tick)
1761 {
1762 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
1763 restart(player);
1764 return;
1765 }
1766 if (message.action != actions::Type::NotifyActionCompleted ||
1767 message.numberC < 0 || message.numberC >= rules::MaxPlayers)
1768 return;
1769 restart(static_cast<rules::PlayerNumber>(message.numberC));
1770 }
1771
1772 void maybeCounterTrade(
1773 const rules::GameState& state,
1774 const actions::Message& message) noexcept
1775 {
1776 if (message.action != actions::Type::NotifyTradeEditor ||
1777 message.numberA < 0 || message.numberA >= rules::MaxPlayers)
1778 return;
1779
1780 const auto player = static_cast<rules::PlayerNumber>(message.numberA);
1782 !profileRuntime.playerLoaded[player] ||
1783 tradeIngress.counterRuntime.sending.state !=
1785 return;
1786
1787 const auto context = makeConfigContext();
1789 profileRuntime.players, player, context);
1791 profileRuntime.players, player, context);
1792
1793 // With tradeAccept=false the retail probability roll is not read.
1794 const auto result = trade::counterProposeCurrentTrade(
1795 state, player, false, 0.0, pendingActionsForPlayer(player), auctionOn,
1797 if (!result.acted())
1798 {
1801 2, 0);
1802 }
1803 }
1804 }
1805
1806 std::expected<void, profile::Error> initializeMessageIngressProfiles(
1807 const std::filesystem::path& directory)
1808 {
1810 }
1811
1813 {
1815 economicRuntime = {};
1816 jailDecisionInFlight.fill(false);
1817 taxDecisionInFlight.fill(false);
1818 buyDecisionInFlight.fill(false);
1819 auctionBidInFlight = false;
1822 buildingPlacementInFlight.fill(false);
1823 debtActionInFlight.fill(false);
1824 debtMoneyOwed.fill(0);
1825 freeUnmortgageInFlight.fill(false);
1826 freeUnmortgageDoneInFlight.fill(false);
1827 pendingTurnPrompt.fill(PendingTurnPrompt::None);
1828 turnActionInFlight.fill(false);
1829 turnPromptPaused.fill(false);
1831 housingShortageOn = false;
1832 buildingPlacementOn = false;
1833 configurationAcceptInFlight.fill(false);
1834 cardSeenInFlight.fill(false);
1835 auctionOn = false;
1837
1838 for (rules::PlayerNumber player = 0;
1839 player < rules::MaxPlayers;
1840 ++player)
1841 {
1843 }
1844 }
1845
1847 const rules::GameState& state,
1848 const actions::Message& message) noexcept
1849 {
1850 if (!ui::localplayers::isLocalRecipient(message.toPlayer))
1851 return;
1852
1853 updateRuntimeFlags(message);
1855 processProfileMessage(state, message);
1859 state, message, tradeIngress);
1861 maybeCounterTrade(state, message);
1865 processAuctionMessage(state, message);
1867 processDebtMessage(state, message);
1871 // Completion handlers above must release their action flags before a
1872 // deferred trade decision is allowed to restart the current phase.
1876 }
1877
1878 const trade::TradeIngressState&
1883
1884 const profile::RuntimeState&
1889}
std::array< decision::EconomicActionPlan, rules::MaxPlayers > pendingControlAction
bool decompositionControl
std::array< bool, rules::MaxPlayers > controlReleaseInFlight
std::size_t count
std::array< rules::PlayerNumber, rules::MaxPlayers > players
std::array< bool, rules::MaxPlayers > controlRestartInFlight
std::array< bool, rules::MaxPlayers > actionInFlight
std::array< bool, rules::MaxPlayers > controlRequestInFlight
bool defer
rules::PlayerNumber controlOwner
std::array< bool, rules::MaxPlayers > hasPendingControlAction
decision::EconomicActionPlan plan
std::size_t offset
std::uint64_t key
unsigned state
unsigned index
data::DataTag first
Kind kind
std::filesystem::path directory
EconomicActionPlan planBuyHouseAction(const rules::GameState &state, rules::PlayerNumber player, CashStrategy strategy, std::int64_t minCashOnHand, std::uint8_t housingPurchaseStrategy, std::int64_t moneyOwed) noexcept
EconomicActionPlan planDebtLiquidationStep(const rules::GameState &state, rules::PlayerNumber player, bool sellHouses) noexcept
bool shouldGiveAwayMonopoly(const rules::GameState &state, rules::PlayerNumber player, CashStrategy strategy, std::int64_t minCashOnHand, int maxHousesPerSquareForGiveAway, std::span< const std::int64_t > moneyOwed) noexcept
bool buildSemiImportantTrade(const rules::GameState &state, rules::PlayerNumber player, std::span< const double > playerAttitudes, const ai::trade::PropertySets &properties, const SemiImportantTradeInputs &inputs, ai::trade::TradeProposalList &proposals, const MonopolyProposalConfig &config) noexcept
HousePurchaseDecision shouldBuyHouse(const rules::GameState &state, rules::PlayerNumber player, CashStrategy strategy, std::int64_t minCashOnHand, std::uint8_t housingPurchaseStrategy, std::int64_t moneyOwed) noexcept
bool shouldBuyProperty(const rules::GameState &state, rules::PlayerNumber player, rules::board::SquareType property, CashStrategy strategy, std::int64_t minCashOnHand, std::span< const std::int64_t > moneyOwed) noexcept
std::optional< rules::board::SquareType > chooseHousingAuctionSquare(const rules::GameState &state, rules::PlayerNumber player, HousingAuctionBuilding building, CashStrategy strategy, std::int64_t minCashOnHand, rules::board::PropertySet legalSquares, bool reservedBuilding, std::int64_t moneyOwed) noexcept
EconomicActionPlan planUnmortgagePropertyAction(const rules::GameState &state, rules::PlayerNumber player, CashStrategy strategy, std::int64_t minCashOnHand, std::int64_t moneyOwed) noexcept
std::int64_t bidForAuctionItem(const rules::GameState &state, rules::PlayerNumber player, rules::board::SquareType item, std::int64_t currentBid, rules::PlayerNumber currentBidder, const AuctionBidConfig &config, bool useTenDollarIncrement) noexcept
std::optional< bool > chooseFractionTax(const rules::GameState &state, rules::PlayerNumber player) noexcept
EconomicActionPlan planHouseSaleAction(const rules::GameState &state, rules::board::SquareType referenceSquare) noexcept
ProactiveTradeResult buildProactiveTrade(const rules::GameState &state, rules::PlayerNumber player, std::span< const double > playerAttitudes, const ai::trade::PropertySets &properties, const ProactiveTradeInputs &inputs, const MonopolyProposalConfig &config) noexcept
bool shouldUnmortgageProperty(const rules::GameState &state, rules::PlayerNumber player, CashStrategy strategy, std::int64_t minCashOnHand, std::int64_t moneyOwed) noexcept
std::optional< JailExitChoice > chooseJailExitChoice(const rules::GameState &state, rules::PlayerNumber player, bool canRollDoubles, bool canPayFee, bool canUseCard) noexcept
std::expected< void, Error > initializeRuntime(RuntimeState &state, const std::filesystem::path &directory)
decision::CounterProposalBalanceConfig makeCounterBalanceConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
void clearPlayerRuntime(RuntimeState &state, rules::PlayerNumber player) noexcept
std::expected< void, Error > loadPlayerRuntime(RuntimeState &state, rules::PlayerNumber player, std::uint8_t token, std::uint8_t level)
decision::MonopolyProposalConfig makeMonopolyProposalConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
decision::TradeEvaluationConfig makeTradeEvaluationConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
decision::CounterProposalPreflightConfig makeCounterPreflightConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
bool encode(const State &state, std::vector< std::uint8_t > &result) noexcept
bool decode(std::span< const std::uint8_t > data, State &result) noexcept
constexpr std::uint8_t TradeForCash
bool tradeIsProper(const rules::GameState &state, const TradeProposalList &proposals, std::span< const FutureImmunityRecord > immunities) noexcept
void resetTradeIngress(TradeIngressState &state) noexcept
bool playerInvolvedInTrade(const TradeProposalRecord &proposal) noexcept
constexpr std::uint8_t TradeSomewhatImportant
std::array< rules::board::SquareGroup, 8 > orderMonopolyImportance(std::int64_t liquidAssets, MonopolySortOrder order, std::span< const std::uint32_t > randomKeys) noexcept
std::array< double, rules::MaxPlayers > tradeResponseAttitudeChanges(const rules::GameState &gameState, rules::PlayerNumber proposer, rules::PlayerNumber respondingPlayer, bool accepted, const TradeIngressState &state, const profile::ProfileSet &profiles, const profile::ConfigContext &context) noexcept
constexpr std::uint8_t TradeDesperate
bool sendProactiveTrade(const rules::GameState &gameState, rules::PlayerNumber player, const TradeProposalList &proposal, const profile::Profile &strategy, TradeIngressState &state) noexcept
bool shouldTrade(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t importance, std::span< const std::int64_t > timeLastTrade, std::size_t maxTrades, const TradeCadenceInputs &inputs) noexcept
std::array< rules::board::PropertySet, rules::MaxPlayers > PropertySets
void advanceTradeTurn(std::uint8_t numberOfPlayers, profile::ProfileSet &profiles, const std::array< bool, rules::MaxPlayers > &localAIPlayers, TradeIngressState &state) noexcept
CounterTradeRunResult counterProposeCurrentTrade(const rules::GameState &gameState, rules::PlayerNumber player, bool tradeAccept, double counterRoll, int pendingActions, bool auctionOn, const decision::CounterProposalPreflightConfig &preflightConfig, const decision::CounterProposalBalanceConfig &balanceConfig, TradeIngressState &state) noexcept
std::array< TradeProposalRecord, rules::MaxPlayers > TradeProposalList
TradeAcceptanceResult evaluateCurrentTradeAcceptance(const rules::GameState &gameState, rules::PlayerNumber player, const TradeAcceptanceConfig &config, const TradeIngressState &state) noexcept
bool processTradeRuleMessage(const rules::GameState &gameState, const actions::Message &message, TradeIngressState &state) noexcept
const profile::RuntimeState & profileRuntimeStateReadOnly() noexcept
std::expected< void, profile::Error > initializeMessageIngressProfiles(const std::filesystem::path &directory)
std::int64_t liquidAssets(const rules::GameState &state, rules::PlayerNumber player, bool countHouses, bool countMonopolies, std::int64_t moneyOwed) noexcept
const trade::TradeIngressState & tradeIngressStateReadOnly() noexcept
void processMessage(const rules::GameState &state, const actions::Message &message) noexcept
void resetMessageIngress() noexcept
rules::board::PropertySet propertiesOwnedByPlayer(const rules::GameState &state, rules::PlayerNumber player) noexcept
Definition AIUtility.cpp:49
constexpr std::int64_t ErrorTradeAccepted
constexpr std::int64_t ErrorTradeChanging
constexpr std::int64_t ErrorTradeRejected
bool sendAction(const actions::Message &message)
std::uint32_t PropertySet
bool acceptedConfigurationMessage(const GameOptions &options, PlayerNumber fromPlayer, bool interim, actions::Message &result)
const PendingPhase & current(const GameState &state)
constexpr PlayerNumber BankPlayer
Definition RuleTypes.hpp:15
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber NobodyPlayer
Definition RuleTypes.hpp:16
bool requestRemoveLocalPlayer(const GameState &uiState, PlayerNumber player)
bool slotIsLocalAIPlayer(PlayerNumber slot)
bool isLocalRecipient(PlayerNumber recipient)