@gamepark/greylune 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/Constants.d.ts +23 -0
  2. package/dist/Constants.d.ts.map +1 -0
  3. package/dist/Constants.js +17 -0
  4. package/dist/Constants.js.map +1 -0
  5. package/dist/GreyluneOptions.d.ts +11 -0
  6. package/dist/GreyluneOptions.d.ts.map +1 -0
  7. package/dist/GreyluneOptions.js +8 -0
  8. package/dist/GreyluneOptions.js.map +1 -0
  9. package/dist/GreyluneRules.d.ts +123 -0
  10. package/dist/GreyluneRules.d.ts.map +1 -0
  11. package/dist/GreyluneRules.js +122 -0
  12. package/dist/GreyluneRules.js.map +1 -0
  13. package/dist/GreyluneSetup.d.ts +18 -0
  14. package/dist/GreyluneSetup.d.ts.map +1 -0
  15. package/dist/GreyluneSetup.js +88 -0
  16. package/dist/GreyluneSetup.js.map +1 -0
  17. package/dist/Memory.d.ts +22 -0
  18. package/dist/Memory.d.ts.map +1 -0
  19. package/dist/Memory.js +23 -0
  20. package/dist/Memory.js.map +1 -0
  21. package/dist/PlayerColor.d.ts +7 -0
  22. package/dist/PlayerColor.d.ts.map +1 -0
  23. package/dist/PlayerColor.js +8 -0
  24. package/dist/PlayerColor.js.map +1 -0
  25. package/dist/Season.d.ts +7 -0
  26. package/dist/Season.d.ts.map +1 -0
  27. package/dist/Season.js +8 -0
  28. package/dist/Season.js.map +1 -0
  29. package/dist/Year.d.ts +8 -0
  30. package/dist/Year.d.ts.map +1 -0
  31. package/dist/Year.js +10 -0
  32. package/dist/Year.js.map +1 -0
  33. package/dist/ai/Evaluation.d.ts +118 -0
  34. package/dist/ai/Evaluation.d.ts.map +1 -0
  35. package/dist/ai/Evaluation.js +1069 -0
  36. package/dist/ai/Evaluation.js.map +1 -0
  37. package/dist/ai/GreyluneAI.d.ts +20 -0
  38. package/dist/ai/GreyluneAI.d.ts.map +1 -0
  39. package/dist/ai/GreyluneAI.js +113 -0
  40. package/dist/ai/GreyluneAI.js.map +1 -0
  41. package/dist/ai/Model.d.ts +38 -0
  42. package/dist/ai/Model.d.ts.map +1 -0
  43. package/dist/ai/Model.js +105 -0
  44. package/dist/ai/Model.js.map +1 -0
  45. package/dist/ai/Simulation.d.ts +13 -0
  46. package/dist/ai/Simulation.d.ts.map +1 -0
  47. package/dist/ai/Simulation.js +20 -0
  48. package/dist/ai/Simulation.js.map +1 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +5 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/material/Area.d.ts +9 -0
  54. package/dist/material/Area.d.ts.map +1 -0
  55. package/dist/material/Area.js +10 -0
  56. package/dist/material/Area.js.map +1 -0
  57. package/dist/material/Distance.d.ts +8 -0
  58. package/dist/material/Distance.d.ts.map +1 -0
  59. package/dist/material/Distance.js +9 -0
  60. package/dist/material/Distance.js.map +1 -0
  61. package/dist/material/Effect.d.ts +103 -0
  62. package/dist/material/Effect.d.ts.map +1 -0
  63. package/dist/material/Effect.js +83 -0
  64. package/dist/material/Effect.js.map +1 -0
  65. package/dist/material/EncounterCard.d.ts +61 -0
  66. package/dist/material/EncounterCard.d.ts.map +1 -0
  67. package/dist/material/EncounterCard.js +214 -0
  68. package/dist/material/EncounterCard.js.map +1 -0
  69. package/dist/material/EventTile.d.ts +17 -0
  70. package/dist/material/EventTile.d.ts.map +1 -0
  71. package/dist/material/EventTile.js +52 -0
  72. package/dist/material/EventTile.js.map +1 -0
  73. package/dist/material/LocationType.d.ts +42 -0
  74. package/dist/material/LocationType.d.ts.map +1 -0
  75. package/dist/material/LocationType.js +43 -0
  76. package/dist/material/LocationType.js.map +1 -0
  77. package/dist/material/MaterialSource.d.ts +8 -0
  78. package/dist/material/MaterialSource.d.ts.map +1 -0
  79. package/dist/material/MaterialSource.js +2 -0
  80. package/dist/material/MaterialSource.js.map +1 -0
  81. package/dist/material/MaterialType.d.ts +23 -0
  82. package/dist/material/MaterialType.d.ts.map +1 -0
  83. package/dist/material/MaterialType.js +24 -0
  84. package/dist/material/MaterialType.js.map +1 -0
  85. package/dist/material/Period.d.ts +6 -0
  86. package/dist/material/Period.d.ts.map +1 -0
  87. package/dist/material/Period.js +7 -0
  88. package/dist/material/Period.js.map +1 -0
  89. package/dist/material/PlayerState.d.ts +34 -0
  90. package/dist/material/PlayerState.d.ts.map +1 -0
  91. package/dist/material/PlayerState.js +106 -0
  92. package/dist/material/PlayerState.js.map +1 -0
  93. package/dist/material/QuestTile.d.ts +21 -0
  94. package/dist/material/QuestTile.d.ts.map +1 -0
  95. package/dist/material/QuestTile.js +32 -0
  96. package/dist/material/QuestTile.js.map +1 -0
  97. package/dist/material/Reaction.d.ts +49 -0
  98. package/dist/material/Reaction.d.ts.map +1 -0
  99. package/dist/material/Reaction.js +37 -0
  100. package/dist/material/Reaction.js.map +1 -0
  101. package/dist/material/Tokens.d.ts +29 -0
  102. package/dist/material/Tokens.d.ts.map +1 -0
  103. package/dist/material/Tokens.js +46 -0
  104. package/dist/material/Tokens.js.map +1 -0
  105. package/dist/material/Village.d.ts +23 -0
  106. package/dist/material/Village.d.ts.map +1 -0
  107. package/dist/material/Village.js +29 -0
  108. package/dist/material/Village.js.map +1 -0
  109. package/dist/material/VillageCard.d.ts +90 -0
  110. package/dist/material/VillageCard.d.ts.map +1 -0
  111. package/dist/material/VillageCard.js +359 -0
  112. package/dist/material/VillageCard.js.map +1 -0
  113. package/dist/material/Villager.d.ts +8 -0
  114. package/dist/material/Villager.d.ts.map +1 -0
  115. package/dist/material/Villager.js +7 -0
  116. package/dist/material/Villager.js.map +1 -0
  117. package/dist/material/VpToken.d.ts +10 -0
  118. package/dist/material/VpToken.d.ts.map +1 -0
  119. package/dist/material/VpToken.js +9 -0
  120. package/dist/material/VpToken.js.map +1 -0
  121. package/dist/rules/ActivateCardRule.d.ts +43 -0
  122. package/dist/rules/ActivateCardRule.d.ts.map +1 -0
  123. package/dist/rules/ActivateCardRule.js +183 -0
  124. package/dist/rules/ActivateCardRule.js.map +1 -0
  125. package/dist/rules/AutumnRule.d.ts +9 -0
  126. package/dist/rules/AutumnRule.d.ts.map +1 -0
  127. package/dist/rules/AutumnRule.js +42 -0
  128. package/dist/rules/AutumnRule.js.map +1 -0
  129. package/dist/rules/BonusTokenRule.d.ts +10 -0
  130. package/dist/rules/BonusTokenRule.d.ts.map +1 -0
  131. package/dist/rules/BonusTokenRule.js +20 -0
  132. package/dist/rules/BonusTokenRule.js.map +1 -0
  133. package/dist/rules/ChooseOutcomeRule.d.ts +9 -0
  134. package/dist/rules/ChooseOutcomeRule.d.ts.map +1 -0
  135. package/dist/rules/ChooseOutcomeRule.js +18 -0
  136. package/dist/rules/ChooseOutcomeRule.js.map +1 -0
  137. package/dist/rules/ChooseSkillRule.d.ts +10 -0
  138. package/dist/rules/ChooseSkillRule.d.ts.map +1 -0
  139. package/dist/rules/ChooseSkillRule.js +20 -0
  140. package/dist/rules/ChooseSkillRule.js.map +1 -0
  141. package/dist/rules/CustomMoveType.d.ts +17 -0
  142. package/dist/rules/CustomMoveType.d.ts.map +1 -0
  143. package/dist/rules/CustomMoveType.js +18 -0
  144. package/dist/rules/CustomMoveType.js.map +1 -0
  145. package/dist/rules/DiscardItemRule.d.ts +9 -0
  146. package/dist/rules/DiscardItemRule.d.ts.map +1 -0
  147. package/dist/rules/DiscardItemRule.js +19 -0
  148. package/dist/rules/DiscardItemRule.js.map +1 -0
  149. package/dist/rules/EncounterRule.d.ts +26 -0
  150. package/dist/rules/EncounterRule.d.ts.map +1 -0
  151. package/dist/rules/EncounterRule.js +75 -0
  152. package/dist/rules/EncounterRule.js.map +1 -0
  153. package/dist/rules/EventRule.d.ts +13 -0
  154. package/dist/rules/EventRule.d.ts.map +1 -0
  155. package/dist/rules/EventRule.js +39 -0
  156. package/dist/rules/EventRule.js.map +1 -0
  157. package/dist/rules/FinalScoringRule.d.ts +18 -0
  158. package/dist/rules/FinalScoringRule.d.ts.map +1 -0
  159. package/dist/rules/FinalScoringRule.js +30 -0
  160. package/dist/rules/FinalScoringRule.js.map +1 -0
  161. package/dist/rules/GreyluneRule.d.ts +87 -0
  162. package/dist/rules/GreyluneRule.d.ts.map +1 -0
  163. package/dist/rules/GreyluneRule.js +432 -0
  164. package/dist/rules/GreyluneRule.js.map +1 -0
  165. package/dist/rules/PlaceVillagerRule.d.ts +10 -0
  166. package/dist/rules/PlaceVillagerRule.d.ts.map +1 -0
  167. package/dist/rules/PlaceVillagerRule.js +17 -0
  168. package/dist/rules/PlaceVillagerRule.js.map +1 -0
  169. package/dist/rules/ReactionRule.d.ts +17 -0
  170. package/dist/rules/ReactionRule.d.ts.map +1 -0
  171. package/dist/rules/ReactionRule.js +48 -0
  172. package/dist/rules/ReactionRule.js.map +1 -0
  173. package/dist/rules/ResolveEffectsRule.d.ts +12 -0
  174. package/dist/rules/ResolveEffectsRule.d.ts.map +1 -0
  175. package/dist/rules/ResolveEffectsRule.js +85 -0
  176. package/dist/rules/ResolveEffectsRule.js.map +1 -0
  177. package/dist/rules/ResolveEncounterRule.d.ts +19 -0
  178. package/dist/rules/ResolveEncounterRule.d.ts.map +1 -0
  179. package/dist/rules/ResolveEncounterRule.js +86 -0
  180. package/dist/rules/ResolveEncounterRule.js.map +1 -0
  181. package/dist/rules/ResolveQuestRule.d.ts +17 -0
  182. package/dist/rules/ResolveQuestRule.d.ts.map +1 -0
  183. package/dist/rules/ResolveQuestRule.js +64 -0
  184. package/dist/rules/ResolveQuestRule.js.map +1 -0
  185. package/dist/rules/RuleId.d.ts +27 -0
  186. package/dist/rules/RuleId.d.ts.map +1 -0
  187. package/dist/rules/RuleId.js +28 -0
  188. package/dist/rules/RuleId.js.map +1 -0
  189. package/dist/rules/SeasonRule.d.ts +11 -0
  190. package/dist/rules/SeasonRule.d.ts.map +1 -0
  191. package/dist/rules/SeasonRule.js +32 -0
  192. package/dist/rules/SeasonRule.js.map +1 -0
  193. package/dist/rules/SpecialActionRule.d.ts +15 -0
  194. package/dist/rules/SpecialActionRule.d.ts.map +1 -0
  195. package/dist/rules/SpecialActionRule.js +32 -0
  196. package/dist/rules/SpecialActionRule.js.map +1 -0
  197. package/dist/rules/SpringRule.d.ts +7 -0
  198. package/dist/rules/SpringRule.d.ts.map +1 -0
  199. package/dist/rules/SpringRule.js +16 -0
  200. package/dist/rules/SpringRule.js.map +1 -0
  201. package/dist/rules/StraightenCardRule.d.ts +9 -0
  202. package/dist/rules/StraightenCardRule.d.ts.map +1 -0
  203. package/dist/rules/StraightenCardRule.js +11 -0
  204. package/dist/rules/StraightenCardRule.js.map +1 -0
  205. package/dist/rules/SummerRule.d.ts +30 -0
  206. package/dist/rules/SummerRule.d.ts.map +1 -0
  207. package/dist/rules/SummerRule.js +140 -0
  208. package/dist/rules/SummerRule.js.map +1 -0
  209. package/dist/rules/TellStoryRule.d.ts +22 -0
  210. package/dist/rules/TellStoryRule.d.ts.map +1 -0
  211. package/dist/rules/TellStoryRule.js +72 -0
  212. package/dist/rules/TellStoryRule.js.map +1 -0
  213. package/dist/rules/TheFirstStepRule.d.ts +5 -0
  214. package/dist/rules/TheFirstStepRule.d.ts.map +1 -0
  215. package/dist/rules/TheFirstStepRule.js +7 -0
  216. package/dist/rules/TheFirstStepRule.js.map +1 -0
  217. package/dist/rules/TravelRule.d.ts +13 -0
  218. package/dist/rules/TravelRule.d.ts.map +1 -0
  219. package/dist/rules/TravelRule.js +41 -0
  220. package/dist/rules/TravelRule.js.map +1 -0
  221. package/dist/rules/UseItemRule.d.ts +6 -0
  222. package/dist/rules/UseItemRule.d.ts.map +1 -0
  223. package/dist/rules/UseItemRule.js +35 -0
  224. package/dist/rules/UseItemRule.js.map +1 -0
  225. package/dist/rules/WinterRule.d.ts +24 -0
  226. package/dist/rules/WinterRule.d.ts.map +1 -0
  227. package/dist/rules/WinterRule.js +112 -0
  228. package/dist/rules/WinterRule.js.map +1 -0
  229. package/package.json +39 -0
@@ -0,0 +1,1069 @@
1
+ import { MAX_COMPANIONS, MAX_SKILL, MAX_STORY_VALUE } from '../Constants';
2
+ import { GreyluneRules } from '../GreyluneRules';
3
+ import { Memory } from '../Memory';
4
+ import { Area } from '../material/Area';
5
+ import { Countable, GainType, gathered, magic, RequirementType, SEAL, usesSeal, vp } from '../material/Effect';
6
+ import { encounterArea, encounterCardData } from '../material/EncounterCard';
7
+ import { eventTileData, isFestival } from '../material/EventTile';
8
+ import { LocationType } from '../material/LocationType';
9
+ import { MaterialType } from '../material/MaterialType';
10
+ import { activeVillagers, adventurerArea, playerSeason, villagersInVillage } from '../material/PlayerState';
11
+ import { heroicQuestAreas, questRequirements, questRewards, QuestTile } from '../material/QuestTile';
12
+ import { incomeTokenGains } from '../material/Tokens';
13
+ import { cardsAroundGap, gapOf, slotGaps, villageGaps, villagersAroundSlot } from '../material/Village';
14
+ import { getVillagerPlayer } from '../material/Villager';
15
+ import { getVillageCardType, VillageCard, VillageCardType, villageCardData } from '../material/VillageCard';
16
+ import { RuleId } from '../rules/RuleId';
17
+ import { specialActions } from '../rules/SpecialActionRule';
18
+ import { Season } from '../Season';
19
+ import { currentYear, YEARS } from '../Year';
20
+ import { buildModel, clampSkill, clampVillagers, cloneModel, modelCardsScore, modelCount, modelItemLimit, modelScoreValue, modelSkillScore, owns, storyPoints } from './Model';
21
+ const COIN_RATE = 0.75;
22
+ const COIN_STEPS = [
23
+ [8, 1],
24
+ [14, 0.8],
25
+ [20, 0.5],
26
+ [Infinity, 0.25]
27
+ ];
28
+ const LAST_YEAR_COIN_STEPS = [
29
+ [14, 1],
30
+ [22, 0.7],
31
+ [Infinity, 0.3]
32
+ ];
33
+ const LEVEL_VALUES = [1.7, 1.6, 1.5, 1.3, 1.1];
34
+ const VILLAGER_YEAR_VALUES = [3, 3, 3, 2.6, 2.3, 2, 1.7];
35
+ const TRAVEL_VALUES = [0, 1.8, 2.6, 3.2, 3.6, 4, 4.2, 4.4, 4.5];
36
+ const STORY_POINT_VALUE = 1.2;
37
+ const SPRING_PLACEMENT = 0.9;
38
+ export const evaluationSettings = { springFloor: 1.5, lastSpringFloor: 0.6 };
39
+ const LAST_YEAR_REACH = { 2: 0.4, 3: 0.3, 4: 0.25 };
40
+ const POTION_VALUES = {
41
+ [VillageCard.StrengthPotion]: 1.8,
42
+ [VillageCard.ManaPotion]: 1.8,
43
+ [VillageCard.FlyingPotion]: 1.6,
44
+ [VillageCard.InvisibilityPotion]: 2.2,
45
+ [VillageCard.CharismaPotion]: 1.5,
46
+ [VillageCard.EndurancePotion]: 1.5
47
+ };
48
+ const TRAVEL_DUPLICATES = [1, 0.7, 0.5, 0.35, 0.25, 0.2];
49
+ const discardsCard = (requirements = []) => requirements.some((requirement) => requirement.type === RequirementType.DiscardCard);
50
+ const signature = (model) => `${model.force}:${model.magic}:${model.coins}:${model.items.length}:${model.untold.length}:${model.markers}`;
51
+ export class Assessment {
52
+ game;
53
+ player;
54
+ rules;
55
+ model;
56
+ players;
57
+ year;
58
+ yearsAfter;
59
+ season;
60
+ final;
61
+ active;
62
+ area;
63
+ pendingPlacements;
64
+ quests;
65
+ lastYearCards;
66
+ base;
67
+ travelCache = new Map();
68
+ areaCache = new Map();
69
+ activationCache = new Map();
70
+ storyCache = new Map();
71
+ tellingChancesCache;
72
+ constructor(game, player) {
73
+ this.game = game;
74
+ this.player = player;
75
+ this.rules = new GreyluneRules(game);
76
+ this.model = buildModel(this.rules, player);
77
+ this.players = game.players.length;
78
+ this.year = currentYear(this.rules, this.players);
79
+ this.yearsAfter = YEARS - this.year;
80
+ this.season = playerSeason(this.rules, player);
81
+ this.final = this.season === Season.Autumn && this.yearsAfter === 0;
82
+ this.active = activeVillagers(this.rules, player).length;
83
+ this.area = adventurerArea(this.rules, player);
84
+ this.pendingPlacements =
85
+ game.players
86
+ .filter((other) => other !== player && playerSeason(this.rules, other) === Season.Spring)
87
+ .reduce((total, other) => total + activeVillagers(this.rules, other).length, 0) * 0.85;
88
+ this.quests = heroicQuestAreas.flatMap((area) => {
89
+ const tile = this.rules.material(MaterialType.QuestTile).location(LocationType.QuestTileSpace).locationId(area).getItem()?.id;
90
+ if (tile === undefined)
91
+ return [];
92
+ const firstTaken = this.rules
93
+ .material(MaterialType.QuestMarker)
94
+ .location(LocationType.QuestRewardSpace)
95
+ .locationId(area)
96
+ .filter((item) => item.location.x === 0).length > 0;
97
+ return [{ area, tile, firstTaken }];
98
+ });
99
+ this.lastYearCards = new Set(this.yearsAfter > 0
100
+ ? [VillageCard.Fortress, VillageCard.Library, VillageCard.ScarletDragon, VillageCard.Mandolin, VillageCard.BagOfHolding]
101
+ : this.season === Season.Autumn
102
+ ? []
103
+ : this.grid.getItems().map((item) => item.id.front));
104
+ this.base = this.staticValue(this.model);
105
+ }
106
+ get grid() {
107
+ return this.rules.material(MaterialType.VillageCard).location(LocationType.VillageGrid);
108
+ }
109
+ get memory() {
110
+ return this.game.memory;
111
+ }
112
+ evaluate() {
113
+ if (this.final)
114
+ return this.base;
115
+ return this.base + this.thisYearPotential() + this.pendingValue();
116
+ }
117
+ breakdown() {
118
+ const model = this.model;
119
+ const round = (value) => Math.round(value * 10) / 10;
120
+ const terms = {
121
+ score: model.vp + model.questPoints + modelCardsScore(model, !this.final) + modelSkillScore(model)
122
+ };
123
+ if (!this.final) {
124
+ Object.assign(terms, {
125
+ coins: this.coinWorth(model, model.coins),
126
+ skills: this.skillValue(model.force) + this.skillValue(model.magic),
127
+ villagers: this.villagersValue(model),
128
+ income: this.incomeValue(model),
129
+ cards: this.cardsValue(model),
130
+ stories: this.storiesValue(model),
131
+ quests: this.questsValue(model),
132
+ lastYear: this.lastYearValue(model),
133
+ thisYear: this.thisYearPotential(),
134
+ pending: this.pendingValue()
135
+ });
136
+ }
137
+ for (const key of Object.keys(terms))
138
+ terms[key] = round(terms[key]);
139
+ return terms;
140
+ }
141
+ staticValue(model) {
142
+ if (this.final)
143
+ return model.vp + model.questPoints + modelCardsScore(model) + modelSkillScore(model) + model.coins * 0.001;
144
+ return (model.vp +
145
+ model.questPoints +
146
+ modelCardsScore(model, true) +
147
+ modelSkillScore(model) +
148
+ this.coinWorth(model, model.coins) +
149
+ this.skillValue(model.force) +
150
+ this.skillValue(model.magic) +
151
+ this.villagersValue(model) +
152
+ this.incomeValue(model) +
153
+ this.cardsValue(model) +
154
+ this.storiesValue(model) +
155
+ this.questsValue(model) +
156
+ this.lastYearValue(model));
157
+ }
158
+ coinsValue(coins, floor = 0) {
159
+ if (this.yearsAfter === 0)
160
+ return coins * Math.max(0.05, floor);
161
+ const lastYear = this.yearsAfter === 1 && this.season === Season.Autumn;
162
+ const steps = lastYear ? LAST_YEAR_COIN_STEPS : COIN_STEPS;
163
+ let value = 0;
164
+ let counted = 0;
165
+ for (const [upTo, weight] of steps) {
166
+ const slice = Math.min(coins, upTo) - counted;
167
+ if (slice <= 0)
168
+ break;
169
+ value += slice * Math.max(floor, weight * COIN_RATE);
170
+ counted += slice;
171
+ }
172
+ return value;
173
+ }
174
+ coinWorth(model, coins) {
175
+ return this.coinsValue(coins, owns(model, VillageCard.Neris) ? 0.5 : 0);
176
+ }
177
+ get skillFactor() {
178
+ return this.yearsAfter >= 2 ? 1 : this.yearsAfter === 1 ? 0.85 : 0.5;
179
+ }
180
+ skillValue(level) {
181
+ let value = 0;
182
+ for (let step = 0; step < Math.min(level, MAX_SKILL); step++)
183
+ value += LEVEL_VALUES[step];
184
+ return value * this.skillFactor;
185
+ }
186
+ villagersValue(model) {
187
+ let value = 0;
188
+ for (let villager = 0; villager < model.villagers; villager++)
189
+ value += VILLAGER_YEAR_VALUES[villager];
190
+ return value * this.yearsAfter;
191
+ }
192
+ incomeValue(model) {
193
+ const tokens = model.incomeTokens.flatMap((token) => incomeTokenGains[token]);
194
+ const total = (type) => tokens.reduce((sum, gain) => (gain.type === type && 'count' in gain ? sum + gain.count : sum), 0);
195
+ const coins = Math.max(0, 3 - model.companions.length) + total(GainType.Coins);
196
+ const thisAutumn = this.season !== Season.Autumn ? 1 : 0;
197
+ const coinAutumns = (this.yearsAfter > 0 ? thisAutumn : 0) + Math.max(0, this.yearsAfter - 1);
198
+ const vpAutumns = thisAutumn + this.yearsAfter;
199
+ return coins * coinAutumns * COIN_RATE * 0.85 + total(GainType.Vp) * vpAutumns + total(GainType.Force) * Math.min(vpAutumns, MAX_SKILL - model.force) * 1.3;
200
+ }
201
+ cardsValue(model) {
202
+ const years = this.yearsAfter;
203
+ const selia = owns(model, VillageCard.Selia);
204
+ const elwen = owns(model, VillageCard.Elwen);
205
+ let value = 0;
206
+ const journeys = [];
207
+ for (const card of model.items) {
208
+ const data = villageCardData[card.front];
209
+ if (data.potion) {
210
+ value += (POTION_VALUES[card.front] ?? 1.5) * (selia ? 1 + 0.6 * years : years ? 1 : 0.5);
211
+ continue;
212
+ }
213
+ const abilities = data.abilities ?? [];
214
+ if (!abilities.length)
215
+ continue;
216
+ const repeatable = abilities.filter((ability) => !discardsCard(ability.requirements));
217
+ if (!repeatable.length) {
218
+ value += 0.7 * Math.max(0, ...abilities.map((ability) => this.genericValue(model, ability.requirements, ability.gains)));
219
+ continue;
220
+ }
221
+ if (!years)
222
+ continue;
223
+ const best = Math.max(0, ...repeatable.map((ability) => this.genericValue(model, ability.requirements, ability.gains)));
224
+ if (repeatable.some((ability) => ability.gains?.some((gain) => gain.type === GainType.Travel))) {
225
+ journeys.push(best + (elwen ? 0.5 : 0));
226
+ }
227
+ else {
228
+ value += best * this.usefulYears(model, repeatable.flatMap((ability) => ability.gains ?? []));
229
+ }
230
+ }
231
+ journeys.sort((a, b) => b - a);
232
+ journeys.forEach((journey, rank) => (value += journey * years * TRAVEL_DUPLICATES[Math.min(rank, TRAVEL_DUPLICATES.length - 1)]));
233
+ for (const card of model.companions) {
234
+ value += this.reactionYearValue(model, card.front) * years + this.growthValue(model, card.front);
235
+ }
236
+ return value;
237
+ }
238
+ usefulYears(model, gains) {
239
+ const skills = gains.filter((gain) => gain.type === GainType.Force || gain.type === GainType.Magic || gain.type === GainType.Skill);
240
+ if (!skills.length || skills.length < gains.length)
241
+ return this.yearsAfter;
242
+ const headroom = MAX_SKILL - Math.min(model.force, model.magic);
243
+ return Math.min(this.yearsAfter, headroom);
244
+ }
245
+ genericValue(model, requirements = [], gains = []) {
246
+ let value = 0;
247
+ for (const gain of gains) {
248
+ switch (gain.type) {
249
+ case GainType.Coins:
250
+ value += gain.count * COIN_RATE * 0.9;
251
+ break;
252
+ case GainType.Vp:
253
+ value += typeof gain.count === 'number' ? gain.count : 2;
254
+ break;
255
+ case GainType.Force:
256
+ value += this.nextLevels(model.force, gain.count);
257
+ break;
258
+ case GainType.Magic:
259
+ value += this.nextLevels(model.magic, gain.count);
260
+ break;
261
+ case GainType.Skill:
262
+ value += Math.max(this.nextLevels(model.force, 1), this.nextLevels(model.magic, 1));
263
+ break;
264
+ case GainType.Villager:
265
+ value += 2;
266
+ break;
267
+ case GainType.Travel:
268
+ value += TRAVEL_VALUES[Math.min(typeof gain.count === 'number' ? gain.count : 2, TRAVEL_VALUES.length - 1)];
269
+ break;
270
+ case GainType.Straighten:
271
+ case GainType.PlaceVillager:
272
+ value += 1;
273
+ break;
274
+ case GainType.TellStory:
275
+ value += 2.5;
276
+ break;
277
+ case GainType.Score:
278
+ value += modelScoreValue(model, gain.score) + 0.5;
279
+ break;
280
+ case GainType.IncomeToken:
281
+ value += 2;
282
+ break;
283
+ case GainType.BonusToken:
284
+ value += 3;
285
+ break;
286
+ }
287
+ }
288
+ for (const requirement of requirements) {
289
+ const count = requirement.count ?? 1;
290
+ switch (requirement.type) {
291
+ case RequirementType.SpendCoins:
292
+ value -= count * COIN_RATE;
293
+ break;
294
+ case RequirementType.SpendForce:
295
+ case RequirementType.SpendMagic:
296
+ value -= count * 1.4;
297
+ break;
298
+ case RequirementType.SpendVillagers:
299
+ value -= count * 1.1;
300
+ break;
301
+ case RequirementType.ReturnVillager:
302
+ value -= 5;
303
+ break;
304
+ case RequirementType.DiscardItem:
305
+ value -= 3;
306
+ break;
307
+ case RequirementType.TellStories:
308
+ value -= 2 * count;
309
+ break;
310
+ case RequirementType.Seal:
311
+ case RequirementType.SealCoins:
312
+ value -= 1.5;
313
+ break;
314
+ }
315
+ }
316
+ return value;
317
+ }
318
+ nextLevels(level, count) {
319
+ let value = 0;
320
+ for (let step = level; step < Math.min(MAX_SKILL, level + count); step++)
321
+ value += LEVEL_VALUES[step];
322
+ return value * this.skillFactor;
323
+ }
324
+ reactionYearValue(model, front) {
325
+ const items = model.items.map((card) => villageCardData[card.front]);
326
+ const journeys = items.filter((data) => data.abilities?.some((ability) => ability.gains?.some((gain) => gain.type === GainType.Travel))).length;
327
+ switch (front) {
328
+ case VillageCard.Kael:
329
+ return 1;
330
+ case VillageCard.Elwen:
331
+ return 0.8 + 0.3 * journeys;
332
+ case VillageCard.Dorian:
333
+ return 1.3;
334
+ case VillageCard.Ariok:
335
+ return model.magic > 0 ? 1 : 0.4;
336
+ case VillageCard.Selia:
337
+ return 0.7;
338
+ case VillageCard.Neris:
339
+ return 1.4;
340
+ case VillageCard.Bran: {
341
+ const needVillagers = items.filter((data) => data.abilities?.some((ability) => ability.requirements?.some((requirement) => requirement.type === RequirementType.SpendVillagers))).length;
342
+ return 0.3 + 0.5 * Math.min(2, needVillagers);
343
+ }
344
+ case VillageCard.Lucan:
345
+ return Math.min(model.force, model.magic) < MAX_SKILL ? 1.4 : 0.2;
346
+ case VillageCard.Isandre:
347
+ return 0.4 + 0.4 * Math.min(3, items.filter((data) => data.abilities?.length).length);
348
+ case VillageCard.Seren:
349
+ return 0.4 + 0.3 * Math.min(3, model.untold.filter((card) => encounterCardData[card].story < MAX_STORY_VALUE).length);
350
+ case VillageCard.Mira:
351
+ return 1.1;
352
+ default:
353
+ return 0;
354
+ }
355
+ }
356
+ growthValue(model, front) {
357
+ const score = villageCardData[front].score;
358
+ if (!score?.per)
359
+ return 0;
360
+ const years = this.yearsAfter;
361
+ const thisYear = this.season === Season.Autumn ? 0 : 1;
362
+ const count = modelCount(model, score.per);
363
+ let growth = 0;
364
+ switch (score.per) {
365
+ case Countable.Force:
366
+ case Countable.Magic:
367
+ case Countable.LowestSkill:
368
+ growth = Math.min(MAX_SKILL - count, years + thisYear) * 0.5;
369
+ break;
370
+ case Countable.Item:
371
+ growth = Math.min(Math.max(0, modelItemLimit(model) - count), years + thisYear) * 0.4;
372
+ break;
373
+ case Countable.Companion:
374
+ growth = Math.min(MAX_COMPANIONS - count, years) * 0.5;
375
+ break;
376
+ case Countable.Potion:
377
+ growth = years * 0.2;
378
+ break;
379
+ case Countable.ToldStory:
380
+ growth = (years + thisYear) * 0.8;
381
+ break;
382
+ case Countable.Villagers:
383
+ growth = Math.min(7 - count, years) * 0.4;
384
+ break;
385
+ case Countable.FarEncounter:
386
+ growth = (years + thisYear) * 0.7;
387
+ break;
388
+ case Countable.DistinctBanner:
389
+ growth = Math.min(5 - count, (years + thisYear) * 0.7);
390
+ break;
391
+ }
392
+ return (score.vp * Math.max(0, growth)) / (score.divide ?? 1);
393
+ }
394
+ storiesValue(model) {
395
+ const points = storyPoints(model.untold);
396
+ if (this.yearsAfter > 0) {
397
+ const capacity = MAX_STORY_VALUE * (this.yearsAfter + 1) * 1.3;
398
+ return Math.min(points, capacity) * STORY_POINT_VALUE + model.untold.length * 0.15;
399
+ }
400
+ return Math.min(points, MAX_STORY_VALUE * this.tellingChances) * 0.6 + model.untold.length * 0.05;
401
+ }
402
+ get tellingChances() {
403
+ if (this.tellingChancesCache === undefined) {
404
+ const taverns = this.grid
405
+ .getItems()
406
+ .filter((item) => villageCardData[item.id.front].abilities?.some((ability) => ability.gains?.some((gain) => gain.type === GainType.TellStory))).length;
407
+ this.tellingChancesCache = this.season === Season.Autumn ? 0 : (this.specialActionUsed ? 0 : 1) + 0.7 * taverns;
408
+ }
409
+ return this.tellingChancesCache;
410
+ }
411
+ questsValue(model) {
412
+ if (!model.markers)
413
+ return 0;
414
+ const journeys = model.items.filter((card) => villageCardData[card.front].abilities?.some((a) => a.gains?.some((g) => g.type === GainType.Travel))).length;
415
+ const reachBonus = Math.min(0.3, 0.1 * (journeys + (owns(model, VillageCard.Elwen) ? 1 : 0)));
416
+ const timeFactor = this.yearsAfter > 0 ? 1 : 0.6;
417
+ let value = 0;
418
+ for (const quest of this.quests) {
419
+ if (model.questsDone.includes(quest.area))
420
+ continue;
421
+ const reward = quest.firstTaken ? questRewards[quest.area].others : questRewards[quest.area].first;
422
+ const [readiness, cost] = this.questReadiness(model, quest.tile);
423
+ const reach = { [Area.Hammer]: 0.5, [Area.Swords]: 0.4, [Area.Edge]: 0.33 }[quest.area] + reachBonus;
424
+ value += Math.max(0, reward - cost) * readiness * readiness * reach * timeFactor * 0.5;
425
+ }
426
+ return value;
427
+ }
428
+ questReadiness(model, tile) {
429
+ switch (tile) {
430
+ case QuestTile.Giant:
431
+ return [Math.min(1, model.force / 4), 0];
432
+ case QuestTile.Wraiths:
433
+ return [Math.min(1, model.magic / 4), 0];
434
+ case QuestTile.Dragon:
435
+ return [Math.min(1, (model.force + model.magic) / 6), 0];
436
+ case QuestTile.BardTournament:
437
+ return [Math.min(1, model.untold.length / 2), 3];
438
+ case QuestTile.Donation:
439
+ return [model.items.length ? 1 : 0.4, 3];
440
+ case QuestTile.Undeads:
441
+ return [((model.force > 0 ? 1 : 0) + (model.magic > 0 ? 1 : 0)) / 2, 3];
442
+ case QuestTile.DeadlyTrap:
443
+ return [1, 1 + this.yearsAfter * 2.3];
444
+ case QuestTile.Ransom:
445
+ return [Math.min(1, model.coins / 6), 4.5];
446
+ case QuestTile.Wedding:
447
+ return [1, 2];
448
+ }
449
+ }
450
+ lastYearValue(model) {
451
+ const reach = LAST_YEAR_REACH[this.players] ?? 0.3;
452
+ const cards = this.lastYearCards;
453
+ let value = 0;
454
+ if (cards.has(VillageCard.Fortress))
455
+ value += 2 * model.companions.length;
456
+ if (cards.has(VillageCard.Library))
457
+ value += model.told.length;
458
+ if (cards.has(VillageCard.ScarletDragon))
459
+ value += 0.8 * Math.min(MAX_STORY_VALUE, storyPoints(model.untold));
460
+ if (cards.has(VillageCard.Mandolin))
461
+ value += 0.8 * model.untold.length;
462
+ if (cards.has(VillageCard.BagOfHolding))
463
+ value += 0.8 * model.items.length;
464
+ return value * reach;
465
+ }
466
+ effectValue(requirements = [], gains = [], options = {}, base = this.model) {
467
+ const model = cloneModel(base);
468
+ let extra = 0;
469
+ if (options.price)
470
+ model.coins -= options.price;
471
+ for (const requirement of gathered(requirements)) {
472
+ const count = requirement.count ?? 1;
473
+ switch (requirement.type) {
474
+ case RequirementType.Force:
475
+ if (base.force < count)
476
+ return undefined;
477
+ break;
478
+ case RequirementType.Magic:
479
+ if (base.magic < count)
480
+ return undefined;
481
+ break;
482
+ case RequirementType.Skills:
483
+ if (base.force + base.magic < count)
484
+ return undefined;
485
+ break;
486
+ case RequirementType.SpendCoins:
487
+ model.coins -= count;
488
+ break;
489
+ case RequirementType.SpendForce:
490
+ if (model.force < count)
491
+ return undefined;
492
+ model.force -= count;
493
+ break;
494
+ case RequirementType.SpendMagic:
495
+ if (model.magic < count)
496
+ return undefined;
497
+ model.magic -= count;
498
+ break;
499
+ case RequirementType.SpendVillagers:
500
+ if (this.active < count)
501
+ return undefined;
502
+ if (!options.villagersFree)
503
+ extra -= count * this.villagerUseCost;
504
+ break;
505
+ case RequirementType.ReturnVillager:
506
+ if (this.active < count)
507
+ return undefined;
508
+ model.villagers = clampVillagers(model.villagers - count);
509
+ extra -= count * this.villagerUseCost;
510
+ break;
511
+ case RequirementType.DiscardItem:
512
+ if (!model.items.length)
513
+ return undefined;
514
+ this.removeLeastItem(model);
515
+ break;
516
+ case RequirementType.TellStories: {
517
+ if (model.untold.length < count)
518
+ return undefined;
519
+ const cheapest = [...model.untold].sort((a, b) => encounterCardData[a].story - encounterCardData[b].story).slice(0, count);
520
+ model.untold = model.untold.filter((card) => !cheapest.includes(card));
521
+ model.told.push(...cheapest);
522
+ break;
523
+ }
524
+ case RequirementType.SealCoins:
525
+ model.coins -= options.seal ?? 1;
526
+ break;
527
+ }
528
+ }
529
+ if (model.coins < 0)
530
+ return undefined;
531
+ if (options.discard !== undefined) {
532
+ const index = model.items.findIndex((card) => card.front === options.discard);
533
+ if (index >= 0)
534
+ model.items.splice(index, 1);
535
+ }
536
+ if (options.encounter !== undefined)
537
+ model.untold.push(options.encounter);
538
+ if (options.quest !== undefined) {
539
+ const quest = this.quests.find((entry) => entry.area === options.quest);
540
+ model.questPoints += quest.firstTaken ? questRewards[quest.area].others : questRewards[quest.area].first;
541
+ model.questsDone.push(quest.area);
542
+ model.markers--;
543
+ }
544
+ const vpBefore = model.vp;
545
+ for (const gain of gains)
546
+ extra += this.applyGain(model, gain, options);
547
+ extra += this.bonusTokenValue(vpBefore, model.vp);
548
+ return this.staticValue(model) - (base === this.model ? this.base : this.staticValue(base)) + extra;
549
+ }
550
+ bonusTokenValue(before, after) {
551
+ const tokens = this.rules.material(MaterialType.BonusToken).location(LocationType.BonusTokens).player(this.player).length;
552
+ let value = 0;
553
+ if (before < 8 && after >= 8 && tokens === 3)
554
+ value += 3.5;
555
+ if (before < 20 && after >= 20 && tokens >= 2)
556
+ value += 3.5;
557
+ return value;
558
+ }
559
+ applyGain(model, gain, options) {
560
+ const amount = (count) => (count === SEAL ? (options.seal ?? 0) : count);
561
+ switch (gain.type) {
562
+ case GainType.Coins:
563
+ model.coins += amount(gain.count);
564
+ return 0;
565
+ case GainType.Vp:
566
+ model.vp += amount(gain.count);
567
+ return 0;
568
+ case GainType.Force:
569
+ model.force = clampSkill(model.force + amount(gain.count));
570
+ return 0;
571
+ case GainType.Magic:
572
+ model.magic = clampSkill(model.magic + amount(gain.count));
573
+ return 0;
574
+ case GainType.Skill: {
575
+ const withForce = cloneModel(model);
576
+ withForce.force = clampSkill(model.force + amount(gain.count));
577
+ const withMagic = cloneModel(model);
578
+ withMagic.magic = clampSkill(model.magic + amount(gain.count));
579
+ if (this.staticValue(withForce) >= this.staticValue(withMagic))
580
+ model.force = withForce.force;
581
+ else
582
+ model.magic = withMagic.magic;
583
+ return 0;
584
+ }
585
+ case GainType.Villager: {
586
+ const before = model.villagers;
587
+ model.villagers = clampVillagers(model.villagers + amount(gain.count));
588
+ return this.season === Season.Autumn ? 0 : (model.villagers - before) * this.newVillagerValue;
589
+ }
590
+ case GainType.Travel: {
591
+ const distance = amount(gain.count);
592
+ if (!options.nested)
593
+ return this.travelValue(distance, model);
594
+ if (options.origin !== undefined && !options.deep)
595
+ return this.travelValue(distance, model, options.origin, true);
596
+ return TRAVEL_VALUES[Math.min(distance, TRAVEL_VALUES.length - 1)];
597
+ }
598
+ case GainType.Straighten:
599
+ return options.nested ? 1 : this.straightenValue();
600
+ case GainType.PlaceVillager:
601
+ return this.active > 0 && this.season !== Season.Autumn ? 1 : 0;
602
+ case GainType.TellStory:
603
+ return options.nested ? 2 : (this.storyValue(gain.rewards) ?? 0);
604
+ case GainType.Score:
605
+ model.vp += modelScoreValue(model, gain.score);
606
+ return 0;
607
+ case GainType.IncomeToken:
608
+ if (options.incomeToken === undefined)
609
+ return 0;
610
+ model.incomeTokens.push(options.incomeToken);
611
+ return incomeTokenGains[options.incomeToken].reduce((total, tokenGain) => total + this.applyGain(model, tokenGain, options), 0);
612
+ case GainType.BonusToken:
613
+ return 3;
614
+ default:
615
+ return 0;
616
+ }
617
+ }
618
+ removeLeastItem(model) {
619
+ let best = -Infinity;
620
+ let kept = model.items;
621
+ for (let index = 0; index < model.items.length; index++) {
622
+ const copy = cloneModel(model);
623
+ copy.items.splice(index, 1);
624
+ const value = this.staticValue(copy);
625
+ if (value > best) {
626
+ best = value;
627
+ kept = copy.items;
628
+ }
629
+ }
630
+ model.items = kept;
631
+ }
632
+ get villagerUseCost() {
633
+ if (this.season === Season.Spring)
634
+ return Math.max(1.8, this.springFloor);
635
+ return this.active > 2 ? 0.6 : 1;
636
+ }
637
+ get newVillagerValue() {
638
+ return this.season === Season.Spring ? 1.8 : 1;
639
+ }
640
+ travelValue(distance, base = this.model, origin = this.area, deep = false) {
641
+ const key = `${distance}:${origin}:${deep}:${signature(base)}`;
642
+ const cached = this.travelCache.get(key);
643
+ if (cached !== undefined)
644
+ return cached;
645
+ let value = this.reachValue(distance, base, origin, deep);
646
+ const elwen = base.companions.find((card) => card.front === VillageCard.Elwen && !card.tilted);
647
+ if (elwen)
648
+ value = Math.max(value, this.reachValue(distance + 2, base, origin, deep) - 0.8);
649
+ this.travelCache.set(key, value);
650
+ return value;
651
+ }
652
+ reachValue(distance, base, origin, deep) {
653
+ let value = 0;
654
+ for (let area = Math.max(Area.Wand, origin - distance); area <= Math.min(Area.Edge, origin + distance); area++) {
655
+ value = Math.max(value, this.areaValue(area, base, deep));
656
+ }
657
+ return value;
658
+ }
659
+ areaValue(area, base = this.model, deep = false) {
660
+ const key = `${area}:${deep}:${signature(base)}`;
661
+ const cached = this.areaCache.get(key);
662
+ if (cached !== undefined)
663
+ return cached;
664
+ let value = 0;
665
+ const skip = area === Area.Wand
666
+ ? this.effectValue([], [{ type: GainType.Coins, count: 1 }], {}, base)
667
+ : area === Area.Bow
668
+ ? this.effectValue([], [vp(1)], {}, base)
669
+ : undefined;
670
+ if (skip !== undefined)
671
+ value = Math.max(value, skip);
672
+ for (const card of this.rules.material(MaterialType.EncounterCard).location(LocationType.EncounterRow).locationId(area).getIndexes()) {
673
+ value = Math.max(value, this.encounterValue(card, base, deep) ?? 0);
674
+ }
675
+ const quest = this.quests.find((entry) => entry.area === area);
676
+ if (quest && base.markers && !base.questsDone.includes(quest.area)) {
677
+ value = Math.max(value, this.effectValue(questRequirements[quest.tile], [], { quest: quest.area, nested: true }, base) ?? 0);
678
+ }
679
+ this.areaCache.set(key, value);
680
+ return value;
681
+ }
682
+ encounterValue(card, base = this.model, deep = false) {
683
+ const front = this.rules.material(MaterialType.EncounterCard).getItem(card).id.front;
684
+ const incomeToken = this.rules.material(MaterialType.IncomeToken).location(LocationType.CardIncome).parent(card).getItem()?.id;
685
+ const outcomes = encounterCardData[front].outcomes;
686
+ const subsets = outcomes.length > 1 ? [[0], [1], [0, 1]] : [[0]];
687
+ let best;
688
+ for (const subset of subsets) {
689
+ const requirements = subset.flatMap((outcome) => outcomes[outcome].requirements ?? []);
690
+ const gains = subset.flatMap((outcome) => outcomes[outcome].gains ?? []);
691
+ const value = this.effectValue(requirements, gains, { encounter: front, incomeToken, nested: true, origin: encounterArea[front], deep }, base);
692
+ if (value !== undefined && (best === undefined || value > best))
693
+ best = value;
694
+ }
695
+ return best;
696
+ }
697
+ storyValue(rewards) {
698
+ const key = JSON.stringify(rewards);
699
+ if (this.storyCache.has(key))
700
+ return this.storyCache.get(key);
701
+ const cards = this.model.untold.filter((card) => encounterCardData[card].story > 0);
702
+ const seen = new Set();
703
+ let best;
704
+ const consider = (told) => {
705
+ const values = told.map((card) => encounterCardData[card].story).sort();
706
+ const sum = values.reduce((total, value) => total + value, 0);
707
+ if (!(sum <= MAX_STORY_VALUE || (values.length === 2 && values[0] === 2 && values[1] === 2)))
708
+ return;
709
+ const signature = values.join();
710
+ if (seen.has(signature))
711
+ return;
712
+ seen.add(signature);
713
+ const model = cloneModel(this.model);
714
+ for (const card of told) {
715
+ model.untold.splice(model.untold.indexOf(card), 1);
716
+ model.told.push(card);
717
+ }
718
+ let extra = 0;
719
+ for (const gain of rewards.slice(0, Math.min(MAX_STORY_VALUE, sum)).flat())
720
+ extra += this.applyGain(model, gain, { nested: true });
721
+ const value = this.staticValue(model) - this.base + extra;
722
+ if (best === undefined || value > best)
723
+ best = value;
724
+ };
725
+ for (let a = 0; a < cards.length; a++) {
726
+ consider([cards[a]]);
727
+ for (let b = a + 1; b < cards.length; b++) {
728
+ consider([cards[a], cards[b]]);
729
+ for (let c = b + 1; c < cards.length; c++)
730
+ consider([cards[a], cards[b], cards[c]]);
731
+ }
732
+ }
733
+ this.storyCache.set(key, best);
734
+ return best;
735
+ }
736
+ straightenValue() {
737
+ if (this.season === Season.Autumn)
738
+ return 0;
739
+ let best = 0;
740
+ for (const card of this.model.items) {
741
+ if (!card.tilted)
742
+ continue;
743
+ for (const use of this.itemUses(card.front))
744
+ best = Math.max(best, use.value);
745
+ }
746
+ for (const card of this.model.companions)
747
+ if (card.tilted)
748
+ best = Math.max(best, this.reactionYearValue(this.model, card.front) * 0.5);
749
+ return best;
750
+ }
751
+ itemUses(front) {
752
+ const data = villageCardData[front];
753
+ if (data.potion)
754
+ return [];
755
+ return (data.abilities ?? []).flatMap((ability) => {
756
+ const requirements = ability.requirements ?? [];
757
+ const slots = requirements.filter((requirement) => requirement.type === RequirementType.SpendVillagers).reduce((total, r) => total + (r.count ?? 1), 0);
758
+ const value = this.effectValue(requirements, ability.gains, { villagersFree: true, discard: discardsCard(requirements) ? front : undefined });
759
+ return value === undefined ? [] : [{ value, slots }];
760
+ });
761
+ }
762
+ slotOf(card) {
763
+ const location = this.grid.getItem(card).location;
764
+ return { x: location.x ?? 0, y: location.y ?? 0 };
765
+ }
766
+ seals(card) {
767
+ return this.rules
768
+ .material(MaterialType.Seal)
769
+ .location(LocationType.CardSeal)
770
+ .parent(card)
771
+ .getItems()
772
+ .map((item) => item.id);
773
+ }
774
+ activationValue(card, crowd) {
775
+ const key = `${card}:${crowd}`;
776
+ if (this.activationCache.has(key))
777
+ return this.activationCache.get(key);
778
+ const result = this.computeActivation(card, crowd);
779
+ this.activationCache.set(key, result);
780
+ return result;
781
+ }
782
+ computeActivation(card, crowd) {
783
+ const front = this.grid.getItem(card).id.front;
784
+ const data = villageCardData[front];
785
+ const type = getVillageCardType(front);
786
+ const neris = this.model.companions.some((owned) => owned.front === VillageCard.Neris && !owned.tilted);
787
+ const surcharge = neris && crowd > 1 ? 0 : crowd;
788
+ const nerisCost = surcharge < crowd ? 0.7 : 0;
789
+ if (type === VillageCardType.Building) {
790
+ let best;
791
+ for (const ability of data.abilities ?? []) {
792
+ if (ability.gains?.some((gain) => gain.type === GainType.TellStory) && !this.model.untold.some((c) => encounterCardData[c].story > 0))
793
+ continue;
794
+ const seals = usesSeal(ability.requirements) ? [...new Set(this.seals(card))] : [undefined];
795
+ for (const seal of seals) {
796
+ const value = this.effectValue(ability.requirements, ability.gains, { price: surcharge, seal });
797
+ if (value === undefined)
798
+ continue;
799
+ const coins = surcharge + (ability.requirements ?? []).reduce((total, r) => total + (r.type === RequirementType.SpendCoins ? (r.count ?? 1) : r.type === RequirementType.SealCoins ? (seal ?? 1) : 0), 0);
800
+ if (!best || value - nerisCost > best.value)
801
+ best = { value: value - nerisCost, coins };
802
+ }
803
+ }
804
+ return best;
805
+ }
806
+ const dorian = type === VillageCardType.Item && this.model.companions.some((owned) => owned.front === VillageCard.Dorian && !owned.tilted);
807
+ const price = data.cost + surcharge - (dorian ? 1 : 0);
808
+ if (this.model.coins < price)
809
+ return undefined;
810
+ if (type === VillageCardType.Companion && this.model.companions.length >= MAX_COMPANIONS)
811
+ return undefined;
812
+ const model = cloneModel(this.model);
813
+ model.coins -= price;
814
+ let extra = dorian ? 0.5 : -nerisCost;
815
+ if (dorian)
816
+ model.vp += 1;
817
+ if (type === VillageCardType.Item)
818
+ model.items.push({ front, tilted: false });
819
+ else
820
+ model.companions.push({ front, tilted: false });
821
+ const vpBefore = model.vp;
822
+ const seal = usesSeal(data.immediate?.requirements) ? this.seals(card)[0] : undefined;
823
+ if (!usesSeal(data.immediate?.requirements) || seal !== undefined) {
824
+ for (const gain of data.immediate?.gains ?? [])
825
+ extra += this.applyGain(model, gain, { seal });
826
+ }
827
+ extra += this.bonusTokenValue(vpBefore, model.vp);
828
+ if (type === VillageCardType.Item) {
829
+ if (model.items.length > modelItemLimit(model))
830
+ this.removeLeastItem(model);
831
+ if (this.season !== Season.Autumn && model.items.some((owned) => owned.front === front)) {
832
+ extra += 0.5 * Math.max(0, ...this.itemUses(front).map((use) => use.value - use.slots * this.villagerUseCost));
833
+ }
834
+ }
835
+ else if (this.season !== Season.Autumn) {
836
+ extra += 0.5 * this.reactionYearValue(model, front);
837
+ }
838
+ return { value: this.staticValue(model) - this.base + extra, coins: price };
839
+ }
840
+ risk(card, crowd) {
841
+ const front = this.grid.getItem(card).id.front;
842
+ const others = crowd.others + crowd.arrivals;
843
+ if (getVillageCardType(front) === VillageCardType.Building) {
844
+ if (!usesSeal(villageCardData[front].abilities?.[0]?.requirements))
845
+ return 0.03;
846
+ return this.seals(card).length <= others ? 0.35 : 0.05;
847
+ }
848
+ return Math.min(0.7, 0.05 + 0.15 * others);
849
+ }
850
+ cardKey(card) {
851
+ const front = this.grid.getItem(card).id.front;
852
+ if (getVillageCardType(front) !== VillageCardType.Building)
853
+ return { key: `card${card}`, capacity: 1 };
854
+ if (!usesSeal(villageCardData[front].abilities?.[0]?.requirements))
855
+ return {};
856
+ return { key: `card${card}`, capacity: this.seals(card).length };
857
+ }
858
+ coinsAroundValue(coins) {
859
+ const neris = this.model.companions.some((owned) => owned.front === VillageCard.Neris && !owned.tilted);
860
+ const total = coins + (neris ? 2 : 0);
861
+ return this.coinWorth(this.model, this.model.coins + total) - this.coinWorth(this.model, this.model.coins) - (neris ? 0.7 : 0);
862
+ }
863
+ crowdAround(card) {
864
+ const slot = this.slotOf(card);
865
+ const around = villagersAroundSlot(this.rules, slot).getItems();
866
+ const mine = around.filter((item) => getVillagerPlayer(item.id) === this.player).length;
867
+ const arrivals = (this.pendingPlacements * slotGaps(slot).length) / villageGaps.length;
868
+ return { mine, others: around.length - mine, arrivals };
869
+ }
870
+ thisYearPotential(explain) {
871
+ if (this.season === Season.Autumn)
872
+ return 0;
873
+ const uses = [];
874
+ const name = (card) => VillageCard[this.grid.getItem(card).id.front];
875
+ let expectedCoins = 0;
876
+ const neighbours = (card, crowd, entry, factor = 1) => {
877
+ const coins = (crowd.others + crowd.arrivals) * 0.75 + crowd.mine / 2;
878
+ const how = entry.villager === undefined ? 'place:' : '';
879
+ uses.push({ ...entry, label: `${how}coins@${name(card)}`, value: this.coinsAroundValue(coins) * factor });
880
+ const activation = this.activationValue(card, Math.round(crowd.others + (crowd.arrivals + crowd.mine) * 0.5));
881
+ if (activation) {
882
+ const value = activation.value * (1 - this.risk(card, crowd)) * factor;
883
+ uses.push({ ...entry, ...this.cardKey(card), label: `${how}${name(card)}`, coins: activation.coins, value });
884
+ }
885
+ return coins;
886
+ };
887
+ for (const [villager, item] of villagersInVillage(this.rules, this.player).entries) {
888
+ const cards = cardsAroundGap(this.rules, gapOf(item.location));
889
+ uses.push({ villager, value: 0 });
890
+ let best = 0;
891
+ for (const card of cards) {
892
+ const crowd = this.crowdAround(card);
893
+ best = Math.max(best, neighbours(card, { ...crowd, mine: crowd.mine - 1 }, { villager }));
894
+ }
895
+ expectedCoins += best / 2;
896
+ }
897
+ if (this.season === Season.Spring && this.active > 0) {
898
+ const seen = new Set();
899
+ for (const gap of villageGaps) {
900
+ for (const card of cardsAroundGap(this.rules, gap)) {
901
+ if (seen.has(card))
902
+ continue;
903
+ seen.add(card);
904
+ neighbours(card, this.crowdAround(card), { slots: 1 }, SPRING_PLACEMENT);
905
+ }
906
+ }
907
+ }
908
+ if (!this.eventUsed) {
909
+ const event = this.eventValue();
910
+ if (event !== undefined)
911
+ uses.push({ slots: 1, key: 'event', label: 'event', capacity: 1, value: event * (this.season === Season.Spring ? 0.95 : 1) });
912
+ }
913
+ if (!this.specialActionUsed) {
914
+ const special = this.specialActionValue();
915
+ if (special !== undefined)
916
+ uses.push({ slots: 1, key: 'special', label: 'special', capacity: 1, value: special * (this.season === Season.Spring ? 0.9 : 1) });
917
+ }
918
+ if (!this.eventUsed && !this.specialActionUsed && this.model.magic < MAX_SKILL) {
919
+ const magicModel = cloneModel(this.model);
920
+ magicModel.magic++;
921
+ const boosted = this.eventValue(magicModel);
922
+ const gained = this.effectValue([], [magic()]);
923
+ if (boosted !== undefined && gained !== undefined && boosted > (this.eventValue() ?? 0)) {
924
+ uses.push({ slots: 2, keys: ['event', 'special'], label: 'special+event', value: (gained + boosted) * (this.season === Season.Spring ? 0.9 : 1) });
925
+ }
926
+ }
927
+ this.model.items.forEach((card, index) => {
928
+ if (card.tilted)
929
+ return;
930
+ for (const use of this.itemUses(card.front)) {
931
+ uses.push({ slots: use.slots, key: `item${index}`, label: `${VillageCard[card.front]}/${use.slots}`, capacity: 1, value: use.value });
932
+ }
933
+ });
934
+ let total = 0;
935
+ for (const card of this.model.companions)
936
+ if (!card.tilted)
937
+ total += 0.5 * this.reactionYearValue(this.model, card.front);
938
+ uses.sort((a, b) => b.value - a.value);
939
+ const busy = new Set();
940
+ const keys = new Map();
941
+ let active = this.active;
942
+ let coins = this.model.coins + expectedCoins;
943
+ const floor = this.springFloor;
944
+ for (const use of uses) {
945
+ if (use.villager !== undefined && busy.has(use.villager))
946
+ continue;
947
+ if ((use.slots ?? 0) > active)
948
+ continue;
949
+ if (use.slots && use.value < floor * use.slots)
950
+ continue;
951
+ if (use.key && (keys.get(use.key) ?? 0) >= (use.capacity ?? 1))
952
+ continue;
953
+ if (use.keys?.some((key) => keys.has(key)))
954
+ continue;
955
+ if (use.coins && use.coins > coins)
956
+ continue;
957
+ if (use.value <= 0 && use.villager === undefined)
958
+ continue;
959
+ total += use.value;
960
+ explain?.push(`${use.label}=${use.value.toFixed(1)}`);
961
+ if (use.villager !== undefined)
962
+ busy.add(use.villager);
963
+ active -= use.slots ?? 0;
964
+ if (use.key)
965
+ keys.set(use.key, (keys.get(use.key) ?? 0) + 1);
966
+ for (const key of use.keys ?? [])
967
+ keys.set(key, 1);
968
+ coins -= use.coins ?? 0;
969
+ }
970
+ return total + active * (this.season === Season.Spring ? Math.max(floor, 0.4) : 0.25);
971
+ }
972
+ get springFloor() {
973
+ if (this.season !== Season.Spring)
974
+ return 0;
975
+ if (!villageGaps.some((gap) => cardsAroundGap(this.rules, gap).length > 0))
976
+ return 0;
977
+ return this.yearsAfter > 0 ? evaluationSettings.springFloor : evaluationSettings.lastSpringFloor;
978
+ }
979
+ get summerVillagerNeeds() {
980
+ let needs = (this.eventUsed ? 0 : 1) + (this.specialActionUsed ? 0 : 1);
981
+ const villagersAsked = (requirements = []) => gathered(requirements)
982
+ .filter((requirement) => requirement.type === RequirementType.SpendVillagers || requirement.type === RequirementType.ReturnVillager)
983
+ .reduce((total, requirement) => total + (requirement.count ?? 1), 0);
984
+ for (const card of this.model.items) {
985
+ const repeatable = (villageCardData[card.front].abilities ?? []).filter((ability) => !discardsCard(ability.requirements));
986
+ needs += Math.max(0, ...repeatable.map((ability) => villagersAsked(ability.requirements)));
987
+ }
988
+ let journey = 0;
989
+ for (const item of this.rules.material(MaterialType.EncounterCard).location(LocationType.EncounterRow).getItems()) {
990
+ journey = Math.max(journey, villagersAsked(encounterCardData[item.id.front].outcomes.flatMap((outcome) => outcome.requirements ?? [])));
991
+ }
992
+ for (const quest of this.quests) {
993
+ if (this.model.markers && !this.model.questsDone.includes(quest.area))
994
+ journey = Math.max(journey, villagersAsked(questRequirements[quest.tile]));
995
+ }
996
+ return needs + journey;
997
+ }
998
+ get eventUsed() {
999
+ return (this.rules
1000
+ .material(MaterialType.Villager)
1001
+ .location(LocationType.EventSpace)
1002
+ .id((villager) => getVillagerPlayer(villager) === this.player).length > 0);
1003
+ }
1004
+ get specialActionUsed() {
1005
+ return this.rules.material(MaterialType.Villager).location(LocationType.SpecialAction).player(this.player).length > 0;
1006
+ }
1007
+ eventValue(base = this.model) {
1008
+ const tile = this.rules.material(MaterialType.EventTile).location(LocationType.EventPile).rotation(true).getItem()?.id;
1009
+ if (tile === undefined)
1010
+ return undefined;
1011
+ const taken = this.rules
1012
+ .material(MaterialType.Villager)
1013
+ .location(LocationType.EventSpace)
1014
+ .getItems()
1015
+ .map((item) => item.location.x);
1016
+ let best;
1017
+ eventTileData[tile].abilities.forEach((ability, option) => {
1018
+ if (isFestival(tile) && taken.includes(option))
1019
+ return;
1020
+ const value = this.effectValue(ability.requirements, ability.gains, {}, base);
1021
+ if (value !== undefined && (best === undefined || value > best))
1022
+ best = value;
1023
+ });
1024
+ return best;
1025
+ }
1026
+ specialActionValue() {
1027
+ const options = [
1028
+ this.storyValue(specialActions[0][0].rewards),
1029
+ this.travelValue(1),
1030
+ this.model.magic < MAX_SKILL ? this.effectValue([], [magic()]) : undefined
1031
+ ].filter((value) => value !== undefined);
1032
+ return options.length ? Math.max(...options) : undefined;
1033
+ }
1034
+ pendingValue() {
1035
+ const rule = this.game.rule;
1036
+ if (!rule || rule.player !== this.player)
1037
+ return 0;
1038
+ const seal = this.memory[Memory.SealValue];
1039
+ const gains = this.memory[Memory.Gains] ?? [];
1040
+ let value = gains.length ? (this.effectValue([], gains, { seal }) ?? 0) : 0;
1041
+ const resume = this.memory[Memory.Resume];
1042
+ const next = rule.id === RuleId.Reaction ? resume : rule.id;
1043
+ const distance = this.memory[Memory.TravelDistance];
1044
+ switch (next) {
1045
+ case RuleId.Travel:
1046
+ value += this.travelValue(distance ?? 0);
1047
+ break;
1048
+ case RuleId.ResolveEncounter:
1049
+ case RuleId.ChooseOutcome:
1050
+ value += this.areaValue(this.area);
1051
+ break;
1052
+ case RuleId.ChooseSkill:
1053
+ value += Math.max(this.nextLevels(this.model.force, 1), this.nextLevels(this.model.magic, 1));
1054
+ break;
1055
+ case RuleId.BonusToken:
1056
+ value += 3;
1057
+ break;
1058
+ case RuleId.TellStory: {
1059
+ const rewards = this.memory[Memory.StoryRewards];
1060
+ if (rewards)
1061
+ value += this.storyValue(rewards) ?? 0;
1062
+ break;
1063
+ }
1064
+ }
1065
+ return value;
1066
+ }
1067
+ }
1068
+ export const evaluate = (game, player) => new Assessment(game, player).evaluate();
1069
+ //# sourceMappingURL=Evaluation.js.map