Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert most sample bots to support maximizing and minimizing their heuristic #38

Merged
merged 14 commits into from
Nov 28, 2023
15 changes: 9 additions & 6 deletions example_tournament.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@

def run_tournament():
tournament = tilewe.tournament.Tournament([
tilewe.engine.MaximizeMoveDifferenceEngine(),
tilewe.engine.LargestPieceEngine(),
tilewe.engine.MoveDifferenceEngine("MaxMoveDiff", "max"),
tilewe.engine.MoveDifferenceEngine("MinMoveDiff", "min"),
tilewe.engine.PieceSizeEngine("LargestPiece", "max"),
tilewe.engine.PieceSizeEngine("SmallestPiece", "min"),
tilewe.engine.TileWeightEngine("WallCrawler", 'wall_crawl'),
tilewe.engine.TileWeightEngine("Turtle", 'turtle'),
tilewe.engine.MostOpenCornersEngine(),
tilewe.engine.OpenCornersEngine("MostOpenCorners", "max"),
maconard marked this conversation as resolved.
Show resolved Hide resolved
tilewe.engine.OpenCornersEngine("LeastOpenCorners", "min"),
tilewe.engine.RandomEngine(),
])

results = tournament.play(100, n_threads=multiprocessing.cpu_count(), move_seconds=1, elo_mode="estimated")

# print the result of game 1
Expand All @@ -23,9 +26,9 @@ def run_tournament():
print(f"Tournament ran for {round(results.real_time, 4)}s with avg " +
f"match duration {round(results.average_match_duration, 4)}s\n")

# print the engine rankings sorted by win_counts desc and then by avg_scores asc
# print the engine rankings sorted by win_counts desc and then by elo_error_margin desc
print(results.get_engine_rankings_display('win_counts', 'desc'))
print(results.get_engine_rankings_display('avg_scores', 'asc'))
print(results.get_engine_rankings_display('elo_error_margin', 'desc'))

if __name__ == '__main__':
run_tournament()
5 changes: 5 additions & 0 deletions tilewe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,8 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool:
]

from ctilewe import * # noqa: E402, F401, F403

PIECE_TILES: list[int] = [n_piece_tiles(piece) for piece in range(NO_PIECE)]
PIECE_CORNERS: list[int] = [n_piece_corners(piece) for piece in range(NO_PIECE)]
PIECE_CONTACTS: list[int] = [n_piece_contacts(piece) for piece in range(NO_PIECE)]
maconard marked this conversation as resolved.
Show resolved Hide resolved

48 changes: 33 additions & 15 deletions tilewe/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,22 @@ def __init__(self, name: str="Random", estimated_elo: float=None):
def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move:
return random.choice(board.generate_legal_moves())

class MostOpenCornersEngine(Engine):
class OpenCornersEngine(Engine):
"""
Plays the move that results in the player having the most
playable corners possible afterwards, i.e. maximizing the
possible moves on the next turn.
Fairly weak but does result in decent board coverage behavior.
"""

def __init__(self, name: str="MostOpenCorners", estimated_elo: float=None):
super().__init__(name, 15.0 if estimated_elo is None else estimated_elo)
def __init__(self, name: str="MostOpenCorners", style: str="max", estimated_elo: float=None):
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")
if estimated_elo is None:
estimated_elo = -120.0 if style == "min" else 15.0
self.func = min if style == "min" else max

super().__init__(name, estimated_elo)

def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move:
moves = board.generate_legal_moves()
Expand All @@ -100,9 +106,9 @@ def corners_after_move(m: tilewe.Move) -> int:
corners = board.n_player_corners(player)
return corners

return max(moves, key=corners_after_move)
return self.func(moves, key=corners_after_move)

class LargestPieceEngine(Engine):
class PieceSizeEngine(Engine):
"""
Plays the best legal move prioritizing the following, in order:
Piece with the most squares (i.e. most points)
Expand All @@ -113,24 +119,30 @@ class LargestPieceEngine(Engine):
ties, it's effectively a greedy form of RandomEngine.
"""

def __init__(self, name: str="LargestPiece", estimated_elo: float=None):
super().__init__(name, 30.0 if estimated_elo is None else estimated_elo)
def __init__(self, name: str="LargestPiece", style: str="max", estimated_elo: float=None):
maconard marked this conversation as resolved.
Show resolved Hide resolved
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")
if estimated_elo is None:
estimated_elo = -120.0 if style == "min" else 30.0
self.func = min if style == "min" else max

super().__init__(name, estimated_elo)

def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move:
moves = board.generate_legal_moves()
random.shuffle(moves)

def score(m: tilewe.Move):
pc = tilewe.move_piece(m)
return tilewe.n_piece_tiles(pc) * 100 + \
tilewe.n_piece_corners(pc) * 10 + \
tilewe.n_piece_contacts(pc)
return (tilewe.PIECE_TILES[pc] * 100 +
tilewe.PIECE_CORNERS[pc] * 10 +
tilewe.PIECE_CONTACTS[pc])

best = max(moves, key=score)
best = self.func(moves, key=score)

return best

class MaximizeMoveDifferenceEngine(Engine):
class MoveDifferenceEngine(Engine):
"""
Plays the move that results in the player having the best difference
in subsequent legal move counts compared to all opponents. That is,
Expand All @@ -141,8 +153,14 @@ class MaximizeMoveDifferenceEngine(Engine):
getting access to an open area on the board, etc.
"""

def __init__(self, name: str="MaximizeMoveDifference", estimated_elo: float=None):
super().__init__(name, 50.0 if estimated_elo is None else estimated_elo)
def __init__(self, name: str="MaxMoveDiff", style: str="max", estimated_elo: float=None):
maconard marked this conversation as resolved.
Show resolved Hide resolved
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")
if estimated_elo is None:
-100.0 if style == "min" else 50.0
self.func = min if style == "min" else max

super().__init__(name, estimated_elo)

def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move:
moves = board.generate_legal_moves()
Expand All @@ -159,7 +177,7 @@ def eval_after_move(m: tilewe.Move) -> int:
total += n_moves * (1 if color == player else -1)
return total

return max(moves, key=eval_after_move)
return self.func(moves, key=eval_after_move)

class TileWeightEngine(Engine):
"""
Expand Down