Create UrHeuristic agent
parent
5453a8acc3
commit
a98e2c3c1e
|
|
@ -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
|
||||||
|
|
||||||
Loading…
Reference in New Issue