""" 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