feat: add L1 Basic AI and L2 Rule AI with heuristic scoring
This commit is contained in:
11
src/ai/base_ai.gd
Normal file
11
src/ai/base_ai.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
# src/ai/base_ai.gd
|
||||
class_name BaseAI
|
||||
extends RefCounted
|
||||
|
||||
var ai_name: String = "AI"
|
||||
|
||||
func decide(hand: Array, table: Array, current_rank: int, config: RuleConfig) -> HandEvaluator.EvaluatedPlay:
|
||||
var pass := HandEvaluator.EvaluatedPlay.new()
|
||||
pass.type = -1
|
||||
pass.primary_rank = 0
|
||||
return pass
|
||||
53
src/ai/l1_basic_ai.gd
Normal file
53
src/ai/l1_basic_ai.gd
Normal file
@@ -0,0 +1,53 @@
|
||||
# src/ai/l1_basic_ai.gd
|
||||
class_name L1BasicAI
|
||||
extends BaseAI
|
||||
|
||||
const _C = preload("res://src/core/constants.gd")
|
||||
|
||||
func _init():
|
||||
ai_name = "Basic AI"
|
||||
|
||||
func decide(hand: Array, table: Array, current_rank: int, config: RuleConfig) -> HandEvaluator.EvaluatedPlay:
|
||||
var moves := MoveGenerator.generate(hand, current_rank, config)
|
||||
var last_play := _last_non_pass(table)
|
||||
if last_play == null:
|
||||
return _smallest_play(moves)
|
||||
var candidates: Array = []
|
||||
for m in moves:
|
||||
if m.type == -1:
|
||||
continue
|
||||
if m.type == last_play.type and m.primary_rank > last_play.primary_rank:
|
||||
candidates.append(m)
|
||||
elif (m.type == _C.TYPE_BOMB or m.type == _C.TYPE_ROCKET) and _beats(m, last_play, config):
|
||||
candidates.append(m)
|
||||
if candidates.is_empty():
|
||||
return _pass_move()
|
||||
return _smallest_play(candidates)
|
||||
|
||||
func _smallest_play(moves: Array) -> HandEvaluator.EvaluatedPlay:
|
||||
var best: HandEvaluator.EvaluatedPlay = null
|
||||
for m in moves:
|
||||
if m.type <= 0:
|
||||
continue
|
||||
if best == null or m.cards.size() < best.cards.size():
|
||||
best = m
|
||||
elif m.cards.size() == best.cards.size() and m.primary_rank < best.primary_rank:
|
||||
best = m
|
||||
if best == null:
|
||||
return _pass_move()
|
||||
return best
|
||||
|
||||
func _last_non_pass(table: Array) -> HandEvaluator.EvaluatedPlay:
|
||||
for i in range(table.size() - 1, -1, -1):
|
||||
if table[i].type != -1:
|
||||
return table[i]
|
||||
return null
|
||||
|
||||
func _beats(a: HandEvaluator.EvaluatedPlay, b: HandEvaluator.EvaluatedPlay, config: RuleConfig) -> bool:
|
||||
return RuleEngine.compare(a, b, config) > 0
|
||||
|
||||
func _pass_move() -> HandEvaluator.EvaluatedPlay:
|
||||
var p := HandEvaluator.EvaluatedPlay.new()
|
||||
p.type = -1
|
||||
p.primary_rank = 0
|
||||
return p
|
||||
70
src/ai/l2_rule_ai.gd
Normal file
70
src/ai/l2_rule_ai.gd
Normal file
@@ -0,0 +1,70 @@
|
||||
# src/ai/l2_rule_ai.gd
|
||||
class_name L2RuleAI
|
||||
extends BaseAI
|
||||
|
||||
const _C = preload("res://src/core/constants.gd")
|
||||
|
||||
func _init():
|
||||
ai_name = "Rule AI"
|
||||
|
||||
func decide(hand: Array, table: Array, current_rank: int, config: RuleConfig) -> HandEvaluator.EvaluatedPlay:
|
||||
var moves := MoveGenerator.generate(hand, current_rank, config)
|
||||
var last_play := _last_non_pass(table)
|
||||
var candidates: Array = []
|
||||
for m in moves:
|
||||
if m.type == -1:
|
||||
continue
|
||||
if last_play == null:
|
||||
candidates.append(m)
|
||||
elif _can_beat(m, last_play, config):
|
||||
candidates.append(m)
|
||||
if candidates.is_empty():
|
||||
return _pass_move()
|
||||
var scored := _score_all(candidates, hand.size(), current_rank)
|
||||
scored.sort_custom(func(a, b): return a["score"] > b["score"])
|
||||
return scored[0]["move"]
|
||||
|
||||
func _score_all(moves: Array, hand_size: int, current_rank: int) -> Array:
|
||||
var results := []
|
||||
for m in moves:
|
||||
var score := _score_move(m, hand_size, current_rank)
|
||||
results.append({"move": m, "score": score})
|
||||
return results
|
||||
|
||||
func _score_move(play: HandEvaluator.EvaluatedPlay, hand_size: int, current_rank: int) -> float:
|
||||
var score := 0.0
|
||||
var remaining := hand_size - play.cards.size()
|
||||
score += (27.0 - remaining) / 27.0 * 0.3
|
||||
if play.type == _C.TYPE_BOMB or play.type == _C.TYPE_ROCKET:
|
||||
score -= 0.2
|
||||
elif play.type == _C.TYPE_STRAIGHT_FLUSH:
|
||||
score -= 0.1
|
||||
score += play.cards.size() * 0.02
|
||||
if play.primary_rank >= 14 and remaining > 10:
|
||||
score -= 0.1
|
||||
if remaining <= 3:
|
||||
score += 0.5
|
||||
if play.is_pure_bomb:
|
||||
score += 0.05
|
||||
return score
|
||||
|
||||
func _can_beat(play: HandEvaluator.EvaluatedPlay, last: HandEvaluator.EvaluatedPlay, config: RuleConfig) -> bool:
|
||||
if play.type == _C.TYPE_ROCKET:
|
||||
return true
|
||||
if play.type == _C.TYPE_BOMB and last.type != _C.TYPE_ROCKET:
|
||||
return RuleEngine.compare_bombs(play, last, config) > 0
|
||||
if play.type == last.type:
|
||||
return play.primary_rank > last.primary_rank
|
||||
return false
|
||||
|
||||
func _last_non_pass(table: Array) -> HandEvaluator.EvaluatedPlay:
|
||||
for i in range(table.size() - 1, -1, -1):
|
||||
if table[i].type != -1:
|
||||
return table[i]
|
||||
return null
|
||||
|
||||
func _pass_move() -> HandEvaluator.EvaluatedPlay:
|
||||
var p := HandEvaluator.EvaluatedPlay.new()
|
||||
p.type = -1
|
||||
p.primary_rank = 0
|
||||
return p
|
||||
Reference in New Issue
Block a user