Files
elements-autochess/scripts/core/BotAI.gd
2026-08-29 11:42:54 +02:00

156 lines
4.0 KiB
GDScript

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
func setup(rand: RandomNumberGenerator) -> void:
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]]
greed = rand.randf_range(0.3, 0.8)
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> void:
_do_upgrades(p, pool)
_do_buys(p, pool, rand, round_no)
_do_placements(p, pool, round_no)
_do_upgrades(p, pool)
var keep := int(lerpf(2.0, 18.0, greed))
if p.gold > keep + Data.REROLL_COST and rand.randf() < 0.6:
p.reroll(pool, rand)
_do_buys(p, pool, rand, round_no)
_do_placements(p, pool, round_no)
_do_upgrades(p, pool)
_free_bench(p, pool)
func _wants(entry: ShopEntry, p: PlayerState) -> bool:
if entry.element < 0:
# Neutral wird beim Kauf spezialisiert, also immer brauchbar.
return true
if pref.has(entry.element):
return true
# Elemente, die schon auf dem Board sind, sind immer Futter.
return p.trait_counts().has(entry.element)
func _do_buys(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> 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, pool: Pool, round_no: int) -> void:
while p.can_place(round_no):
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, round_no):
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 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 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()
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
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 counts := p.trait_counts()
for i in p.bench.size():
var a = p.bench[i]
if a == null:
continue
if counts.get(a.elements[0], 0) == 0 and not pref.has(a.elements[0]):
p.sell_bench(i, pool)
return
p.sell_bench(0, pool)