pyscreeps-arena 0.5.7a1__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.
- pyscreeps_arena/__init__.py +7 -2
- pyscreeps_arena/core/const.py +1 -1
- pyscreeps_arena/project.7z +0 -0
- pyscreeps_arena/ui/creeplogic_edit.py +14 -0
- pyscreeps_arena/ui/project_ui.py +1 -0
- pyscreeps_arena/ui/qcreeplogic/__init__.py +3 -0
- pyscreeps_arena/ui/qcreeplogic/model.py +72 -0
- pyscreeps_arena/ui/qcreeplogic/qcreeplogic.py +709 -0
- pyscreeps_arena/ui/qrecipe/__init__.py +1 -0
- pyscreeps_arena/ui/qrecipe/model.py +434 -0
- pyscreeps_arena/ui/qrecipe/qrecipe.py +914 -0
- {pyscreeps_arena-0.5.7a1.dist-info → pyscreeps_arena-0.5.7a2.dist-info}/METADATA +1 -1
- pyscreeps_arena-0.5.7a2.dist-info/RECORD +28 -0
- pyscreeps_arena-0.5.7a1.dist-info/RECORD +0 -21
- {pyscreeps_arena-0.5.7a1.dist-info → pyscreeps_arena-0.5.7a2.dist-info}/WHEEL +0 -0
- {pyscreeps_arena-0.5.7a1.dist-info → pyscreeps_arena-0.5.7a2.dist-info}/entry_points.txt +0 -0
- {pyscreeps_arena-0.5.7a1.dist-info → pyscreeps_arena-0.5.7a2.dist-info}/top_level.txt +0 -0
|
@@ -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()
|