Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
StatsUI.cpp
Go to the documentation of this file.
1#include "StatsUI.hpp"
2
3#include "AIUtility.hpp"
4#include "BoardRules.hpp"
5
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <cstdint>
10
11namespace monopoly::statsui
12{
13 namespace
14 {
15 template <std::size_t N>
17 std::array<std::int64_t, N>& values,
18 std::array<std::uint8_t, N>& indices,
19 std::size_t count) noexcept
20 {
21 for (std::size_t i = 0; i < count; ++i)
22 {
23 std::size_t maximum = i;
24 for (std::size_t j = i; j < count; ++j)
25 {
26 if (values[j] > values[maximum]) maximum = j;
27 }
28 std::swap(values[i], values[maximum]);
29 std::swap(indices[i], indices[maximum]);
30 }
31 }
32
33 void refreshPlayers(State& state, const rules::GameState& gameState) noexcept
34 {
35 const auto count = std::min<std::size_t>(
36 gameState.numberOfPlayers, rules::MaxPlayers);
37 state.playerCount = count;
38 state.playerMetric.fill(0);
39 for (std::size_t i = 0; i < rules::MaxPlayers; ++i)
40 state.playerOrder[i] = static_cast<rules::PlayerNumber>(i);
41
42 if (state.activeSort == 0 || count == 0) return;
43
44 std::array<std::int64_t, rules::MaxPlayers> values{};
45 std::array<std::uint8_t, rules::MaxPlayers> indices{};
46 for (std::size_t i = 0; i < rules::MaxPlayers; ++i)
47 indices[i] = static_cast<std::uint8_t>(i);
48
49 for (std::size_t i = 0; i < count; ++i)
50 {
51 const auto player = static_cast<rules::PlayerNumber>(i);
52 switch (state.activeSort)
53 {
54 case 1: values[i] = ai::totalWorth(gameState, player); break;
55 case 2: values[i] = ai::potentialIncome(gameState, player); break;
56 case 3: values[i] = gameState.players[i].cash; break;
57 default: return;
58 }
59 }
60
61 selectionSortDescending(values, indices, count);
62 for (std::size_t i = 0; i < count; ++i)
63 {
64 state.playerOrder[i] = indices[i];
65 state.playerMetric[i] = values[i];
66 }
67 }
68
69 void refreshDeeds(State& state, const rules::GameState& gameState) noexcept
70 {
71 std::array<std::int64_t, rules::SquareCount> values{};
72 std::array<std::uint8_t, rules::SquareCount> indices{};
73 for (std::size_t i = 0; i < rules::SquareCount; ++i)
74 {
75 indices[i] = static_cast<std::uint8_t>(i);
76 const auto square = static_cast<rules::board::SquareType>(i);
77 switch (state.activeSort)
78 {
79 case 0:
80 values[i] = rules::board::definition(square).purchaseCost;
81 break;
82 case 1:
83 values[i] = gameState.squares[i].owner;
84 break;
85 case 2:
86 {
87 const auto owner = gameState.squares[i].owner < rules::MaxPlayers
88 ? gameState.squares[i].owner : rules::BankPlayer;
89 values[i] = ai::rentIfSteppedOn(
91 break;
92 }
93 case 3:
94 values[i] = gameState.squares[i].gameEarnings;
95 break;
96 default:
97 return;
98 }
99 }
100
101 if (state.activeSort == 1)
102 {
103 // The source uses a stable bubble sort with a strict > test.
104 for (std::size_t pass = 0; pass < rules::SquareCount; ++pass)
105 {
106 for (std::size_t i = 0; i + 1 < rules::SquareCount; ++i)
107 {
108 if (values[i] > values[i + 1])
109 {
110 std::swap(values[i], values[i + 1]);
111 std::swap(indices[i], indices[i + 1]);
112 }
113 }
114 }
115 }
116 else
117 {
119 }
120
121 for (std::size_t i = 0; i < rules::SquareCount; ++i)
122 {
123 state.deedOrder[i] = indices[i];
124 state.deedMetric[i] = values[i];
125 }
126 }
127
128 void refreshBank(State& state, const rules::GameState& gameState) noexcept
129 {
130 state.bankPlayerHouses.fill(0);
131 state.bankPlayerHotels.fill(0);
132 state.bankDeeds.fill(BankDeedState::Hidden);
133 state.bankHousesRemaining = gameState.options.maximumHouses;
134 state.bankHotelsRemaining = gameState.options.maximumHotels;
135 state.activeDatasetAvailable = true;
136
137 if (state.activeSort == 0)
138 {
139 for (std::size_t squareIndex = 0;
141 {
142 const auto square =
144 if (!rules::board::isOwnable(square)) continue;
145
146 const auto owner = gameState.squares[squareIndex].owner;
147 if (owner >= gameState.numberOfPlayers ||
148 owner >= rules::MaxPlayers)
149 {
150 continue;
151 }
152
153 const int houses = gameState.squares[squareIndex].houses;
154 if (houses <= 0) continue;
155 if (houses == gameState.options.housesPerHotel)
156 ++state.bankPlayerHotels[owner];
157 else
158 state.bankPlayerHouses[owner] += houses;
159 }
160
161 for (std::size_t player = 0; player < rules::MaxPlayers; ++player)
162 {
163 state.bankHousesRemaining -= state.bankPlayerHouses[player];
164 state.bankHotelsRemaining -= state.bankPlayerHotels[player];
165 }
166 return;
167 }
168
169 if (state.activeSort == 1)
170 {
171 for (std::size_t squareIndex = 0;
173 {
174 const auto square =
176 if (!rules::board::isOwnable(square)) continue;
177
178 const auto& squareState = gameState.squares[squareIndex];
179 if (squareState.mortgaged)
181 else if (squareState.owner < rules::MaxPlayers)
183 else
185 }
186 }
187 }
188
189 [[nodiscard]] constexpr bool propertyActionMode(ibar::RuleMode mode) noexcept
190 {
191 return mode == ibar::RuleMode::Build || mode == ibar::RuleMode::Sell ||
193 }
194
195 [[nodiscard]] bool propertyEligible(const State& state,
196 const rules::GameState& gameState, int square) noexcept
197 {
198 if (state.propertyActionPlayer >= gameState.numberOfPlayers ||
199 state.propertyActionPlayer >= rules::MaxPlayers || square < 0 ||
200 square >= static_cast<int>(rules::SquareCount) ||
201 gameState.squares[static_cast<std::size_t>(square)].owner !=
202 state.propertyActionPlayer)
203 return false;
204 const auto bit = ibar::layout::propertyBit(square);
205 if (bit == 0) return false;
206 switch (state.propertyActionMode)
207 {
208 case ibar::RuleMode::Build: return (state.buildProperties & bit) != 0;
209 case ibar::RuleMode::Sell: return (state.sellProperties & bit) != 0;
210 case ibar::RuleMode::Mortgage: return (state.mortgageProperties & bit) != 0;
212 return gameState.squares[static_cast<std::size_t>(square)].mortgaged;
213 default: return false;
214 }
215 }
216
217 [[nodiscard]] std::optional<int> playerPropertyHit(const State& state,
218 const rules::GameState& gameState, int mouseX, int mouseY) noexcept
219 {
220 const auto count = std::min<std::size_t>(state.playerCount,
221 std::min<std::size_t>(gameState.numberOfPlayers, rules::MaxPlayers));
222 for (std::size_t column = 0; column < count; ++column)
223 {
224 if (state.playerOrder[column] != state.propertyActionPlayer) continue;
225 int bestSquare = -1;
226 int bestPriority = -1;
227 for (int square = 0; square < static_cast<int>(rules::SquareCount); ++square)
228 {
229 if (!propertyEligible(state, gameState, square)) continue;
230 const int order = ibar::layout::propertyBarOrder(square);
231 if (order < 0) continue;
232 const auto rect = playerPropertyRect(state, gameState, column, square);
233 if (rect && rect->contains(mouseX, mouseY))
234 {
235 const int priority = 510 + order;
237 {
239 bestSquare = square;
240 }
241 }
242 }
243 if (bestSquare >= 0) return bestSquare;
244 return std::nullopt;
245 }
246 return std::nullopt;
247 }
248
249 [[nodiscard]] std::optional<int> deedPropertyHit(const State& state,
250 const rules::GameState& gameState, int mouseX, int mouseY) noexcept
251 {
252 int displayed = 0;
253 for (std::size_t rank = 0; rank < rules::SquareCount; ++rank)
254 {
255 const int square = state.deedOrder[rank];
256
257 if (square < 0 || square >= static_cast<int>(rules::SquareCount) ||
258 ibar::layout::propertyIndex(square) < 0)
259 continue;
260 if (state.activeSort == 3 && state.deedMetric[rank] == 0) continue;
261 if (!propertyEligible(state, gameState, square)) continue;
262 const int x = 22 + 110 * (displayed % 7);
263 const int y = 234 + 54 * (displayed / 7);
264 if (mouseX >= x && mouseX < x + 36 &&
265 mouseY >= y && mouseY < y + 42)
266 return square;
267 ++displayed;
268 }
269 return std::nullopt;
270 }
271 }
272
273 void reset(State& state) noexcept
274 {
275 state = {};
276 for (std::size_t i = 0; i < rules::SquareCount; ++i)
277 state.deedOrder[i] = static_cast<std::uint8_t>(i);
278 for (std::size_t i = 0; i < rules::MaxPlayers; ++i)
279 state.playerOrder[i] = static_cast<rules::PlayerNumber>(i);
280 }
281
283 {
284 state.activeSort = state.lastSort[static_cast<std::size_t>(state.screen)];
285 state.activeDatasetAvailable = true;
286 switch (state.screen)
287 {
290 case Screen::Bank:
292 break;
293 }
294 state.initialized = true;
295 }
296
299 display::Screen2D view) noexcept
300 {
301 const bool visible = view == display::Screen2D::Portfolio;
302 if (visible && !state.portfolioVisible)
304 state.portfolioVisible = visible;
305 if (!visible) state.mouseKnown = false;
306 }
307
309 State& state, Screen screen, const rules::GameState& gameState) noexcept
310 {
311 state.screen = screen;
313 return true;
314 }
315
317 State& state, std::uint8_t sortIndex,
318 const rules::GameState& gameState) noexcept
319 {
320 if (sortIndex >= 4) return false;
321 state.lastSort[static_cast<std::size_t>(state.screen)] = sortIndex;
323 return true;
324 }
325
326 std::optional<Screen> categoryHit(int x, int y) noexcept
327 {
328 for (std::uint8_t i = 0; i < 3; ++i)
329 {
330 if (categoryRect(i).contains(x, y)) return static_cast<Screen>(i);
331 }
332 return std::nullopt;
333 }
334
335 std::optional<std::uint8_t> sortHit(int x, int y) noexcept
336 {
337 for (std::uint8_t i = 0; i < 4; ++i)
338 {
339 if (sortRect(i).contains(x, y)) return i;
340 }
341 return std::nullopt;
342 }
343
345 rules::PlayerNumber player, bool localHuman,
349 bool modernPlayerLayout) noexcept
350 {
351 state.modernPlayerLayout = modernPlayerLayout;
352 if (!modernPlayerLayout) state.modernJailLayoutQualified = false;
353 state.propertyActionMode = mode;
354 state.propertyActionPlayer = player;
355 state.propertyActionPlayerLocalHuman = localHuman;
356 state.buildProperties = buildProperties;
357 state.sellProperties = sellProperties;
358 state.mortgageProperties = mortgageProperties;
359 }
360
362 const rules::GameState& gameState, rules::PlayerNumber player) noexcept
363 {
364 const auto count = std::min<std::size_t>(state.playerCount,
365 std::min<std::size_t>(gameState.numberOfPlayers, rules::MaxPlayers));
366 if (!state.modernPlayerLayout || count <= 4 || player >= gameState.numberOfPlayers ||
367 player >= rules::MaxPlayers) return false;
368 int ownedCount = 0;
369 for (int square = 0; square < static_cast<int>(rules::SquareCount); ++square)
370 if (gameState.squares[static_cast<std::size_t>(square)].owner == player &&
372 if (std::any_of(gameState.countHits.begin(), gameState.countHits.end(), [&](const auto& hit)
373 { return hit.toPlayer == player && (hit.hitType == rules::CountHitType::FutureRent ||
374 hit.hitType == rules::CountHitType::RentImmunity); })) return false;
375 const bool jail = std::any_of(gameState.cards.begin(), gameState.cards.end(),
376 [&](const auto& card) { return card.jailOwner == player; });
377 return jail ? state.modernJailLayoutQualified && ownedCount <= 6 : ownedCount <= 9;
378 }
379
380 std::optional<Rect> playerPropertyRect(
381 const State& state, const rules::GameState& gameState,
382 std::size_t column, int square) noexcept
383 {
384 const auto count = std::min<std::size_t>(state.playerCount,
385 std::min<std::size_t>(gameState.numberOfPlayers, rules::MaxPlayers));
386 if (column >= count || square < 0 || square >= static_cast<int>(rules::SquareCount))
387 return std::nullopt;
388 const auto player = state.playerOrder[column];
389 const int order = ibar::layout::propertyBarOrder(square);
390 if (player >= gameState.numberOfPlayers || player >= rules::MaxPlayers || order < 0 ||
391 gameState.squares[static_cast<std::size_t>(square)].owner != player)
392 return std::nullopt;
393 std::array<bool, 11> counted{};
394 int rank = 0;
396 {
397 if (gameState.squares[static_cast<std::size_t>(other)].owner != player) continue;
399 if (otherOrder < 0) continue;
400 if (otherOrder < order) ++rank;
401 counted[static_cast<std::size_t>(otherOrder / 3)] = true;
402 }
403 const int columnIndex = static_cast<int>(column);
405 {
406 // Local BSSM displays only its player's box; its gap remains 3.
407 const int gap = state.propertyActionPlayerLocalHuman &&
408 propertyActionMode(state.propertyActionMode) ? 3 : 3 + 3 * columnIndex;
409 const int x = columnIndex * 130 + gap + 5 + (rank % 3) * 42;
410 const int y = 224 + 84 + (rank / 3) * 44;
411 return Rect{x, y, x + 36, y + 42};
412 }
413 std::array<int, 11> compressed{};
414 compressed.fill(-1);
415 int columns = 0;
416 for (std::size_t index = 0; index < counted.size(); ++index)
418 const int width = count > 4 ? 120 : 130;
419 const int apart = columns == 0 ? 0 : std::min((width - 8 - 36) / columns, 72);
420 const int x = columnIndex * (count > 4 ? 130 : 198) + 3 + 3 * columnIndex +
421 (count > 4 ? 5 : 61) + compressed[static_cast<std::size_t>(order / 3)] * apart +
422 4 * (order % 3);
423 const int y = (count > 4 ? 300 : 275) + 20 * (order % 3);
424 return Rect{x, y, x + 36, y + 42};
425 }
426
427 std::optional<int> propertyActionHit(const State& state,
428 const rules::GameState& gameState, int x, int y) noexcept
429 {
430 if (!state.portfolioVisible || !state.propertyActionPlayerLocalHuman ||
431 !propertyActionMode(state.propertyActionMode))
432 return std::nullopt;
433 if (state.screen == Screen::Player)
435 if (state.screen == Screen::Deed)
436 return deedPropertyHit(state, gameState, x, y);
437 return std::nullopt;
438 }
439
441 const uimsg::Message& message) noexcept
442 {
443 if (view != display::Screen2D::Portfolio || state.screen != Screen::Bank ||
444 state.activeSort != 3)
445 {
446 state.historyArrowPressed = -1;
447 return 0;
448 }
449 if (message.type == uimsg::Type::MouseLeftUp)
450 state.historyArrowPressed = -1;
451 if (message.type != uimsg::Type::MouseLeftDown) return 0;
452 const int x = static_cast<int>(message.numberA);
453 const int y = static_cast<int>(message.numberB);
454 for (int arrow = 0; arrow < 2; ++arrow)
455 {
456 const Rect rect{745, 402 - arrow * 119, 763, 428 - arrow * 119};
457 if (!rect.contains(x, y)) continue;
458 state.historyArrowPressed = arrow;
459 return arrow == 0 ? 1 : -1;
460 }
461 state.historyArrowPressed = -1;
462 return 0;
463 }
464
467 display::Screen2D view, const uimsg::Message& message) noexcept
468 {
469 syncView(state, gameState, view);
470 if (!state.portfolioVisible) return false;
471 if (message.type == uimsg::Type::MouseMoved)
472 {
473 state.mouseX = static_cast<int>(message.numberA);
474 state.mouseY = static_cast<int>(message.numberB);
475 state.mouseKnown = true;
476 return false;
477 }
478 if (message.type != uimsg::Type::MouseLeftDown) return false;
479
480 const int x = static_cast<int>(message.numberA);
481 const int y = static_cast<int>(message.numberB);
482 if (const auto category = categoryHit(x, y))
484 if (const auto sort = sortHit(x, y))
485 return selectSort(state, *sort, gameState);
486 return false;
487 }
488}
std::size_t count
ibar::layout::PropertyMask mortgageProperties
Definition Engine.cpp:279
ibar::layout::PropertyMask sellProperties
Definition Engine.cpp:278
ibar::layout::PropertyMask buildProperties
Definition Engine.cpp:277
std::uint16_t priority
std::uint32_t width
unsigned state
unsigned index
Rect rect
CardTag other
std::int64_t potentialIncome(const rules::GameState &state, rules::PlayerNumber player) noexcept
std::int64_t rentIfSteppedOn(const rules::GameState &state, SquareType square, rules::board::PropertySet propertiesOwned) noexcept
std::int64_t totalWorth(const rules::GameState &state, rules::PlayerNumber player) noexcept
rules::board::PropertySet propertiesOwnedByPlayer(const rules::GameState &state, rules::PlayerNumber player) noexcept
Definition AIUtility.cpp:49
int propertyIndex(int square) noexcept
std::uint32_t PropertyMask
PropertyMask propertyBit(int square) noexcept
int propertyBarOrder(int square) noexcept
const SquareDefinition & definition(SquareType square)
bool isOwnable(SquareType square)
constexpr PlayerNumber BankPlayer
Definition RuleTypes.hpp:15
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t SquareCount
Definition RuleTypes.hpp:24
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
std::optional< std::uint8_t > sortHit(int x, int y) noexcept
Definition StatsUI.cpp:335
constexpr Rect sortRect(std::uint8_t index) noexcept
Definition StatsUI.hpp:84
void refresh(State &state, const rules::GameState &gameState) noexcept
Definition StatsUI.cpp:282
std::optional< int > propertyActionHit(const State &state, const rules::GameState &gameState, int x, int y) noexcept
Definition StatsUI.cpp:427
bool selectSort(State &state, std::uint8_t sortIndex, const rules::GameState &gameState) noexcept
Definition StatsUI.cpp:316
std::optional< Screen > categoryHit(int x, int y) noexcept
Definition StatsUI.cpp:326
void syncView(State &state, const rules::GameState &gameState, display::Screen2D view) noexcept
Definition StatsUI.cpp:297
constexpr Rect categoryRect(std::uint8_t index) noexcept
Definition StatsUI.hpp:78
void reset(State &state) noexcept
Definition StatsUI.cpp:273
std::optional< Rect > playerPropertyRect(const State &state, const rules::GameState &gameState, std::size_t column, int square) noexcept
Definition StatsUI.cpp:380
bool modernPlayerGridActive(const State &state, const rules::GameState &gameState, rules::PlayerNumber player) noexcept
Definition StatsUI.cpp:361
void setPropertyActionContext(State &state, ibar::RuleMode mode, rules::PlayerNumber player, bool localHuman, ibar::layout::PropertyMask buildProperties, ibar::layout::PropertyMask sellProperties, ibar::layout::PropertyMask mortgageProperties, bool modernPlayerLayout) noexcept
Definition StatsUI.cpp:344
int historyScrollInput(State &state, display::Screen2D view, const uimsg::Message &message) noexcept
Definition StatsUI.cpp:440
bool selectCategory(State &state, Screen screen, const rules::GameState &gameState) noexcept
Definition StatsUI.cpp:308
bool processInput(State &state, const rules::GameState &gameState, display::Screen2D view, const uimsg::Message &message) noexcept
Definition StatsUI.cpp:465
constexpr bool contains(int x, int y) const noexcept
Definition StatsUI.hpp:38