Jelajahi Sumber

update fall2022

olinox14 3 tahun lalu
induk
melakukan
f5ebdee635
1 mengubah file dengan 582 tambahan dan 70 penghapusan
  1. 582 70
      fall2022/fall2022.py

+ 582 - 70
fall2022/fall2022.py

@@ -6,6 +6,13 @@ debug = True
 
 t0 = time.time()
 
+# Debugger : il y a des mouvements qui ne se font pas, faut tout vérifier
+# Faire des zones à l'intérieur des contigs, pour attirer le mouvement vers les zones à coloniser
+# Limiter les constructions agressives en fonction de ces zones
+# penser les recycleurs comme des obstacles!
+# Identifier le moment où la situation est "verrouillée" (plus d'accès entre alliés et ennemis)
+#     et cesser les construction de recycleurs
+
 
 def log(*msg):
     if debug:
@@ -42,6 +49,17 @@ class Queue(BaseClass):
         return heapq.heappop(self.items)
 
 
+class PathNode(tuple):
+    def __new__(cls, x, y, parent=None):
+        n = tuple.__new__(cls, (x, y))
+        n.parent = parent
+        n.cost = 0
+        return n
+
+    def __repr__(self):
+        return f"<{self[0]}, {self[1]}, c:{self.cost}>"
+
+
 class Player(BaseClass):
     ME = 1
     OPPONENT = 0
@@ -50,7 +68,7 @@ class Player(BaseClass):
     def __init__(self, id_):
         self.id = id_
         self.matter = 0
-
+        self.territory = 0
 
 class Cell(BaseClass):
     def __init__(self, x, y):
@@ -78,9 +96,23 @@ class Cell(BaseClass):
     def is_opponents(self):
         return self.owner == Player.OPPONENT
 
+    @property
+    def is_neutral(self):
+        return self.owner == Player.NONE
+
+    @property
+    def is_grass(self):
+        return self.amount == 0
+
+    @property
     def is_movable(self):
-        return self.amount > 0 and not self.recycler
+        return not self.is_grass and not self.recycler
 
+    @property
+    def lifetime(self):
+        return 10000 if not self.in_range_of_recycler else self.amount
+
+    @property
     def unmovable_next_round(self):
         return self.amount == 1 and self.in_range_of_recycler
 
@@ -97,16 +129,30 @@ class Cell(BaseClass):
 class RobotGroup(BaseClass):
     COST = 10
 
-    def __init__(self, x, y, owner, amount):
+    def __init__(self, x, y, owner, amount, initial_amount=None):
         self.x = x
         self.y = y
         self.owner = owner
         self.amount = amount
+        self.initial_amount = initial_amount if initial_amount is not None else amount
+
+        self.amount_played = 0
 
     @property
     def pos(self):
         return self.x, self.y
 
+    @pos.setter
+    def pos(self, pos):
+        self.x, self.y = pos
+
+    @property
+    def owned(self):
+        return self.owner == Player.ME
+
+    @property
+    def has_played(self):
+        return self.amount_played >= self.initial_amount
 
 class Recycler(BaseClass):
     COST = 10
@@ -133,6 +179,43 @@ class Recycler(BaseClass):
                f"{self.immediately_available_amount}, {self.total_available_amount}, {self.lifetime()}>"
 
 
+class Contig(BaseClass):
+    UNKNOWN = 'U'
+    OWNED = 'O'
+    PARTIALLY_OWNED = 'P'
+    CONFLICTUAL = 'C'
+    NOT_OWNED = 'N'
+
+    def __init__(self, start):
+        self.start = start
+        self.area = [start]
+        self.status = Contig.UNKNOWN
+        self.has_robots = False
+
+    def __repr__(self):
+        return f"<Contig(start: {self.start}, size: {len(self.area)}, status: {self.status}, robots: {self.has_robots})>"
+
+
+class MoveOrder(BaseClass):
+    def __init__(self, pos, amount, priority):
+        self.pos = pos
+        self.amount = amount
+        self.priority = priority
+
+        self.affected = 0
+
+    @property
+    def fulfilled(self):
+        return self.affected >= self.amount
+
+
+class MoveOrderCandidate(BaseClass):
+    def __init__(self, order, unit, destination):
+        self.order = order
+        self.unit = unit
+        self.destination = destination
+
+
 class Action(BaseClass):
     pass
 
@@ -144,10 +227,11 @@ class Wait(Action):
 
 
 class Move(Action):
-    def __init__(self, from_, to, amount):
+    def __init__(self, from_, to, amount, priority):
         self.from_ = from_
         self.to = to
         self.amount = amount
+        self.priority = priority
 
     def do(self):
         x0, y0 = self.from_
@@ -157,9 +241,13 @@ class Move(Action):
 
 class Build(Action):
     COST = 10
+    SUPPORT = 'S'
+    AGGRESSIVE = 'A'
 
-    def __init__(self, pos):
+    def __init__(self, pos, destination, priority):
         self.pos = pos
+        self.priority = priority
+        self.destination = destination
 
     def do(self):
         x, y = self.pos
@@ -169,9 +257,10 @@ class Build(Action):
 class Spawn(Action):
     COST = 10
 
-    def __init__(self, pos, amount):
+    def __init__(self, pos, amount, priority):
         self.pos = pos
         self.amount = amount
+        self.priority = priority
 
     def do(self):
         x, y = self.pos
@@ -191,6 +280,12 @@ class Grid(BaseClass):
 
         self.units = {}
         self.recyclers = {}
+        self.contigs = []
+        self._neighbors_cache = {}
+        self._distance_cache = {}
+        self.index_contigs = {}
+        self.index_tensions = {}
+        self.index_nearest_enemy = {}
 
     @property
     def grid(self):
@@ -201,18 +296,32 @@ class Grid(BaseClass):
             key = lambda x: x
         log("\n" + "\n".join(["".join([f"{key(c)}|" for c in row]) for row in self.grid]))
 
-    @staticmethod
-    def manhattan(from_, to_):
+    def manhattan(self, from_, to_):
+        if (from_, to_) in self._distance_cache:
+            return self._distance_cache[(from_, to_)]
+
         xa, ya = from_
         xb, yb = to_
-        return abs(xa - xb) + abs(ya - yb)
-
-    def neighbors(self, x, y):
-        return [
+        dist = abs(xa - xb) + abs(ya - yb)
+        self._distance_cache[(from_, to_)] = dist
+        self._distance_cache[(to_, from_)] = dist
+        return dist
+
+    def neighbors(self, x, y, diags=False):
+        # if (x, y, diags) in self._neighbors_cache:
+        #     return self._neighbors_cache[(x, y, diags)]
+
+        n = [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+        if diags:
+            n += [(x - 1, y - 1), (x + 1, y - 1), (x - 1, y + 1), (x + 1, y + 1)]
+        
+        neighbors = [
             (x, y)
-            for x, y in [(x, y - 1), (x - 1, y), (x + 1, y), (x, y + 1)]
+            for x, y in n
             if 0 <= x < self.width and 0 <= y < self.height
         ]
+        # self._neighbors_cache[(x, y, diags)] = neighbors
+        return neighbors
 
     @staticmethod
     def create():
@@ -234,6 +343,8 @@ class Grid(BaseClass):
                 self.cells[(x, y)].update(scrap_amount, owner, units, recycler, can_build, can_spawn,
                                           in_range_of_recycler)
 
+        log('update')
+
         # update robots
         self.units = {}
         for cell in self.cells.values():
@@ -249,6 +360,11 @@ class Grid(BaseClass):
                 seen |= set(self.neighbors(*cell.pos))
                 self.recyclers[cell.pos] = Recycler(cell.x, cell.y, cell.owner, area)
 
+        self.update_possessions()
+        self.update_contigs()
+        self.update_tension_map()
+        self.update_nearest_enemy()
+
     def owned_units(self):
         return [r for r in self.units.values() if r.owner == Player.ME]
 
@@ -261,77 +377,471 @@ class Grid(BaseClass):
     def opponent_recyclers(self):
         return [r for r in self.recyclers.values() if r.owner == Player.OPPONENT]
 
-    def act(self):
+    def tension(self, cell):
+        return self.index_tensions.get(cell.pos, 0)
+
+    def current_winner(self):
+        if self.me.territory > self.opponent.territory:
+            return Player.ME
+        elif self.me.territory < self.opponent.territory:
+            return Player.OPPONENT
+        else:
+            return Player.NONE
+
+    def update_possessions(self):
+        self.me.territory = 0
+        self.opponent.territory = 0
+
+        for c in self.cells.values():
+            if c.owned:
+                self.me.territory += 1
+            elif c.is_opponents:
+                self.opponent.territory += 1
+
+    def update_contigs(self):
+        """ contigs are isolated blocks of cells """
+        self.contigs = []
+        self.index_contigs = {}
+
+        seen = []
+        # build contigs
+        for c in self.cells.values():
+            if c.pos in seen or c.is_grass:
+                continue
+
+            contig = Contig(c.pos)
+            candidates = self.neighbors(*c.pos)
+
+            while candidates:
+
+                candidate = candidates.pop()
+                seen.append(candidate)
+
+                if self.cells[candidate].is_grass or self.cells[candidate].recycler or candidate in contig.area:
+                    continue
+
+                for n in self.neighbors(*candidate):
+                    if n not in contig.area:
+                        candidates.append(n)
+
+                contig.area.append(candidate)
+
+            self.contigs.append(contig)
+
+        self.index_contigs = {p: None for p in self.cells}
+
+        for contig in self.contigs:
+            owners = set()
+
+            # update index
+            for p in contig.area:
+                self.index_contigs[p] = contig
+                owners.add(self.cells[p].owner)
+
+                if self.cells[p].owned and self.cells[p].units:
+                    contig.has_robots = True
+
+            # status
+            if Player.ME in owners:
+                if Player.OPPONENT in owners:
+                    contig.status = Contig.CONFLICTUAL
+                elif Player.NONE in owners:
+                    contig.status = Contig.PARTIALLY_OWNED
+                else:
+                    contig.status = Contig.OWNED
+            else:
+                contig.status = Contig.NOT_OWNED
+
+    def update_tension_map(self):
+        self.index_tensions = {}
+
+        for c in self.cells.values():
+
+            if not c.units:
+                continue
+
+            k = 1 if c.is_opponents else -1
+            tension = k * c.units
+            self.index_tensions[c.pos] = self.index_tensions.get(c.pos, 0) + tension
+
+    def update_nearest_enemy(self):
+        self.index_nearest_enemy = {pos: 0 for pos in self.cells}
+
+        for contig in self.contigs:
+            for pos in contig.area:
+                c = self.cells[pos]
+                if not c.owned or not any(self.cells[n].owned for n in self.neighbors(*pos)):
+                    continue
+
+                nearest = None
+
+                for other_pos in contig.area:
+                    other = self.cells[other_pos]
+                    if not other.is_opponents:
+                        continue
+
+                    dist = self.manhattan(c.pos, other.pos)
+
+                    if not nearest or dist < nearest[1]:
+                        nearest = (other.pos, dist)
+
+                if nearest:
+                    self.index_nearest_enemy[c.pos] = nearest[1]
+
+    def path(self, start, target):
+        nodes = Queue()
+        its, break_on = 0, 500
+
+        origin = PathNode(*start)
+        nodes.put(0, origin)
+
+        while nodes:
+            current = nodes.get()
+
+            if current == target:
+                path = []
+                previous = current
+                while previous:
+                    if previous != start:
+                        path.insert(0, previous)
+                    previous = previous.parent
+                return path
+
+            neighbors = self.neighbors(*current)
+
+            for x, y in neighbors:
+                its += 1
+                if its > break_on:
+                    log(f"<!> pathfinding broken from {start} to {target}")
+                    return None
+
+                if (x, y) == current.parent:
+                    continue
+
+                cell = self.cells[(x, y)]
+
+                if not cell.is_movable:
+                    continue
+
+                if cell.lifetime <= (current.cost + 1):
+                    continue
+
+                cost = current.cost + 1
+                priority = cost + self.manhattan((x, y), target)
+
+                node = PathNode(x, y, current)
+                node.cost = cost
+                nodes.put(priority, node)
+
+        return None
+
+    def get_next_build_action(self):
 
         build_actions = Queue()
 
         # List available build actions
-        places = [c for c in self.cells.values() if c.owned and c not in self.units]
-        k0 = 100
-        k_available_amount = -1
-        k_already_exploited = 30
+        k0 = 80000
+        kmax_build = 70000
+        k_available_amount = -1000
+        k_already_exploited = 30000
+        k_area_unit = 500
+        k_area_owned = 1000
+        k_area_neutral = 0
+        k_area_opponents = -4000
+        k_area_opponent_unit = -1000
+        k_recyclers_owned = 5000
+        k_space_around = -1000
+        k_aggressive_build = 8000
+        k_aggressive_build_for_defense = -6000
+        k_aggressive_build_tension = -1000
+
+        k0_recyclers_owned = k_recyclers_owned * len(self.owned_recyclers())
+
+        for contig in self.contigs:
+            if contig.status in (Contig.OWNED, Contig.PARTIALLY_OWNED, Contig.NOT_OWNED):
+                # nothing to do
+                continue
 
-        for place in places:
-            action = Build(place.pos)
-            k = k0
+            for p in contig.area:
+                place = self.cells[p]
+                if not place.owned or place.units or place.recycler:
+                    continue
 
-            area = [place.pos] + self.neighbors(*place.pos)
-            for pos in area:
-                k += k_available_amount * self.cells[pos].amount
-                k += k_already_exploited * self.cells[pos].in_range_of_recycler
+                k = k0
+
+                area = [place.pos] + self.neighbors(*place.pos)
+                destination = Build.SUPPORT if not any(self.cells[p].is_opponents for p in area) else Build.AGGRESSIVE
+                k0_already_exploited = 0
+
+                for pos in area:
+                    k += k_available_amount * self.cells[pos].amount
+                    k0_already_exploited += k_already_exploited * self.cells[pos].in_range_of_recycler
 
-            build_actions.put(k, action)
+                    if self.cells[pos].owned:
+                        k += k_area_owned
+                        k += k_area_unit * self.cells[pos].units
+                    elif self.cells[pos].is_opponents:
+                        k += k_area_opponents
+                        k += k_area_opponent_unit * self.cells[pos].units
+                    else:
+                        k += k_area_neutral
+
+                if destination == Build.SUPPORT:
+                    neighborhood = {n for p in area for n in self.neighbors(*p) if n not in area}
+
+                    for n in neighborhood:
+                        if self.cells[n].amount > 1:
+                            k += k_space_around
+
+                    k += k0_recyclers_owned
+                    k += k0_already_exploited
+
+                else:
+                    k += k_aggressive_build
+                    if self.current_winner() == Player.ME:
+                        k += k_aggressive_build_for_defense
+
+                    for n in self.neighbors(p[0], p[1], True):
+                        k += k_aggressive_build_tension * self.index_tensions.get(n, 0)
+
+                if k > kmax_build:
+                    continue
+
+                action = Build(place.pos, destination, k)
+                build_actions.put(k, action)
 
         # List available spawn actions
-        places = [c for c in self.cells.values() if c.owned]
-        k0 = 60
-        for place in places:
-            action = Spawn(place.pos, 1)
-            k = k0
-            build_actions.put(k, action)
-
-        # List available move actions
-        move_actions_per_unit = {}
-        # targets = Queue()
-        k0 = 100
-        k_blitzkrieg = -2
-        k_occupy = -5
-        k_destroy = -2
-
-        for u in self.owned_units():
-            move_actions_per_unit[u.pos] = Queue()
-
-            nearest_enemy_cell = min(
-                (c for c in self.cells.values() if c.is_opponents),
-                key=lambda c: self.manhattan(c.pos, u.pos)
-            )
-            nearest_enemy_cell_dist = self.manhattan(nearest_enemy_cell.pos, u.pos)
-
-            for pos in self.neighbors(*u.pos):
-                place = self.cells[pos]
-                action = Move(u.pos, place.pos, u.amount)
-
-                if not place.owned and place.is_movable():
-                    k = k0
-                    k += k_blitzkrieg * (nearest_enemy_cell_dist - self.manhattan(place.pos, nearest_enemy_cell.pos))
-                    if place.is_opponents:
-                        k += k_occupy
-                        if place.pos in self.units:
-                            k += k_destroy * self.units[place.pos].amount
-                    move_actions_per_unit[u.pos].put(k, action)
+        k0 = 54000
+        kmax_spawn = 70000
+        k_opportunities = -1000
+        k_conquest = -3000
+        k_tension = -1000
+        k_dist_to_enemy = 400
+        k_bridgehead = -6000
+        k_reinforcement = 500
+
+        amount_default = 1
+        amount_bridgehead = 3
+        ki = 0  # little hack to avoid the while in queue.put
+
+        for contig in self.contigs:
+            if contig.status == Contig.OWNED:
+                # nothing to do
+                continue
+
+            if contig.status == Contig.PARTIALLY_OWNED and contig.has_robots:
+                # nothing to do
+                continue
+
+            for p in contig.area:
+                place = self.cells[p]
+                amount = amount_default
+
+                if not place.owned or not place.is_movable or place.unmovable_next_round:
+                    continue
+
+                k = k0 + ki
+                ki += 1
+
+                tension = self.index_tensions.get(place.pos, 0)
+                for pos in self.neighbors(place.pos[0], place.pos[1], True):
+                    tension += self.index_tensions.get(pos, 0)
+                k += k_tension * tension
+
+                for pos in self.neighbors(*place.pos):
+                    n = self.cells[pos]
+                    if not n.is_movable:
+                        continue
+
+                    if n.is_neutral:
+                        k += k_opportunities
+                    elif n.is_opponents:
+                        k += k_conquest
+
+                k += k_dist_to_enemy * self.index_nearest_enemy[p]
+
+                if sum(self.cells[n].is_opponents for n in self.neighbors(p[0], p[1], True)) > 5:
+                    k += k_bridgehead
+                    amount = amount_bridgehead
+
+                amount = max(sum([self.index_tensions.get(n, 0) for n in self.neighbors(*p)]), 1)
+
+                if k > kmax_spawn:
+                    continue
+
+                for _ in range(amount):
+                    action = Spawn(place.pos, 1, k)
+                    build_actions.put(k, action)
+                    k += k_reinforcement
+
+        # for action in build_actions.items[:8]:
+        #     log(action)
+
+        if not build_actions:
+            return None
+
+        action = build_actions.get()
+
+        # update cells to take this order into account
+        place = self.cells[action.pos]
+
+        if type(action) is Build:
+            place.recycler = True
+            area = [action.pos] + self.neighbors(*action.pos)
+            self.recyclers[action.pos] = Recycler(action.pos[0], action.pos[1], Player.ME, area)
+            for pos in area:
+                self.cells[pos].in_range_of_recycler = True
+
+        if type(action) is Spawn:
+            place.units += action.amount
 
+            if action.pos in self.units:
+                self.units[action.pos].amount += action.amount
+            else:
+                self.units[action.pos] = RobotGroup(action.pos[0], action.pos[1], Player.ME, action.amount, 0)
+
+            if action.pos in self.index_tensions:
+                self.index_tensions[action.pos] -= action.amount
+            else:
+                self.index_tensions[action.pos] = -1 * action.amount
+
+        return action
+
+    def build_move_actions(self):
+        # List possible destinations per contig
+        move_actions = []
+
+        k0_position = 60000
+        k_position_distance = 2000
+        k_position_opponents = -5000
+        k_position_destroy = -3000
+        k_dist_to_enemy = 1000
+        k_expand = -800
+
+        for contig in self.contigs:
+            orders = []
+            units = []
+
+            if contig.status in (Contig.NOT_OWNED, Contig.OWNED):
+                # nothing to do
+                continue
+
+            # list destinations
+            for pos in contig.area:
+                c = self.cells[pos]
+                priority = k0_position
+
+                if c.owned or not c.is_movable:
+                    continue
+
+                if not any((self.cells[n].owned and not self.cells[n].recycler) for n in self.neighbors(*pos)):
+                    # on ne conserve que les cases voisines du territoire allié
+                    continue
+
+                amount_needed = 1
+
+                if c.is_opponents:
+                    priority += k_position_opponents
+                    if c.units:
+                        priority += k_position_destroy
+                        amount_needed = c.amount
+
+                priority += k_dist_to_enemy * self.index_nearest_enemy[pos]
+
+                for n in self.neighbors(pos[0], pos[1], True):
+                    if not self.cells[n].is_grass and not self.cells[n].owned:
+                        priority += k_expand
+
+                order = MoveOrder(pos, amount_needed, priority)
+
+                orders.append(order)
+
+            # for move_order in orders:
+            #     log(move_order)
+
+            # Prioritize units per distance
+            for pos in contig.area:
+                if pos in self.units:
+                    unit = self.units[pos]
+                    if not unit.owned or not unit.initial_amount:
+                        continue
+                    units.append(unit)
+
+            q = Queue()
+            for unit in units:
+                for order in orders:
+                    destination = order.pos
+                    dist = self.manhattan(unit.pos, destination)
+
+                    priority = order.priority
+                    priority += k_position_distance * dist
+
+                    candidate = MoveOrderCandidate(order, unit, destination)
+                    q.put(priority, candidate)
+
+            # affect orders
+            while q:
+                k, candidate = q.get_items()
+                if candidate.unit.has_played:
+                    continue
+                if candidate.order.fulfilled:
+                    continue
+
+                # attention, on prend le montant initial car les unités spawned ce tour ne peuvent pas bouger
+                amount = min(candidate.order.amount, candidate.unit.initial_amount)
+
+                from_, to_ = candidate.unit.pos, candidate.destination
+
+                action = Move(from_, to_, amount, k)
+
+                candidate.unit.amount_played += amount
+                candidate.order.affected += amount
+
+                # update situation
+                # next_pos = None
+                # if to_ in self.neighbors(*from_):
+                #     next_pos = to_
+                # # else:
+                # #     path = self.path(from_, to_)
+                # #     if path:
+                # #         next_pos = path[0]
+                #
+                # self.cells[from_].units -= amount
+                # self.index_tensions[from_] += amount
+                # if next_pos:
+                #     self.cells[next_pos].units -= amount
+                #     candidate.unit.pos = next_pos
+                #     self.index_tensions[next_pos] = self.index_tensions.get(next_pos, 0) - amount
+
+                move_actions.append(action)
+
+        return move_actions
+
+    def act(self):
+        # Resolve
         actions = []
         expanse = 0
-        while build_actions and expanse <= self.me.matter:
-            action = build_actions.get()
+        log('compute build actions')
+        while expanse < self.me.matter:
+            action = self.get_next_build_action()
+            if action is None:
+                break
+
+            if (expanse + action.COST) > self.me.matter:
+                break
+
             actions.append(action)
             expanse += action.COST
 
-        for move_actions in move_actions_per_unit.values():
-            if not move_actions:
-                continue
-            action = move_actions.get()
-            actions.append(action)
+        log('computes move actions')
+        actions += self.build_move_actions()
+
+        if not actions:
+            actions.append(Wait())
+
+        log('resolve')
+        for action in actions:
+            log(action)
 
         print(";".join([a.do() for a in actions]))
 
@@ -341,6 +851,8 @@ grid = Grid.create()
 while True:
     grid.update()
 
-    # grid.print(lambda x: f"{x.amount:2d}")
+    # for contig in grid.contigs:
+    #     log(contig)
+    # grid.print(lambda x: f"{grid.tension(x):02d}")
 
     grid.act()