Compare commits
No commits in common. "agent/ur-heuristic-1.0.0" and "main" have entirely different histories.
agent/ur-h
...
main
|
|
@ -1,121 +0,0 @@
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
@ -76,10 +76,6 @@ class RoyalGameOfUr(Game):
|
||||||
for i in range(BOARD_LENGTH - self.roll):
|
for i in range(BOARD_LENGTH - self.roll):
|
||||||
dest = i + self.roll
|
dest = i + self.roll
|
||||||
|
|
||||||
# Skip star fields if they're already occupied
|
|
||||||
if dest in STAR_FIELDS and dest not in SAFE_FIELDS and self.board[dest] != ( 0, 0 ):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.board[i][self.mover] > 0:
|
if self.board[i][self.mover] > 0:
|
||||||
if dest in STACK_FIELDS or self.board[dest][self.mover] == 0:
|
if dest in STACK_FIELDS or self.board[dest][self.mover] == 0:
|
||||||
yield i
|
yield i
|
||||||
|
|
@ -93,20 +89,19 @@ class RoyalGameOfUr(Game):
|
||||||
|
|
||||||
d : dict[str, Any] = dict(
|
d : dict[str, Any] = dict(
|
||||||
enemies_at_start=self.board[0][ot_index],
|
enemies_at_start=self.board[0][ot_index],
|
||||||
enemies_at_finish=self.board[BOARD_LENGTH-1][ot_index],
|
enemies_at_finish=self.board[15][ot_index],
|
||||||
roll=self.roll,
|
|
||||||
safe_fields=list(SAFE_FIELDS),
|
safe_fields=list(SAFE_FIELDS),
|
||||||
stack_fields=list(STACK_FIELDS),
|
stack_fields=list(STACK_FIELDS),
|
||||||
star_fields=list(STAR_FIELDS),
|
star_fields=list(STAR_FIELDS),
|
||||||
tokens_at_start=self.board[0][my_index],
|
tokens_at_start=self.board[0][my_index],
|
||||||
tokens_at_finish=self.board[BOARD_LENGTH-1][my_index],
|
tokens_at_finish=self.board[15][my_index],
|
||||||
valid_moves=list(self.__valid_moves()),
|
valid_moves=list(self.__valid_moves()),
|
||||||
)
|
)
|
||||||
|
|
||||||
for i in range(BOARD_LENGTH):
|
for i in range(BOARD_LENGTH):
|
||||||
d[str(i)] = (
|
d[str(i)] = (
|
||||||
"YOU" if self.board[i][my_index] > 0 else (
|
"YOU" if self.board[i][my_index] > 0 else (
|
||||||
"ENEMY" if i not in SAFE_FIELDS and self.board[i][ot_index] > 0 else (
|
"ENEMY" if my_index not in SAFE_FIELDS and self.board[i][ot_index] > 0 else (
|
||||||
""
|
""
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import agents.ur_heuristic
|
import agents
|
||||||
|
|
||||||
from pyserver import PyServer
|
from pyserver import PyServer
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ def main() -> int:
|
||||||
:return: Exit code
|
:return: Exit code
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
player = PyServer(agents.ur_heuristic.UrHeuristic())
|
player = PyServer(agents.AgentOfChaos())
|
||||||
|
|
||||||
# Start listening for games
|
# Start listening for games
|
||||||
player.start(
|
player.start(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue