Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
UDPennyVoice.cpp
Go to the documentation of this file.
1#include "UDPennyVoice.hpp"
2
3#include <array>
4
6{
7 namespace
8 {
9 using CardTag = std::uint16_t;
10
11 inline constexpr std::array<int, 12> NativeSystemByCity{
12 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 9};
13
14 struct CityCurrencyTags
15 {
19 };
20
21 inline constexpr std::array<CityCurrencyTags, 12> ChanceCard1Tags{{
22 {0x0056,0x0060,0x0061}, {0x0065,0x006F,0x0070},
23 {0x0074,0x007E,0x007F}, {0x0083,0x008D,0x008E},
24 {0x0092,0x009C,0x009D}, {0x00A1,0x00AB,0x00AC},
25 {0x00B0,0x00BA,0x00BB}, {0x00C8,0x00C9,0x00CA},
26 {0x00CE,0x00D8,0x00D9}, {0x00EA,0x00F4,0x00F5},
27 {0x00F9,0x0103,0x0103}, {0x00DD,0x00E7,0x00E7}}};
28 inline constexpr std::array<CityCurrencyTags, 12> ChanceCard7Tags{{
29 {0x005B,0x0062,0x0063}, {0x006A,0x0071,0x0072},
30 {0x0079,0x0080,0x0081}, {0x0088,0x008F,0x0090},
31 {0x0097,0x009E,0x009F}, {0x00A6,0x00AD,0x00AE},
32 {0x00B5,0x00BC,0x00BD}, {0x00C3,0x00CB,0x00CC},
33 {0x00D3,0x00DA,0x00DB}, {0x00EF,0x00F6,0x00F7},
34 {0x00FE,0x0104,0x0104}, {0x00E2,0x00E8,0x00E8}}};
35
37 const std::array<CityCurrencyTags, 12>& table, int city,
38 int monetarySystem) noexcept
39 {
40 if (city < 0 || city >= static_cast<int>(table.size())) return 0;
41 const auto index = static_cast<std::size_t>(city);
42 if (monetarySystem == NativeSystemByCity[index]) return table[index].native;
43 if (monetarySystem == 12) return table[index].euro;
44 return table[index].other;
45 }
46
48 std::uint8_t cardIndex, int city, int monetarySystem) noexcept
49 {
50 static constexpr std::array<std::array<CardTag, 13>, 7> SystemCards{{
51 {{0x0055,0x0064,0x0073,0x0082,0x0091,0x00A0,0x00AF,0x00BE,0x00CD,0x00DC,0x00E9,0x00F8,0x004E}},
52 {{0x0057,0x0066,0x0075,0x0084,0x0093,0x00A2,0x00B1,0x00BF,0x00CF,0x00DE,0x00EB,0x00FA,0x004F}},
53 {{0x0058,0x0067,0x0076,0x0085,0x0094,0x00A3,0x00B2,0x00C0,0x00D0,0x00DF,0x00EC,0x00FB,0x0050}},
54 {{0x0059,0x0068,0x0077,0x0086,0x0095,0x00A4,0x00B3,0x00C1,0x00D1,0x00E0,0x00ED,0x00FC,0x0051}},
55 {{0x005C,0x006B,0x007A,0x0089,0x0098,0x00A7,0x00B6,0x00C4,0x00D4,0x00E3,0x00F0,0x00FF,0x0052}},
56 {{0x005D,0x006C,0x007B,0x008A,0x0099,0x00A8,0x00B7,0x00C5,0x00D5,0x00E4,0x00F1,0x0100,0x0053}},
57 {{0x005F,0x006E,0x007D,0x008C,0x009B,0x00AA,0x00B9,0x00C7,0x00D7,0x00E6,0x00F3,0x0102,0x0054}}}};
58 static constexpr std::array<std::uint8_t, 7> SystemCardIndices{0,3,4,5,10,12,14};
59 if (monetarySystem >= 0 && monetarySystem < 13)
60 for (std::size_t row = 0; row < SystemCardIndices.size(); ++row)
62 return SystemCards[row][static_cast<std::size_t>(monetarySystem)];
63
64 if (cardIndex == 1) return cityCurrencyTag(ChanceCard1Tags, city, monetarySystem);
65 if (cardIndex == 7) return cityCurrencyTag(ChanceCard7Tags, city, monetarySystem);
66 if (city < 0 || city >= 12) return 0;
67 switch (cardIndex)
68 {
69 case 2: return 0x0105;
70 case 6: { static constexpr std::array<CardTag,12> t{0x005A,0x0069,0x0078,0x0087,0x0096,0x00A5,0x00B4,0x00C2,0x00D2,0x00EE,0x00FD,0x00E1}; return t[static_cast<std::size_t>(city)]; }
71 case 8: return 0x0106;
72 case 9: return 0x0107;
73 case 11: return 0x0108;
74 case 13: { static constexpr std::array<CardTag,12> t{0x005E,0x006D,0x007C,0x008B,0x009A,0x00A9,0x00B8,0x00C6,0x00D6,0x00F2,0x0101,0x00E5}; return t[static_cast<std::size_t>(city)]; }
75 case 15: return 0x0109;
76 default: return 0;
77 }
78 }
79
81 std::uint8_t cardIndex, int city, int monetarySystem) noexcept
82 {
83 if (cardIndex < 16 || cardIndex >= 32) return 0;
84 if (cardIndex == 20)
85 return city >= 0 && city < 12 ? static_cast<CardTag>(0x01DC) : 0;
87
88 // dat_lk02 keeps 15 spoken variants per monetary system: files
89 // 01..04 then 06..16. Card 20 is the shared country-dependent 05.
90 static constexpr std::array<CardTag, 13> BaseBySystem{
91 0x0128, 0x0137, 0x0146, 0x0155, 0x0164, 0x0173,
92 0x0182, 0x0191, 0x01A0, 0x01AF, 0x01BE, 0x01CD, 0x010A};
93 const auto offset = static_cast<CardTag>(
94 cardIndex < 20 ? cardIndex - 16 : cardIndex - 17);
95 return static_cast<CardTag>(
96 BaseBySystem[static_cast<std::size_t>(monetarySystem)] + offset);
97 }
98
99 [[nodiscard]] bool validPlayer(
100 const rules::GameState& state,
101 rules::PlayerNumber player) noexcept
102 {
103 return player < rules::MaxPlayers &&
104 player < state.numberOfPlayers;
105 }
106
108 const rules::GameState& state,
109 std::uint8_t square,
110 rules::PlayerNumber player,
111 std::uint8_t assumedProperty) noexcept
112 {
113 if (square == assumedProperty)
114 return player;
115 return state.squares[square].owner;
116 }
117
118 template <std::size_t N>
119 [[nodiscard]] bool ownsGroup(
120 const rules::GameState& state,
121 rules::PlayerNumber player,
122 std::uint8_t assumedProperty,
123 const std::array<std::uint8_t, N>& group) noexcept
124 {
125 for (const auto square : group)
126 if (ownerOf(state, square, player, assumedProperty) != player)
128 return true;
129 }
130
131 [[nodiscard]] std::optional<TokenReaction> wait(
132 udsound::TokenVoiceLine line) noexcept
133 {
134 return TokenReaction{line,
136 false};
137 }
138
139 [[nodiscard]] std::optional<TokenReaction> skip(
140 udsound::TokenVoiceLine line) noexcept
141 {
142 return TokenReaction{line,
144 false};
145 }
146
147 [[nodiscard]] bool isRailroad(std::uint8_t square) noexcept
148 {
149 return square == 5 || square == 15 ||
150 square == 25 || square == 35;
151 }
152 [[nodiscard]] bool isUtility(std::uint8_t square) noexcept
153 {
154 return square == 12 || square == 28;
155 }
156
157 inline constexpr std::array<std::int8_t, 42> PropertyAnnouncementIndex{
158 -1, 0,-1, 1,-1, 2, 3,-1, 4, 5,
159 -1, 6, 7, 8, 9,10,11,-1,12,13,
160 -1,14,-1,15,16,17,18,19,20,21,
161 -1,22,23,-1,24,25,-1,26,-1,27,-1,-1};
162
163 [[nodiscard]] std::optional<TokenReaction> rentImpactReaction(
164 const LandingEconomics& economics) noexcept
165 {
166 if (!economics.valid || economics.totalWorth <= 0 || economics.rent < 0)
167 return std::nullopt;
168 const auto scaledRent = economics.rent * 100;
169 if (scaledRent >= economics.totalWorth * 75)
171 if (scaledRent >= economics.totalWorth * 50)
173 if (scaledRent >= economics.totalWorth * 25)
175 if (scaledRent >= economics.totalWorth * 10)
177 if (scaledRent >= economics.totalWorth * 4)
180 }
181 }
182 std::optional<TurnStartReactions> nextPlayerReactions(
184 std::uint32_t random100, std::optional<std::uint32_t> random8) noexcept
185 {
186 if (!validPlayer(state, player)) return std::nullopt;
187 const auto token = state.players[player].token;
189 if ((random100 % 100u) <= 92u && token < rules::MaxTokens)
190 {
191 hostVoice = static_cast<udsound::PennybagsVoice>(
192 static_cast<std::uint16_t>(udsound::PennybagsVoice::RollDice_Cannon) + token);
193 }
194 TurnStartReactions result{};
197 if (!state.players[player].firstMoveMade)
198 {
201 }
202 else if (state.players[player].aiPlayerLevel != 0 && random8 && (*random8 % 8u) == 0u)
203 {
204 const auto square = state.players[player].currentSquare;
205 result.token = TokenReaction{
206 square > 30 && square < 40
210 }
211 result.host.watchAfterStart = result.token.has_value();
212 return result;
213 }
214
215 std::optional<PennybagsReaction> diceRollPennybagsReaction(
216 std::uint8_t total, bool tokenAnimationsOn) noexcept
217 {
219 return std::nullopt;
220 const auto voice = static_cast<udsound::PennybagsVoice>(
221 static_cast<std::uint16_t>(udsound::PennybagsVoice::SayDiceRoll_2) +
222 static_cast<std::uint16_t>(total - 2u));
223 return PennybagsReaction{voice,
225 }
226
227 std::optional<PennybagsReaction> offBoardPennybagsReaction(
228 bool victory, bool localHuman) noexcept
229 {
230 if (victory)
233 if (localHuman)
236 return std::nullopt;
237 }
238
240 const rules::GameState& state,
241 rules::PlayerNumber player,
242 std::uint8_t property,
243 bool assumePropertyOwned) noexcept
244 {
245 if (!validPlayer(state, player) || property >= 40)
246 return false;
247 const auto assumed = assumePropertyOwned ? property :
248 static_cast<std::uint8_t>(rules::SquareCount);
249
250 if (property >= 1 && property <= 3)
251 return ownsGroup(state, player, assumed,
252 std::array<std::uint8_t, 2>{1, 3});
253 if (property >= 6 && property <= 9)
254 return ownsGroup(state, player, assumed,
255 std::array<std::uint8_t, 3>{6, 8, 9});
256 if (property >= 11 && property <= 14)
257 return ownsGroup(state, player, assumed,
258 std::array<std::uint8_t, 3>{11, 13, 14});
259 if (property >= 16 && property <= 19)
260 return ownsGroup(state, player, assumed,
261 std::array<std::uint8_t, 3>{16, 18, 19});
262 if (property >= 21 && property <= 24)
263 return ownsGroup(state, player, assumed,
264 std::array<std::uint8_t, 3>{21, 23, 24});
265 if (property >= 26 && property <= 29)
266 return ownsGroup(state, player, assumed,
267 std::array<std::uint8_t, 3>{26, 27, 29});
268 if (property >= 31 && property <= 34)
269 return ownsGroup(state, player, assumed,
270 std::array<std::uint8_t, 3>{31, 32, 34});
271 if (property >= 37 && property <= 39)
272 return ownsGroup(state, player, assumed,
273 std::array<std::uint8_t, 2>{37, 39});
274 return false;
275 }
276
277 std::optional<TokenReaction> boughtPropertyReaction(
278 const rules::GameState& state,
279 rules::PlayerNumber player,
280 std::uint8_t property) noexcept
281 {
282 if (!validPlayer(state, player) || property >= 40 ||
283 state.players[player].aiPlayerLevel == 0)
284 return std::nullopt;
285 if (propertyFormsMonopoly(state, player, property, true))
286 return std::nullopt;
287 if (isUtility(property) || isRailroad(property))
289 if (property >= 1 && property <= 9)
291 if (property >= 11 && property <= 19)
293 if (property >= 21 && property <= 29)
295 if (property >= 31 && property <= 39)
297 return std::nullopt;
298 }
299
300 std::optional<TokenReaction> choseAuctionReaction(
301 const rules::GameState& state,
302 rules::PlayerNumber player) noexcept
303 {
304 if (!validPlayer(state, player) ||
305 state.players[player].aiPlayerLevel == 0)
306 return std::nullopt;
308 }
309
310 std::optional<PennybagsReaction> buyOrAuctionPennybagsReaction(
311 const rules::GameState& state, rules::PlayerNumber player) noexcept
312 {
313 if (!validPlayer(state, player) || state.players[player].aiPlayerLevel != 0)
314 return std::nullopt;
317 }
318
319 std::optional<PennybagsReaction> boughtPropertyPennybagsReaction(
321 std::uint8_t property) noexcept
322 {
323 if (!validPlayer(state, player) || property >= 40) return std::nullopt;
324 const auto waitPb = [](udsound::PennybagsVoice voice) {
325 return std::optional<PennybagsReaction>{PennybagsReaction{voice,
327 };
328 if (propertyFormsMonopoly(state, player, property, true))
330 if (state.players[player].aiPlayerLevel != 0) return std::nullopt;
333 if (property >= 1 && property <= 9) return waitPb(udsound::PennybagsVoice::BuyBoardSide1);
334 if (property >= 11 && property <= 19) return waitPb(udsound::PennybagsVoice::BuyBoardSide2);
335 if (property >= 21 && property <= 29) return waitPb(udsound::PennybagsVoice::BuyBoardSide3);
336 if (property >= 31 && property <= 39) return waitPb(udsound::PennybagsVoice::BuyBoardSide4);
337 return std::nullopt;
338 }
339
340 std::optional<PennybagsReaction> goToJailPennybagsReaction(
342 bool localHuman) noexcept
343 {
344 if (!validPlayer(state, player)) return std::nullopt;
345 int ownedSquares = 0;
346 for (const auto& square : state.squares)
347 if (square.owner != rules::NobodyPlayer) ++ownedSquares;
348 if (ownedSquares > 25 || !localHuman) return std::nullopt;
351 }
352
353 std::optional<TokenReaction> goToJailReaction(
354 const rules::GameState& state,
355 rules::PlayerNumber player,
356 bool localHuman) noexcept
357 {
358 if (!validPlayer(state, player))
359 return std::nullopt;
360 int ownedSquares = 0;
361 for (std::size_t square = 0; square < state.squares.size(); ++square)
362 if (state.squares[square].owner != rules::NobodyPlayer)
363 ++ownedSquares;
364
365 if (ownedSquares > 25)
367 if (!localHuman)
369 return std::nullopt;
370 }
371
372 std::optional<PennybagsReaction> landedOnSquarePennybagsReaction(
374 std::uint8_t square, std::uint32_t random100) noexcept
375 {
376 if (!validPlayer(state, player) || square >= rules::SquareCount)
377 return std::nullopt;
378 const bool computer = state.players[player].aiPlayerLevel != 0;
379 const auto waitPb = [](udsound::PennybagsVoice voice) {
380 return std::optional<PennybagsReaction>{PennybagsReaction{voice,
382 };
383 if (square == 7 || square == 22 || square == 36)
385 if (square == 2 || square == 17 || square == 33)
387 if (square == 20 && (random100 % 100u) < 20u)
388 return waitPb(state.options.freeParkingPot
391 if (square == 0)
392 return computer && state.options.doubleSalaryOnGo ? std::nullopt
393 : waitPb(state.options.doubleSalaryOnGo
396 if (square == 4 && !computer)
398 if (square == 38 && !computer)
400 if (square == 10)
401 {
402 bool jailOccupied = false;
403 for (rules::PlayerNumber index = 0;
404 index < state.numberOfPlayers && index < rules::MaxPlayers; ++index)
405 jailOccupied = jailOccupied || state.players[index].currentSquare == 40;
406 if (!jailOccupied && !computer)
408 }
409 return std::nullopt;
410 }
411
412 std::optional<data::DataId> squareAnnouncementWave(
413 data::BoardEdition edition, data::LanguageId language,
414 int city, std::uint8_t square) noexcept
415 {
416 if (square >= PropertyAnnouncementIndex.size() ||
417 PropertyAnnouncementIndex[square] < 0)
418 return std::nullopt;
419
420 if (edition == data::BoardEdition::Usa)
421 {
422 const auto normalizedCity = city < 0 ? 0 : city;
423 const auto tag = static_cast<data::DataTag>(
424 0x0968u + square + static_cast<std::uint32_t>(normalizedCity) * 41u);
426 }
427
428 // Source/monopoly/Udpenny.cpp:1443-1450 substitutes the installation
429 // language only for custom city -1, then clamps the city at zero.
430 if (city == -1)
431 city = static_cast<int>(language) -
432 static_cast<int>(data::LanguageId::EnglishUk);
433 const auto normalizedCity = city < 0 ? 0 : city;
434 const auto tag = static_cast<data::DataTag>(0x2137u +
435 static_cast<std::uint32_t>(PropertyAnnouncementIndex[square]) +
436 static_cast<std::uint32_t>(normalizedCity) * 28u);
438 }
439
440 std::optional<data::DataId> cardReadWave(
441 data::BoardEdition edition, std::uint8_t cardIndex) noexcept
442 {
443 if (cardIndex >= 32 || edition != data::BoardEdition::Usa)
444 return std::nullopt;
446 static_cast<data::DataTag>(0x0811u + cardIndex));
447 }
448
449 std::optional<data::DataId> cardReadWave(
450 data::BoardEdition edition, data::LanguageId language, int city,
451 int monetarySystem, std::uint8_t cardIndex) noexcept
452 {
453 if (cardIndex >= 32) return std::nullopt;
454 if (edition == data::BoardEdition::Usa)
455 return cardReadWave(edition, cardIndex);
456 if (city < 0)
457 city = static_cast<int>(language) -
458 static_cast<int>(data::LanguageId::EnglishUk);
459 const auto tag = cardIndex < 16
460 ? europeChanceCardTag(cardIndex, city, monetarySystem)
461 : europeCommunityCardTag(cardIndex, city, monetarySystem);
462 if (tag == 0) return std::nullopt;
464 static_cast<data::DataTag>(tag));
465 }
466
467 std::optional<TokenReaction> landedOnSquareReaction(
468 const rules::GameState& state,
469 rules::PlayerNumber player,
470 std::uint8_t square,
471 std::uint32_t random100,
473 bool justReadCard) noexcept
474 {
475 if (!validPlayer(state, player) || square >= rules::SquareCount)
476 return std::nullopt;
477 const bool computer = state.players[player].aiPlayerLevel != 0;
478 const auto tokenOwner = state.squares[square].owner;
479 if (square == 20)
480 {
481 if ((random100 % 100u) < 20u)
482 return std::nullopt;
483 return wait(state.options.freeParkingPot
486 }
487 if (square == 0)
488 {
489 if (computer && state.options.doubleSalaryOnGo)
491 return std::nullopt;
492 }
493 if (square == 4)
494 {
495 if (!computer || !economics.valid)
496 return std::nullopt;
497 return wait(economics.totalWorth < 1000
500 }
501 if (square == 38)
503 : std::nullopt;
504 if (square == 10)
505 {
506 bool jailOccupied = false;
507 for (rules::PlayerNumber index = 0;
508 index < state.numberOfPlayers && index < rules::MaxPlayers; ++index)
509 jailOccupied = jailOccupied || state.players[index].currentSquare == 40;
510 if (jailOccupied)
512 return computer
514 : std::nullopt;
515 }
516
517 if (square == 2 || square == 7 || square == 17 || square == 22 ||
518 square == 30 || square == 33 || square == 36 || square == 40 ||
519 square == 41)
520 return std::nullopt;
521
523 {
524 if (!computer)
525 return std::nullopt;
526 if (square >= 31 && square <= 34)
528 if (square >= 37 && square <= 39)
530 if (isUtility(square))
531 {
532 const auto other = static_cast<std::uint8_t>(square == 12 ? 28 : 12);
533 return wait(state.squares[other].owner == player
536 }
537 if (isRailroad(square))
538 {
539 int ownedRailroads = 0;
540 for (const auto railroad : std::array<std::uint8_t, 4>{5, 15, 25, 35})
541 if (state.squares[railroad].owner == player)
543 return wait(ownedRailroads == 3
546 }
548 }
549
550 if (tokenOwner == player)
552 if (justReadCard)
553 return std::nullopt;
554 if (state.squares[square].mortgaged)
556
557 if ((random100 % 100u) < 50u &&
558 propertyFormsMonopoly(state, tokenOwner, square, false))
559 {
560 const auto houses = state.squares[square].houses;
561 if (houses == state.options.housesPerHotel)
563 if (houses == 4)
565 if (houses == 0)
568 }
569
571 }
572}
std::size_t offset
std::uint16_t group
unsigned state
unsigned index
int token
CardTag other
CardTag native
CardTag euro
std::uint16_t DataTag
Definition DataBanks.hpp:11
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39
std::optional< PennybagsReaction > goToJailPennybagsReaction(const rules::GameState &state, rules::PlayerNumber player, bool localHuman) noexcept
std::optional< PennybagsReaction > diceRollPennybagsReaction(std::uint8_t total, bool tokenAnimationsOn) noexcept
std::optional< PennybagsReaction > landedOnSquarePennybagsReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t square, std::uint32_t random100) noexcept
std::optional< PennybagsReaction > boughtPropertyPennybagsReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t property) noexcept
std::optional< data::DataId > cardReadWave(data::BoardEdition edition, std::uint8_t cardIndex) noexcept
std::optional< data::DataId > squareAnnouncementWave(data::BoardEdition edition, data::LanguageId language, int city, std::uint8_t square) noexcept
std::optional< TokenReaction > landedOnSquareReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t square, std::uint32_t random100, LandingEconomics economics, bool justReadCard) noexcept
std::optional< PennybagsReaction > offBoardPennybagsReaction(bool victory, bool localHuman) noexcept
std::optional< TokenReaction > goToJailReaction(const rules::GameState &state, rules::PlayerNumber player, bool localHuman) noexcept
bool propertyFormsMonopoly(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t property, bool assumePropertyOwned) noexcept
std::optional< PennybagsReaction > buyOrAuctionPennybagsReaction(const rules::GameState &state, rules::PlayerNumber player) noexcept
std::optional< TurnStartReactions > nextPlayerReactions(const rules::GameState &state, rules::PlayerNumber player, std::uint32_t random100, std::optional< std::uint32_t > random8) noexcept
std::optional< TokenReaction > boughtPropertyReaction(const rules::GameState &state, rules::PlayerNumber player, std::uint8_t property) noexcept
std::optional< TokenReaction > choseAuctionReaction(const rules::GameState &state, rules::PlayerNumber player) noexcept
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t MaxTokens
Definition RuleTypes.hpp:30
constexpr std::size_t SquareCount
Definition RuleTypes.hpp:24
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
constexpr PlayerNumber NobodyPlayer
Definition RuleTypes.hpp:16
bool wait(Message &message)