pyscreeps-arena 0.5.7a0__py3-none-any.whl → 0.5.7a2__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ from pyscreeps_arena.ui.qrecipe.qrecipe import QPSARecipe
@@ -0,0 +1,434 @@
1
+ from typing import List, Dict, Optional
2
+ import math
3
+
4
+ class PartsVector:
5
+ INDEXS = {
6
+ 'MOVE': 0,
7
+ 'CARRY': 1,
8
+ 'WORK': 2,
9
+ 'ATTACK': 3,
10
+ 'RANGED_ATTACK': 4,
11
+ 'HEAL': 5,
12
+ 'TOUGH': 6
13
+ }
14
+
15
+ VALUES = {
16
+ 0: 'MOVE',
17
+ 1: 'CARRY',
18
+ 2: 'WORK',
19
+ 3: 'ATTACK',
20
+ 4: 'RANGED_ATTACK',
21
+ 5: 'HEAL',
22
+ 6: 'TOUGH'
23
+ }
24
+
25
+ SCORE_TABLE = {
26
+ 'ATTACK': 10,
27
+ 'RANGED_ATTACK': 15,
28
+ 'HEAL': 15,
29
+ 'TOUGH': 1,
30
+ 'MOVE': 2,
31
+ 'CARRY': 2,
32
+ 'WORK': 5,
33
+ }
34
+
35
+ PARTS_COST = {
36
+ 'MOVE': 50,
37
+ 'WORK': 100,
38
+ 'CARRY': 50,
39
+ 'ATTACK': 80,
40
+ 'RANGED_ATTACK': 150,
41
+ 'HEAL': 250,
42
+ 'TOUGH': 10,
43
+ }
44
+
45
+ COLORS = {
46
+ 'MOVE': '#a5b7c6',
47
+ 'CARRY': '#5c6e74',
48
+ 'WORK': '#ffdb5b',
49
+ 'ATTACK': '#f92c2e',
50
+ 'RANGED_ATTACK': '#1e90ff',
51
+ 'HEAL': '#65d833',
52
+ 'TOUGH': '#FFFAFA'
53
+ }
54
+
55
+ def __init__(self, recipe: List[str]):
56
+ self.recipe = recipe
57
+ self.vec7 = [0] * 7
58
+
59
+ for part in self.recipe:
60
+ if part in self.INDEXS:
61
+ self.vec7[self.INDEXS[part]] += 1
62
+
63
+ # non-move non-carry count
64
+ self.nmCount = self.vec7[2] + self.vec7[3] + self.vec7[4] + self.vec7[5] + self.vec7[6]
65
+ # total count
66
+ self.bodyCount = len(self.recipe)
67
+
68
+ @property
69
+ def others(self):
70
+ return self.nmCount
71
+
72
+ @property
73
+ def moves(self):
74
+ return self.vec7[0]
75
+
76
+ @property
77
+ def carries(self):
78
+ return self.vec7[1]
79
+
80
+ @property
81
+ def works(self):
82
+ return self.vec7[2]
83
+
84
+ @property
85
+ def melees(self):
86
+ return self.vec7[3]
87
+
88
+ @property
89
+ def ranges(self):
90
+ return self.vec7[4]
91
+
92
+ @property
93
+ def heals(self):
94
+ return self.vec7[5]
95
+
96
+ @property
97
+ def toughs(self):
98
+ return self.vec7[6]
99
+
100
+ @property
101
+ def total(self):
102
+ return self.bodyCount
103
+
104
+ def add(self, other: 'PartsVector'):
105
+ for i in range(len(self.vec7)):
106
+ self.vec7[i] += other.vec7[i]
107
+ self.bodyCount += other.bodyCount
108
+ self.nmCount = self.vec7[2] + self.vec7[3] + self.vec7[4] + self.vec7[5] + self.vec7[6]
109
+
110
+ def sub(self, other: 'PartsVector'):
111
+ for i in range(len(self.vec7)):
112
+ self.vec7[i] = max(0, self.vec7[i] - other.vec7[i])
113
+ self.bodyCount = sum(self.vec7)
114
+ self.nmCount = self.vec7[2] + self.vec7[3] + self.vec7[4] + self.vec7[5] + self.vec7[6]
115
+
116
+ @staticmethod
117
+ def similarity(a: 'PartsVector', b: 'PartsVector') -> float:
118
+ if not a or not b:
119
+ raise ValueError("PartsVector instances cannot be None")
120
+
121
+ a_vec7 = a.vec7
122
+ b_vec7 = b.vec7
123
+
124
+ if len(a_vec7) != len(b_vec7):
125
+ raise ValueError(f"Vectors must be of the same length, but: {len(a_vec7)} and {len(b_vec7)}")
126
+
127
+ dot_product = 0
128
+ norm_a = 0
129
+ norm_b = 0
130
+
131
+ for i in range(len(a_vec7)):
132
+ dot_product += a_vec7[i] * b_vec7[i]
133
+ norm_a += a_vec7[i] ** 2
134
+ norm_b += b_vec7[i] ** 2
135
+
136
+ norm_a = math.sqrt(norm_a)
137
+ norm_b = math.sqrt(norm_b)
138
+
139
+ if norm_a == 0 or norm_b == 0:
140
+ return 0.0
141
+
142
+ return dot_product / (norm_a * norm_b)
143
+
144
+ @staticmethod
145
+ def parts_grade(recipe: List[str]) -> int:
146
+ carries = 0
147
+ moves = 0
148
+ score = 0
149
+ prt_length = len(recipe)
150
+ usage = 0
151
+ for i, prt in enumerate(recipe):
152
+ if prt == 'ATTACK':
153
+ score += 10 + i / 5
154
+ usage += 8
155
+ elif prt == 'RANGED_ATTACK':
156
+ score += 15 + i / 4
157
+ usage += 5
158
+ elif prt == 'HEAL':
159
+ score += 15 + i
160
+ usage += 5
161
+ elif prt == 'TOUGH':
162
+ score += 5 - i / (1+ 0.05 * prt_length)
163
+ usage -= 1
164
+ else:
165
+ score += 1
166
+ usage += 1
167
+ if prt == 'MOVE':
168
+ moves += 1
169
+ elif prt == 'CARRY':
170
+ carries += 1
171
+
172
+ usage_coef = 1 if usage > 0 else 0.5
173
+ length = (len(recipe) - carries) - moves
174
+ movements = moves * 2
175
+ swamp_ratio = 0.3 # Default value
176
+ move_cost = length * swamp_ratio * 20
177
+
178
+ if length == 0:
179
+ coef = 0
180
+ elif movements > move_cost:
181
+ coef = 2
182
+ else:
183
+ coef = (2 * movements) / move_cost
184
+
185
+ final_coef = (1 + coef) / 2 * usage_coef
186
+ return math.floor(score * final_coef)
187
+
188
+ @staticmethod
189
+ def parts_cost(recipe: List[str]) -> int:
190
+ cost = 0
191
+ for part in recipe:
192
+ cost += PartsVector.PARTS_COST.get(part, 0)
193
+ return cost
194
+
195
+ @staticmethod
196
+ def parts_optimise(parts: List[str]) -> List[str]:
197
+ if len(parts) <= 1:
198
+ return parts
199
+
200
+ # Step 1: Put all TOUGH parts at the front
201
+ tough_head = [part for part in parts if part == 'TOUGH']
202
+ rest_parts = [part for part in parts if part != 'TOUGH']
203
+
204
+ # Step 2: Create order
205
+ order = list(reversed(list(dict.fromkeys(reversed(rest_parts)))))
206
+
207
+ # Step 3: Count each kind
208
+ each_count = {}
209
+ for kind in order:
210
+ each_count[kind] = rest_parts.count(kind)
211
+
212
+ # Step 4: Find minimum count kind
213
+ min_count = float('inf')
214
+ min_kind = None
215
+ for kind in order:
216
+ if each_count[kind] < min_count:
217
+ min_count = each_count[kind]
218
+ min_kind = kind
219
+
220
+ # Step 5: Create unit_count and other_count
221
+ unit_count = {}
222
+ other_count = {}
223
+ for kind in order:
224
+ unit_count[kind] = each_count[kind] // min_count
225
+ other_count[kind] = each_count[kind] % min_count
226
+
227
+ # Step 6: Create group pattern
228
+ group_pattern = []
229
+ while True:
230
+ flag = False
231
+ for kind in order:
232
+ if unit_count[kind] > 0:
233
+ group_pattern.append(kind)
234
+ unit_count[kind] -= 1
235
+ flag = True
236
+ if not flag:
237
+ break
238
+ group_pattern = list(reversed(group_pattern))
239
+
240
+ # Step 7: Create others sequence
241
+ others_sequence = []
242
+ while True:
243
+ flag = False
244
+ for kind in order:
245
+ if other_count[kind] > 0:
246
+ others_sequence.append(kind)
247
+ other_count[kind] -= 1
248
+ flag = True
249
+ if not flag:
250
+ break
251
+
252
+ # Step 8: Split others into min_count lists
253
+ def split_others(min_count, others_sequence):
254
+ if min_count <= 1:
255
+ return [others_sequence]
256
+ total = (min_count + 1) * min_count / 2
257
+ last_idx = 0
258
+ cur_idx = 0
259
+ res = []
260
+ for i in range(min_count, 0, -1):
261
+ cur_idx = last_idx + (i / total) * len(others_sequence)
262
+ last_idx = math.ceil(last_idx)
263
+ cur_idx = math.ceil(cur_idx)
264
+ res.append(others_sequence[last_idx:cur_idx])
265
+ last_idx = cur_idx
266
+ return res
267
+
268
+ others_splits = split_others(min_count, others_sequence)
269
+
270
+ # Step 9: Special optimize
271
+ tails = []
272
+ move_count = each_count.get('MOVE', 0)
273
+ not_move_count = 0
274
+ for kind in order:
275
+ if kind == 'MOVE' or kind == 'CARRY':
276
+ continue
277
+ not_move_count += each_count.get(kind, 0)
278
+
279
+ if (move_count * 2) < (not_move_count * 5):
280
+ # Move all MOVE parts from others_splits[0] to tails
281
+ moves = [part for part in others_splits[0] if part == 'MOVE']
282
+ tails.extend(moves)
283
+ others_splits[0] = [part for part in others_splits[0] if part != 'MOVE']
284
+ elif move_count < not_move_count * 5:
285
+ # Move half of MOVE parts from others_splits[0] to tails
286
+ moves = [part for part in others_splits[0] if part == 'MOVE']
287
+ move_count_to_move = math.ceil(len(moves) / 2)
288
+ tails.extend(moves[:move_count_to_move])
289
+ for _ in range(move_count_to_move):
290
+ if 'MOVE' in others_splits[0]:
291
+ others_splits[0].remove('MOVE')
292
+
293
+ # Step 10: Merge all parts
294
+ res = []
295
+ res.extend(tough_head)
296
+ for i in range(min_count):
297
+ res.extend(others_splits[i])
298
+ res.extend(group_pattern)
299
+ res.extend(tails)
300
+
301
+ # Step 11: Optimize HEAL parts
302
+ heals = [part for part in res if part == 'HEAL']
303
+ for heal in heals:
304
+ res.remove(heal)
305
+ res.extend(heals)
306
+
307
+ return res
308
+
309
+ class NamedRecipe:
310
+ def __init__(self, name: str, recipe: List[str]):
311
+ self.name = name
312
+ self.recipe = recipe
313
+ self.vector = PartsVector(recipe)
314
+
315
+ class CreepInfo:
316
+ def __init__(self, recipe: List[str], named_recipe: Optional[NamedRecipe] = None):
317
+ self.recipe = recipe
318
+ self.named_recipe = named_recipe
319
+ self.vector = PartsVector(recipe)
320
+ self.dynamic_vector = PartsVector(recipe) # Same as vector initially
321
+
322
+ @property
323
+ def cost(self) -> int:
324
+ return PartsVector.parts_cost(self.recipe)
325
+
326
+ @property
327
+ def grade(self) -> int:
328
+ return PartsVector.parts_grade(self.recipe)
329
+
330
+ @property
331
+ def effect(self) -> float:
332
+ # Simplified effect calculation
333
+ return self.grade / max(1, self.cost / 100)
334
+
335
+ @property
336
+ def melee(self) -> bool:
337
+ return self.vector.melees > 0
338
+
339
+ @property
340
+ def ranged(self) -> bool:
341
+ return self.vector.ranges > 0
342
+
343
+ @property
344
+ def heal(self) -> bool:
345
+ return self.vector.heals > 0
346
+
347
+ @property
348
+ def work(self) -> bool:
349
+ return self.vector.works > 0
350
+
351
+ @property
352
+ def storable(self) -> bool:
353
+ return self.vector.carries > 0
354
+
355
+ @property
356
+ def attack_power(self) -> int:
357
+ return self.vector.melees * 30 + self.vector.ranges * 10
358
+
359
+ @property
360
+ def melee_power(self) -> int:
361
+ return self.vector.melees * 30
362
+
363
+ @property
364
+ def ranged_power(self) -> int:
365
+ return self.vector.ranges * 10
366
+
367
+ @property
368
+ def heal_power(self) -> int:
369
+ return self.vector.heals * 12
370
+
371
+ @property
372
+ def motion_ability(self) -> float:
373
+ if self.vector.others == 0:
374
+ return 0.0
375
+ move_ratio = self.vector.moves * 2 / (self.vector.others * 10)
376
+ return move_ratio
377
+
378
+ @property
379
+ def armor_ratio(self) -> float:
380
+ if self.vector.total == 0:
381
+ return 0.0
382
+ tough_ratio = self.vector.toughs / self.vector.total
383
+ return tough_ratio * 0.5
384
+
385
+ @property
386
+ def melee_ratio(self) -> float:
387
+ # Simplified calculation
388
+ return self.vector.total * self.attack_power
389
+
390
+ def get_recipe_string(self) -> str:
391
+ """Generate short string representation like W3M3"""
392
+ counts = {}
393
+ for part in self.recipe:
394
+ counts[part] = counts.get(part, 0) + 1
395
+
396
+ # Sort by priority
397
+ priority = ['WORK', 'ATTACK', 'RANGED_ATTACK', 'HEAL', 'TOUGH', 'CARRY', 'MOVE']
398
+ sorted_parts = sorted(counts.items(), key=lambda x: priority.index(x[0]) if x[0] in priority else len(priority))
399
+
400
+ result = []
401
+ for part, count in sorted_parts:
402
+ # Get first letter and add count
403
+ result.append(f"{part[0]}{count}")
404
+
405
+ return ''.join(result)
406
+
407
+ class RecipeModel:
408
+ def __init__(self):
409
+ self.recipe = []
410
+ self.optimise = True
411
+
412
+ def update_recipe(self, new_recipe: List[str]):
413
+ self.recipe = new_recipe
414
+
415
+ def set_optimise(self, optimise: bool):
416
+ self.optimise = optimise
417
+
418
+ def get_final_recipe(self) -> List[str]:
419
+ """Get final recipe with optimisation only (no multiplier applied to entire recipe)"""
420
+ if self.optimise:
421
+ return PartsVector.parts_optimise(self.recipe)
422
+ return self.recipe
423
+
424
+ def get_creep_info(self) -> CreepInfo:
425
+ final_recipe = self.get_final_recipe()
426
+ return CreepInfo(final_recipe)
427
+
428
+ def get_preview(self) -> str:
429
+ final_recipe = self.get_final_recipe()
430
+ return str(final_recipe).replace("'", "\"").replace('"', "'").replace("\", '")
431
+
432
+ def get_string_representation(self) -> str:
433
+ creep_info = self.get_creep_info()
434
+ return creep_info.get_recipe_string()