Create UrHeuristic agent

bram/ur-heuristic
Bram van den Heuvel 2026-07-05 08:29:30 +02:00
parent 5453a8acc3
commit a98e2c3c1e
1 changed files with 121 additions and 0 deletions

121
agents/ur_heuristic.py Normal file
View File

@ -0,0 +1,121 @@
"""
This module contains a heuristic in the Royal Game of Ur.
"""
from __future__ import annotations
import random
from .agent import Agent, Payload
BAD_OMEN = {
-1 : 4 / 16,
-2 : 6 / 16,
-3 : 4 / 16,
-4 : 1 / 16,
}
class UrHeuristic(Agent):
"""
Describe here what your agent does and how it behaves! This will help
others understand how your agent works.
"""
def __init__(self) -> None:
"""
Create a custom instance of your agent. This function allows you
to define which games your agent can play.
You may add parameters to the function if your agent requires more
information to be able to operate.
"""
super().__init__(
name="Ur heuristic agent",
author="Bram",
version="1.0.0",
profile={
"me.noordstar.peanuts.is_ai": False,
},
)
# Indicate that you're willing to play tic-tac-toe
# Remove this if you don't want your bot to participate there.
self.add_royal_game_of_ur(on_move=self.play_ur, profile={})
def enemy_at(self, payload : Payload, i : int) -> bool:
"""
Determine whether an enemy is at a given position.
"""
return payload.get(f"{i}_enemy", payload.get(str(i), "")) == "ENEMY"
def play_ur(self, payload : Payload) -> Payload:
"""
Use a heuristic. The score of each state is the total number of
spaces that your pieces have moved forward minus the total number
of spaces that your opponent's pieces have moved forward.
"""
moves = [ int(n) for n in payload.get("valid_moves", [0]) ]
star_fields = [ int(n) for n in payload.get("star_fields", []) ]
safe_fields = [ int(n) for n in payload.get("safe_fields", []) ]
roll = int(payload.get("roll", 0))
scores : dict[int, float] = { i : 0 for i in moves }
for m in moves:
dest = m + roll
# The total sum of steps taken will be the same regardless of which
# piece we move. Hence we ignore the roll itself.
#
# There's a few other aspects we need to keep in mind, however.
# If we can hit a star, we'll be able to roll again.
# The expected value is that we'll be able to take 2 extra steps.
if dest in star_fields:
scores[m] += 2
# If we can capture an enemy's piece, they'll be put back to start.
# That is a massive gain of points.
if self.enemy_at(payload=payload, i=dest) and dest not in safe_fields and dest not in star_fields:
scores[m] += dest
# Every spot has a certain danger to it.
# Improve the score based on the danger of the spot we're leaving.
scores[m] += self.risk_score(payload=payload, i=m)
# Similarly, punish the score based on the danger we're getting
# ourselves into.
scores[m] -= self.risk_score(payload=payload, i=dest)
# Near the end, you can get stuck if it takes too long to finish
# Therefore, slightly before the finish, you're rewarded with
# finishing precisely.
off = m - 15
if off in BAD_OMEN:
scores[m] = 1 / BAD_OMEN[off]
highscore = max(scores.values())
keys = [ pos for pos, score in scores.items() if score == highscore ]
return { "move": random.choice(keys) }
def risk_score(self, payload : Payload, i : int) -> float:
"""
What is the risk score of a given field?
The risk score is the expected number of steps you'll need to move
back as a result of your opponent's next turn.
"""
if i in list(payload.get("safe_fields", [])) or i in list(payload.get("star_fields", [])):
return 0.0
score : float = 0.0
for offset, risk in BAD_OMEN.items():
if self.enemy_at(payload=payload, i=i+offset):
score += risk
return i * score