Last active
March 11, 2019 13:38
-
-
Save johnchen902/9c877b14eb272d0d0dbacf68847d83e2 to your computer and use it in GitHub Desktop.
Girls Frontline Chip Calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from dataclasses import dataclass | |
from ortools.sat.python import cp_model | |
from functools import reduce | |
### PARAMETERS | |
def get_available_area(): | |
# This is 5-star AGS-30, change it for different HOC/stars. | |
return Mask(""" | |
..xx.... | |
.xxxx... | |
xxxxxx.. | |
xxxxxxx. | |
.xxxxxxx | |
..xxxxxx | |
...xxxx. | |
....xx.. | |
""") | |
def get_params(area): | |
# Minimum filled blocks, fully filled by default | |
minc = area.count() | |
# Minimum 殺傷 | |
mind = 0 | |
# Minimum 破防 | |
minp = 0 | |
# Minimum 精度 | |
mina = 0 | |
# Minimum 裝填 | |
minr = 0 | |
# Maximum calibration tickets (校準點券), default zero (i.e. no rotation) | |
maxt = 0 | |
return minc, mind, minp, mina, minr, maxt | |
def get_chips(): | |
chips = [] | |
def add_chip(maskstr, **kwargs): | |
chips.append(Chip(Mask(maskstr), **kwargs)) | |
# You can add a chip like this: | |
# a=精度 r=裝填 d=殺傷 p=破防 | |
add_chip(''' | |
xx | |
xxx | |
.x | |
''', a=15, r=6, d=9, p=13) | |
# You can also use a normal string | |
add_chip('.xx\nxx\n.xx', a=15, r=6, d=9, p=13) | |
# Stats can be written in any order | |
# Unspecified stats default to zero | |
add_chip('xx\n.xx\n.x', d=9, r=18) | |
# Some extra chips to make this example feasible | |
add_chip('.x\nxxx\nx') | |
add_chip('xx\n.xx\n..x') | |
add_chip('..x\nxxxx') | |
add_chip('xxxx\n.xx') | |
return chips | |
### END OF PARAMETERS | |
def iterate(func, init): | |
while True: | |
yield init | |
try: | |
init = func(init) | |
except ValueError: | |
break | |
@dataclass | |
class Mask: | |
mask: int | |
def __init__(self, mask): | |
if not isinstance(mask, int): | |
mask = Mask.parse_mask(mask) | |
self.mask = mask | |
def width(self): | |
for i in range(8): #fedcba9876543210 | |
if self.mask & (0x0101010101010101 << (7 - i)): | |
return 8 - i | |
return 0 | |
def height(self): | |
for i in range(8): | |
if self.mask & (0xff << (8 * (7 - i))): | |
return 8 - i | |
return 0 | |
def shift_right(self): | |
if self.width() >= 8: | |
raise ValueError | |
return Mask(self.mask << 1) | |
def shift_down(self): | |
if self.height() >= 8: | |
raise ValueError | |
return Mask(self.mask << 8) | |
def translations(self): | |
return [right for down in iterate(Mask.shift_down, self) | |
for right in iterate(Mask.shift_right, down)] | |
def rotate_clockwise(self): | |
"""123 741 | |
456 -> 852 | |
789 963""" | |
width, height = self.width(), self.height() | |
newmask = 0 | |
for j in range(height): | |
for i in range(width): | |
if self.mask & (1 << (j * 8 + i)): | |
j2, i2 = i, height - 1 - j | |
newmask |= 1 << (j2 * 8 + i2) | |
return Mask(newmask) | |
def rotations(self): | |
rotations = [self] | |
while True: | |
newmask = rotations[-1].rotate_clockwise() | |
if newmask == self: | |
break | |
rotations.append(newmask) | |
return rotations | |
def is_superset_of(self, other): | |
return (other.mask & ~self.mask) == 0 | |
def count(self): | |
return bin(self.mask).count('1') | |
@staticmethod | |
def parse_mask(s, x='x'): | |
mask = 0 | |
for i, line in enumerate(s.strip().split('\n')): | |
for j, c in enumerate(line.strip()): | |
if c == x: | |
mask += 1 << (i * 8 + j) | |
return mask | |
@dataclass | |
class Chip: | |
mask: Mask | |
d: int = 0 # 殺傷 (damage) | |
p: int = 0 # 破防 (penetration) | |
a: int = 0 # 精度 (accuracy) | |
r: int = 0 # 裝填 (reload/rate of fire) | |
t: int = 0 # 校準點券 (calibration tickets) | |
def with_mask(self, mask): | |
return Chip(mask, self.d, self.p, self.a, self.r, self.t) | |
def with_t(self, t): | |
return Chip(self.mask, self.d, self.p, self.a, self.r, t) | |
class ChipSolutionPrinter(cp_model.CpSolverSolutionCallback): | |
def __init__(self, area, chips): | |
cp_model.CpSolverSolutionCallback.__init__(self) | |
self.__area = area | |
self.__chips = chips | |
self.__solution_count = 0 | |
def __empty_result(self): | |
result = [] | |
for i in range(8): | |
for j in range(8): | |
if self.__area.mask & (1 << (i * 8 + j)): | |
result.append('.') | |
else: | |
result.append(' ') | |
result.append('\n') | |
return result | |
def on_solution_callback(self): | |
self.__solution_count += 1 | |
result = self.__empty_result() | |
print(f'Solution #{self.__solution_count}') | |
print(f'Label ID Dmg Pen Acc Rel') | |
idx = count = damage = penetration = accuracy = rof = tickets = 0 | |
for var, chip in self.__chips: | |
if not self.Value(var): | |
continue | |
label = chr(ord('A') + idx) | |
chipid = int(str(var)[1 : str(var).find('_')]) | |
rotatestr = '*' if chip.t else '' | |
print(f'{label} {chipid:6}{rotatestr:1} {chip.d:3} {chip.p:3} {chip.a:3} {chip.r:3}') | |
count += chip.mask.count() | |
damage += chip.d | |
penetration += chip.p | |
accuracy += chip.a | |
rof += chip.r | |
tickets += chip.t | |
for i in range(8): | |
for j in range(8): | |
if chip.mask.mask & (1 << (i * 8 + j)): | |
result[i * 9 + j] = label | |
idx += 1 | |
print(f'Total -- {damage:3} {penetration:3} {accuracy:3} {rof:3}') | |
if tickets: | |
print(f'Calibration Tickets: {tickets}') | |
print(''.join(result).rstrip()) | |
print() | |
def solution_count(self): | |
return self.__solution_count | |
def main(): | |
area = get_available_area() | |
minc, mind, minp, mina, minr, maxt = get_params(area) | |
primary_chips = get_chips() | |
model = cp_model.CpModel() | |
chips = [] # [(var, chip)] | |
for i, chip in enumerate(primary_chips): | |
variants = [] | |
for n_rotate, mask2 in enumerate(chip.mask.rotations()): | |
for j, mask3 in enumerate(mask2.translations()): | |
if not area.is_superset_of(mask3): | |
continue | |
chip3 = chip.with_mask(mask3) | |
if n_rotate: | |
chip3 = chip3.with_t(50) | |
var = model.NewBoolVar(f'x{i}_{n_rotate}_{j}') | |
chips.append((var, chip3)) | |
variants.append(var) | |
model.AddSumConstraint(variants, 0, 1) | |
for i in range(64): | |
if not area.mask & (1 << i): | |
continue | |
elemvars = [var for var, chip in chips if chip.mask.mask & (1 << i)] | |
model.AddSumConstraint(elemvars, 0, 1) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.mask.count() for var, chip in chips)) | |
>= minc) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.d for var, chip in chips)) >= mind) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.p for var, chip in chips)) >= minp) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.a for var, chip in chips)) >= mina) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.r for var, chip in chips)) >= minr) | |
model.Add(reduce(lambda x, y: x + y, | |
(var * chip.t for var, chip in chips)) <= maxt) | |
solver = cp_model.CpSolver() | |
solution_printer = ChipSolutionPrinter(area, chips) | |
status = solver.SearchForAllSolutions(model, solution_printer) | |
print(f'Status = {solver.StatusName(status)}') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment