class_name BotAI extends RefCounted ## Einfache Bot-Strategie: bevorzugte Elemente sammeln, Board füllen, aufwerten. var pref: Array[int] = [] var greed: float = 0.5 ## wie stark der Bot Gold hortet var difficulty: int = Data.Difficulty.NORMAL var opponent: PlayerState = null ## bekannter Gegner der Runde, für Konterwahl func setup(rand: RandomNumberGenerator, p_difficulty: int = Data.Difficulty.NORMAL) -> void: difficulty = p_difficulty var all: Array[int] = [] for e in Data.ELEMENT_COUNT: all.append(e) # Bewusst mit dem Spiel-RNG mischen, nicht mit Array.shuffle(), # damit ein Seed dieselbe Partie ergibt. for i in range(all.size() - 1, 0, -1): var j := rand.randi_range(0, i) var tmp := all[i] all[i] = all[j] all[j] = tmp pref = [all[0], all[1], all[2]] var g: Array = Data.BOT_GREED[difficulty] greed = rand.randf_range(g[0], g[1]) func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, opp: PlayerState = null) -> void: opponent = opp _do_upgrades(p, pool) _do_buys(p, pool) _do_placements(p) _do_upgrades(p, pool) var keep := int(lerpf(2.0, 18.0, greed)) # Wer kurz vor einem Upgrade steht, würfelt gezielt nach Futter. # Leichte Bots jagen nicht, schwere immer. var hunting := difficulty != Data.Difficulty.LEICHT and _closest_upgrade_gap(p) <= 3 var rolls := 3 if hunting else 1 if difficulty == Data.Difficulty.SCHWER: rolls += 1 for i in rolls: if p.gold > keep + Data.REROLL_COST and (hunting or rand.randf() < 0.6): p.reroll(pool, rand) _do_buys(p, pool) _do_placements(p) _do_upgrades(p, pool) # Level kaufen, wenn das Brett voll ist und Nachwuchs auf der Bank wartet. while p.gold > keep + Data.XP_BUY_COST and not p.can_place() \ and _pick_bench_for_board(p) >= 0 and p.buy_xp(): _do_placements(p) _free_bench(p, pool) ## Stellt das Brett gegen den bekannten Gegner um: Tanks vor seine Schützen, ## eigene Schützen weg von seinen Tanks. Ohne Gegner gilt die Standardaufstellung. func arrange(p: PlayerState, opp: PlayerState) -> void: if p.board.is_empty(): return var units: Array = p.board_units() p.board.clear() var threat := [] # Spalten (gespiegelt), in denen gegnerische Schützen stehen var enemy_tanks := [] # Spalten (gespiegelt) gegnerischer Tanks if opp != null: for hex in opp.board: var col: int = Data.BOARD_COLS - 1 - hex.x if opp.board[hex].role == Data.Role.TANK: enemy_tanks.append(col) else: threat.append(col) var center := [3, 2, 4, 1, 5, 0, 6] var tank_cols := threat.duplicate() for c in center: if not tank_cols.has(c): tank_cols.append(c) # Schützen möglichst weit weg von gegnerischen Tanks. var far_cols := center.duplicate() far_cols.sort_custom(func(a, b): return _min_dist(a, enemy_tanks) > _min_dist(b, enemy_tanks)) units.sort_custom(func(a, b): return a.role == Data.Role.TANK and b.role != Data.Role.TANK) for u in units: # Heiler direkt hinter die Tanks, sonst reicht ihre Heilung (2 Felder) nicht. var rows: Array match u.role: Data.Role.TANK: rows = [0, 1, 2, 3] Data.Role.HEILER: rows = [1, 2, 0, 3] _: rows = [3, 2, 1, 0] var cols: Array = tank_cols if u.role != Data.Role.DPS else far_cols var placed := false for r in rows: for c in cols: var h := Vector2i(c, r) if not p.board.has(h): p.board[h] = u placed = true break if placed: break func _min_dist(col: int, cols: Array) -> int: if cols.is_empty(): return 0 var best := 99 for c in cols: best = mini(best, absi(col - c)) return best ## Kleinste Zahl fehlender Futter-Avatare über alle Board-Einheiten. func _closest_upgrade_gap(p: PlayerState) -> int: var best := 99 for hex in p.board: var miss := p.upgrade_missing(hex) if p.board[hex].upgrade_cost().is_empty(): continue var total := 0 for e in miss: total += miss[e] best = mini(best, total) return best ## Elemente, die irgendeine Board-Einheit für ihr nächstes Upgrade braucht. func _needed_elements(p: PlayerState) -> Dictionary: var need := {} for hex in p.board: for e in p.board[hex].upgrade_cost(): need[e] = true return need func _wants(entry: ShopEntry, p: PlayerState) -> bool: if entry.element < 0: # Neutral wird beim Kauf spezialisiert, also immer brauchbar. return true if _needed_elements(p).has(entry.element): return true return pref.has(entry.element) func _do_buys(p: PlayerState, pool: Pool) -> void: for i in p.shop.size(): var e: ShopEntry = p.shop[i] if e.sold: continue if p.free_bench_slot() < 0: return if not _wants(e, p): continue if p.gold - e.price() < 0: continue p.buy(i, pool, _pick_element(p) if e.element < 0 else e.element) func _do_placements(p: PlayerState) -> void: while p.can_place(): var idx := _pick_bench_for_board(p) if idx < 0: return var a: Avatar = p.bench[idx] var hex := _pick_hex(p, a.role) if hex.x < 0: return if not p.place(idx, hex): return ## Zuerst Elemente aufstellen, die noch gar nicht im Team sind. ## Alles Weitere bleibt als Futter auf der Bank. func _pick_bench_for_board(p: PlayerState) -> int: var counts := p.trait_counts() var need := _needed_elements(p) var best := -1 var best_score := -1 for i in p.bench.size(): var a = p.bench[i] if a == null: continue var score := 0 if need.has(a.elements[0]) and counts.get(a.elements[0], 0) > 0: score = 0 # Futter für ein laufendes Upgrade bleibt auf der Bank elif pref.has(a.elements[0]): score = 3 if counts.get(a.elements[0], 0) == 0 else 1 else: score = 2 if counts.get(a.elements[0], 0) == 0 else 0 if score > best_score: best_score = score best = i return best if best_score > 0 else -1 func _pick_element(p: PlayerState) -> int: var counts := p.trait_counts() # Konter: schlägt eines meiner Lieblingselemente das häufigste Element des Gegners, # nehme ich das. Schwere Bots kontern auch außerhalb ihrer Vorlieben. var dominant := _dominant_element(opponent) if dominant >= 0: var counter := Data.beaten_by(dominant) if pref.has(counter) or difficulty == Data.Difficulty.SCHWER: return counter var best := pref[0] var best_c := -1 for e in pref: var c: int = counts.get(e, 0) if c > best_c and c < 6: best_c = c best = e return best ## Häufigstes Hauptelement auf dem Brett eines Spielers, -1 wenn leer. func _dominant_element(p: PlayerState) -> int: if p == null or p.board.is_empty(): return -1 var counts := {} for u in p.board_units(): var e: int = u.primary_element() counts[e] = counts.get(e, 0) + 1 var best := -1 var best_n := 1 # ab zwei Einheiten lohnt das Kontern for e in counts: if counts[e] > best_n: best_n = counts[e] best = e return best func _pick_hex(p: PlayerState, role: int) -> Vector2i: var rows: Array match role: Data.Role.TANK: rows = [0, 1, 2, 3] Data.Role.DPS: rows = [2, 1, 3, 0] _: rows = [3, 2, 1, 0] var cols := [3, 2, 4, 1, 5, 0, 6] for r in rows: for c in cols: var h := Vector2i(c, r) if not p.board.has(h): return h return Vector2i(-1, -1) func _do_upgrades(p: PlayerState, pool: Pool) -> void: var again := true while again: again = false for hex in p.board.keys(): if p.can_upgrade(hex): var options := p.available_new_elements(hex) var choice := -1 for e in pref: if options.has(e): choice = e break if choice < 0 and not options.is_empty(): choice = options[0] if choice >= 0 and p.upgrade(hex, choice, pool): again = true break ## Bank leeren, wenn sie voll ist und nichts mehr gebraucht wird. func _free_bench(p: PlayerState, pool: Pool) -> void: if p.free_bench_slot() >= 0: return var need := _needed_elements(p) # Erst verkaufen, was weder Futter noch Lieblingselement ist. for i in p.bench.size(): var a = p.bench[i] if a != null and not need.has(a.elements[0]) and not pref.has(a.elements[0]): p.sell_bench(i, pool) return for i in p.bench.size(): var a = p.bench[i] if a != null and not need.has(a.elements[0]): p.sell_bench(i, pool) return p.sell_bench(0, pool)