Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
AIProfile.cpp
Go to the documentation of this file.
1#include "AIProfile.hpp"
2
3#include <algorithm>
4#include <cmath>
5#include <fstream>
6#include <sstream>
7#include <vector>
8
10{
11 namespace
12 {
13 constexpr std::size_t MaxTrades = 20;
14 constexpr std::string_view WhatToTradePrefix = "WHAT TO TRADE";
15
16 [[nodiscard]] std::string_view trim(std::string_view text) noexcept
17 {
18 const auto first = text.find_first_not_of(" \t\r\n");
19 if (first == std::string_view::npos)
20 return {};
21 const auto last = text.find_last_not_of(" \t\r\n");
22 return text.substr(first, last - first + 1);
23 }
24
25 [[nodiscard]] std::string upper(std::string_view text)
26 {
27 std::string result(text);
28 for (auto& character : result)
29 {
30 if (character >= 'a' && character <= 'z')
31 character = static_cast<char>(character - 'a' + 'A');
32 }
33 return result;
34 }
35
36 [[nodiscard]] std::vector<double> parseValues(std::string_view text)
37 {
38 std::istringstream input{std::string(text)};
39 std::vector<double> values;
40 double value{};
41 while (input >> value)
42 values.push_back(value);
43 return values;
44 }
45
46 [[nodiscard]] std::expected<void, Error> needValues(
47 std::span<const double> values,
48 std::size_t count,
49 std::size_t line,
50 std::string_view key)
51 {
52 if (values.size() >= count)
53 return {};
54 return std::unexpected(Error{
56 line,
57 std::string(key) + ": not enough numeric values"});
58 }
59
60 [[nodiscard]] std::expected<trade::PropertyClassification, Error>
61 monopolyClassification(double raw, std::size_t line)
62 {
63 const int value = static_cast<int>(raw);
64 if (value == 0)
66 if (value == 1)
68 if (value == 2)
70 return std::unexpected(Error{
72 line,
73 "WHAT TO TRADE monopoly classification must be 0, 1, or 2"});
74 }
75
76 [[nodiscard]] std::expected<void, Error> applyAttribute(
77 Profile& profile,
78 std::string_view key,
79 std::span<const double> values,
80 std::size_t line)
81 {
82 if (key == "PLAYER ATTITUDE")
83 {
84 if (auto needed = needValues(values, rules::MaxPlayers, line, key); !needed)
85 return needed;
86 std::copy_n(values.begin(), rules::MaxPlayers, profile.playerAttitude.begin());
87 return {};
88 }
89 const auto scalar = [&](double& target) -> std::expected<void, Error> {
90 if (auto needed = needValues(values, 1, line, key); !needed)
91 return needed;
92 target = values[0];
93 return {};
94 };
95 if (key == "ATTITUDE LOST FOR REJECTING TRADE")
97 if (key == "ATTITUDE LOST PER EVALUATION POINT WHEN OBSERVING")
99 if (key == "ATTITUDE CHANGE PER TICK")
100 return scalar(profile.attitudeTickChange);
101 if (key == "ATTITUDE NEUTRAL")
102 return scalar(profile.neutralAttitude);
103 if (key == "PROBABILITY OF COUNTERING TRADE")
104 return scalar(profile.tradeCounterProbability);
105 if (key == "BUYING STAGE CASH MULTIPLIER")
106 return scalar(profile.buyingStageCashMultiplier);
107 if (key == "NO MONOPOLY STAGE CASH MULTIPLIER")
109 if (key == "CASH LIQUID ASSETS DEPENDANCE")
110 return scalar(profile.cashLiquidAssetsDependence);
111 if (key == "MONOPOLY EXISTS BUT NOT OWNER CASH MULTIPLIER")
113 if (key == "MONOPOLY WAR CASH MULTIPLIER")
115 if (key == "ODDS OF WINNING FACTOR")
116 return scalar(profile.chancesFactor);
117 if (key == "MAX WINNING FACTOR LOSS ACCEPTABLE IN TRADE")
118 return scalar(profile.chancesThreshold);
119 if (key == "CASH FACTOR")
120 return scalar(profile.cashFactor);
121 if (key == "PROPERTY STRATEGIC FACTOR")
122 return scalar(profile.tradeImportanceFactor);
123 if (key == "MINIMUM EVALUATION TRADE BENEFIT")
124 return scalar(profile.minEvaluationThreshold);
125 if (key == "MINIMUM EVALUATION WHEN GIVING A MONOPOLY FOR CASH")
126 return scalar(profile.minGiveMonopolyEvaluation);
127 if (key == "MONOPOLY RECEIVED IMPORTANCE")
129 if (key == "GIVING MONOPOLY IMPORTANCE")
131 if (key == "BALTIC RECEIVED IMPORTANCE")
132 return scalar(profile.balticReceivedImportance);
133 if (key == "PROPERTY ALLOWS TRADE IMPORTANCE")
135 if (key == "PROPERTY ALLOWS ANOTHER TRADE IMPORTANCE")
137 if (key == "PROPERTY GIVEN WITH ONE UNOWNED IMPORTANCE")
139 if (key == "PROPERTY GIVEN WITH TWO UNOWNED IMPORTANCE")
141 if (key == "NEGATIVE PROPERTY IMPORTANCE CHANGE MULTIPLIER FOR OPPONENTS")
143 if (key == "PROPOSE TRADE PROBABILITY")
144 return scalar(profile.proposeTradeProbability);
145 if (key == "MONOPOLY TRADE PROBABILITY")
146 return scalar(profile.monopolyTradeProbability);
147 if (key == "PROPOSE MONOPOLY GIVE AWAY PROBABILITY")
149 if (key == "MINIMUM EVALUATION IF FED UP")
150 return scalar(profile.minEvaluationIfFedUp);
151 if (key == "MONOPOLY SUICIDE FACTOR")
152 return scalar(profile.monopolySuicideFactor);
153 if (key == "LOWEST PROPERTY STRAT FACTOR FOR COUNTER PROPOSAL")
155 if (key == "MINIMUM NON-MONOPOLY ATTITUDE")
157
158 if (key == "TIME TO FORGET TRADE" || key == "MAX TRADES" ||
159 key == "TRADE COUNTER LIMIT" || key == "MINIMUM CASH ON HAND" ||
160 key == "BUYING HOUSE STRATEGY" ||
161 key == "MAXIMUMG HOUSES PER SQUARE FOR MONOPOLY GIVE AWAY" ||
162 key == "MINIMUM PAUSE FOR ACTION" || key == "MAXIMUM PAUSE FOR ACTION" ||
163 key == "PAUSE WHEN CONSIDERING TRADE" || key == "PATIENCE" ||
164 key == "NUMBER OF TIMES CONSIDER PROPERTY TRADE" ||
165 key == "TURNS BEFORE FORGETTING PROPERTY WAS TRADED")
166 {
167 if (auto needed = needValues(values, 1, line, key); !needed)
168 return needed;
169 const auto integer = static_cast<std::int64_t>(values[0]);
170 if (key == "TIME TO FORGET TRADE") profile.timeForgetTrade = integer;
171 else if (key == "MAX TRADES")
172 profile.maxTrades = static_cast<std::size_t>(std::clamp<std::int64_t>(integer, 0, MaxTrades));
173 else if (key == "TRADE COUNTER LIMIT") profile.tradeCounterLimit = static_cast<int>(integer);
174 else if (key == "MINIMUM CASH ON HAND") profile.minCashOnHand = integer;
175 else if (key == "BUYING HOUSE STRATEGY") profile.housingPurchaseStrategy =
176 static_cast<std::uint8_t>(std::clamp<std::int64_t>(integer, 0, 3));
177 else if (key == "MAXIMUMG HOUSES PER SQUARE FOR MONOPOLY GIVE AWAY")
178 profile.maxHousesPerSquareForGiveAway = static_cast<int>(integer);
179 else if (key == "MINIMUM PAUSE FOR ACTION") profile.minActionWait = static_cast<int>(integer);
180 else if (key == "MAXIMUM PAUSE FOR ACTION") profile.maxActionWait = static_cast<int>(integer);
181 else if (key == "PAUSE WHEN CONSIDERING TRADE") profile.waitForTradeConsider = static_cast<int>(integer);
182 else if (key == "PATIENCE") profile.patience = static_cast<std::uint64_t>(std::max<std::int64_t>(0, integer));
183 else if (key == "NUMBER OF TIMES CONSIDER PROPERTY TRADE")
184 profile.numberTimesAllowPropertyTrade = static_cast<int>(std::clamp<std::int64_t>(integer, 0, 3));
185 else profile.turnsToForgetPropertyTrade = static_cast<int>(integer);
186 return {};
187 }
188
189 if (key == "CASH STRATEGY")
190 {
191 if (auto needed = needValues(values, 1, line, key); !needed)
192 return needed;
193 const int strategy = static_cast<int>(values[0]);
195 return std::unexpected(Error{ErrorCode::InvalidEnum, line, "CASH STRATEGY must be 0..4"});
196 profile.cashStrategy = static_cast<decision::CashStrategy>(strategy);
197 return {};
198 }
199
200 if (key == "PROPERTY FACTORS")
201 {
202 if (auto needed = needValues(values, profile.worthFactors.property.size(), line, key); !needed)
203 return needed;
204 std::copy_n(values.begin(), profile.worthFactors.property.size(), profile.worthFactors.property.begin());
205 return {};
206 }
207 if (key == "CASH COW FACTORS")
208 {
209 if (auto needed = needValues(values, profile.worthFactors.cashCow.size(), line, key); !needed)
210 return needed;
211 std::copy_n(values.begin(), profile.worthFactors.cashCow.size(), profile.worthFactors.cashCow.begin());
212 return {};
213 }
214 if (key == "RAILROAD IMPORTANCE FACTORS")
215 {
217 if (auto needed = needValues(values, target.size(), line, key); !needed)
218 return needed;
219 std::copy_n(values.begin(), target.size(), target.begin());
220 return {};
221 }
222 if (key == "UTILITY IMPORTANCE FACTORS")
223 {
225 if (auto needed = needValues(values, target.size(), line, key); !needed)
226 return needed;
227 std::copy_n(values.begin(), target.size(), target.begin());
228 return {};
229 }
230 if (key == "MONOPOLY VETO IMPORTANCE VALUES")
231 {
233 if (auto needed = needValues(values, target.size(), line, key); !needed)
234 return needed;
235 std::copy_n(values.begin(), target.size(), target.begin());
236 return {};
237 }
238 if (key.starts_with(WhatToTradePrefix))
239 {
240 if (auto needed = needValues(values, 5, line, key); !needed)
241 return needed;
242 const auto suffix = trim(key.substr(WhatToTradePrefix.size()));
243 const auto attitudeValues = parseValues(suffix);
244 if (attitudeValues.size() != 1)
245 return std::unexpected(Error{ErrorCode::MalformedValue, line, "invalid WHAT TO TRADE attitude"});
246 const int index = static_cast<int>(std::lround((attitudeValues[0] + 0.9) * 10.0));
247 if (index < 0 || index >= static_cast<int>(trade::WhatToTradeEntries))
248 return std::unexpected(Error{ErrorCode::MalformedValue, line, "WHAT TO TRADE attitude out of range"});
249 auto classification = monopolyClassification(values[0], line);
250 if (!classification)
251 return std::unexpected(classification.error());
252 auto& entry = profile.whatToTrade[static_cast<std::size_t>(index)];
253 entry.giveMonopoly = *classification;
254 entry.giveGroupTrades = static_cast<std::uint8_t>(std::clamp(values[1], 0.0, 6.0));
255 entry.giveCashCows = static_cast<std::uint8_t>(std::clamp(values[2], 0.0, 6.0));
256 entry.giveJunk = static_cast<std::uint8_t>(std::clamp(values[3], 0.0, 6.0));
257 entry.cashMultiplier = values[4];
258 return {};
259 }
260
261 return std::unexpected(Error{ErrorCode::UnknownAttribute, line, std::string(key)});
262 }
263
264 [[nodiscard]] int sectionForLine(std::string_view upperLine) noexcept
265 {
266 if (upperLine.find("GLOBAL SETTINGS") != std::string_view::npos)
267 return -1;
268 if (upperLine.find("STRATEGY FOR LEVEL 1") != std::string_view::npos)
269 return 1;
270 if (upperLine.find("STRATEGY FOR LEVEL 2") != std::string_view::npos)
271 return 2;
272 if (upperLine.find("STRATEGY FOR LEVEL 3") != std::string_view::npos)
273 return 3;
274 return 0;
275 }
276 }
277
278 std::expected<Profile, Error> parse(std::string_view text, int level)
279 {
281 return std::unexpected(Error{ErrorCode::InvalidLevel, 0, "AI level must be 1..3"});
282
283 Profile profile{};
284 int currentSection{};
285 std::size_t lineNumber{};
286 std::size_t offset{};
287 while (offset <= text.size())
288 {
289 ++lineNumber;
290 const auto end = text.find('\n', offset);
291 const auto raw = text.substr(offset,
292 end == std::string_view::npos ? text.size() - offset : end - offset);
293 offset = end == std::string_view::npos ? text.size() + 1 : end + 1;
294
295 const auto stripped = trim(raw);
296 if (stripped.empty() || stripped.front() == '/')
297 continue;
298 const auto upperLine = upper(stripped);
299 if (const int section = sectionForLine(upperLine); section != 0)
300 {
302 continue;
303 }
304
305 const auto equals = upperLine.find('=');
306 if (equals == std::string::npos)
307 continue;
308 if (currentSection != -1 && currentSection != level)
309 continue;
310
311 const auto key = trim(std::string_view(upperLine).substr(0, equals));
312 const auto valueText = trim(std::string_view(upperLine).substr(equals + 1));
313 const auto values = parseValues(valueText);
314 auto applied = applyAttribute(profile, key, values, lineNumber);
315 if (!applied)
316 return std::unexpected(applied.error());
317 }
318 return profile;
319 }
320
321 std::expected<Profile, Error> load(const std::filesystem::path& path, int level)
322 {
323 std::ifstream input(path, std::ios::binary);
324 if (!input)
325 return std::unexpected(Error{ErrorCode::IoError, 0, "cannot open AI profile"});
326 std::ostringstream content;
327 content << input.rdbuf();
328 if (!input.good() && !input.eof())
329 return std::unexpected(Error{ErrorCode::IoError, 0, "cannot read AI profile"});
330 return parse(content.str(), level);
331 }
332
333 std::string_view tokenFileName(std::uint8_t token) noexcept
334 {
335 static constexpr std::array<std::string_view, TokenProfileCount> Names{
336 "Cannon.ai", "RaceCar.ai", "Dog.ai", "TopHat.ai", "Iron.ai",
337 "Horse.ai", "Ship.ai", "Boot.ai", "Thimble.ai", "Barrow.AI", "Moneybag.ai"};
338 return token < Names.size() ? Names[token] : std::string_view{};
339 }
340
342 const ProfileSet& profiles,
344 const ConfigContext& context) noexcept
345 {
348 return config;
349 const auto& strategy = profiles[strategyPlayer];
350 for (rules::PlayerNumber player = 0; player < rules::MaxPlayers; ++player)
351 {
352 config.winningChance.cashStrategy[player] = profiles[player].cashStrategy;
353 config.winningChance.minCashOnHand[player] = profiles[player].minCashOnHand;
354 config.winningChance.moneyOwed[player] = context.moneyOwed[player];
355 }
356 config.winningChance.buyingStageCashMultiplier = strategy.buyingStageCashMultiplier;
357 config.winningChance.noMonopolyStageCashMultiplier = strategy.noMonopolyStageCashMultiplier;
358 config.winningChance.cashLiquidAssetsDependence = strategy.cashLiquidAssetsDependence;
359 config.winningChance.monopolyNotOwnedStageCashMultiplier =
360 strategy.monopolyNotOwnedStageCashMultiplier;
361 config.winningChance.monopolyOwnedStageCashMultiplier =
362 strategy.monopolyOwnedStageCashMultiplier;
363 config.worthFactors = strategy.worthFactors;
364 config.propertyImportance = strategy.propertyImportance;
365 config.chancesThreshold = strategy.chancesThreshold;
366 config.chancesFactor = strategy.chancesFactor;
367 config.cashFactor = strategy.cashFactor;
368 config.tradeImportanceFactor = strategy.tradeImportanceFactor;
369 config.purchasingPlayer = context.purchasingPlayer;
370 config.purchasingProperty = context.purchasingProperty;
371 return config;
372 }
373
375 const ProfileSet& profiles,
377 const ConfigContext& context) noexcept
378 {
381 return config;
382 const auto& strategy = profiles[strategyPlayer];
384 config.playerAttitude = strategy.playerAttitude;
385 config.tradeCounterLimit = strategy.tradeCounterLimit;
386 config.numberTimesAllowPropertyTrade = strategy.numberTimesAllowPropertyTrade;
387 config.tradeCounterProbability = strategy.tradeCounterProbability;
388 return config;
389 }
390
392 const ProfileSet& profiles,
394 const ConfigContext& context) noexcept
395 {
398 return config;
399 const auto& strategy = profiles[strategyPlayer];
401 config.fairTrade.playerAttitude = strategy.playerAttitude;
402 config.fairTrade.localAIPlayer = context.localAIPlayer;
403 for (std::size_t index = 0; index < trade::WhatToTradeEntries; ++index)
404 config.fairTrade.cashMultipliers[index] = strategy.whatToTrade[index].cashMultiplier;
405 config.fairTrade.minEvaluationThreshold = strategy.minEvaluationThreshold;
406 config.fairTrade.minGiveMonopolyEvaluation = strategy.minGiveMonopolyEvaluation;
407 config.whatToTrade = strategy.whatToTrade;
408 config.minEvaluationThreshold = strategy.minEvaluationThreshold;
409 config.lowestPropertyImportanceForCounter = strategy.lowestPropertyImportanceForCounter;
410 return config;
411 }
412
414 const ProfileSet& profiles,
416 const ConfigContext& context) noexcept
417 {
420 return config;
421 const auto& strategy = profiles[strategyPlayer];
423 profiles, strategyPlayer, context);
424 config.fairTrade.playerAttitude = strategy.playerAttitude;
425 config.fairTrade.localAIPlayer = context.localAIPlayer;
426 for (std::size_t index = 0; index < trade::WhatToTradeEntries; ++index)
427 config.fairTrade.cashMultipliers[index] =
428 strategy.whatToTrade[index].cashMultiplier;
429 config.fairTrade.minEvaluationThreshold = strategy.minEvaluationThreshold;
430 config.fairTrade.minGiveMonopolyEvaluation = strategy.minGiveMonopolyEvaluation;
431 config.whatToTrade = strategy.whatToTrade;
432 return config;
433 }
434}
std::size_t count
std::size_t offset
std::string_view text
std::uint64_t key
unsigned index
data::DataTag first
int token
std::string path
Bytes input
std::uint16_t value
Definition Timers.cpp:16
const char * suffix
std::expected< Profile, Error > load(const std::filesystem::path &path, int level)
std::array< Profile, rules::MaxPlayers > ProfileSet
Definition AIProfile.hpp:78
decision::CounterProposalBalanceConfig makeCounterBalanceConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
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
std::string_view tokenFileName(std::uint8_t token) noexcept
std::expected< Profile, Error > parse(std::string_view text, int level)
decision::CounterProposalPreflightConfig makeCounterPreflightConfig(const ProfileSet &profiles, rules::PlayerNumber strategyPlayer, const ConfigContext &context) noexcept
constexpr std::size_t WhatToTradeEntries
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
std::array< CashStrategy, rules::MaxPlayers > cashStrategy
decision::CashStrategy cashStrategy
Definition AIProfile.hpp:42
decision::WorthFactors worthFactors
Definition AIProfile.hpp:53
std::array< double, rules::MaxPlayers > playerAttitude
Definition AIProfile.hpp:32
trade::WhatToTradeTable whatToTrade
Definition AIProfile.hpp:74
trade::PropertyImportanceConfig propertyImportance
Definition AIProfile.hpp:57