Skip to content

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

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

Merged
merged 14 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions example_tournament.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@

def run_tournament():
tournament = tilewe.tournament.Tournament([
tilewe.engine.MaximizeMoveDifferenceEngine(),
tilewe.engine.LargestPieceEngine(),
tilewe.engine.TileWeightEngine("WallCrawler", 'wall_crawl'),
tilewe.engine.TileWeightEngine("Turtle", 'turtle'),
tilewe.engine.MostOpenCornersEngine(),
tilewe.engine.RandomEngine(),
tilewe.engine.MoveDifferenceEngine(style="max"),
tilewe.engine.MoveDifferenceEngine(style="min"),
tilewe.engine.PieceSizeEngine(style="max"),
tilewe.engine.PieceSizeEngine(style="min"),
tilewe.engine.TileWeightEngine(style='wall_crawl'),
tilewe.engine.TileWeightEngine(style='turtle'),
tilewe.engine.OpenCornersEngine(style="max"),
tilewe.engine.OpenCornersEngine(style="min"),
tilewe.engine.RandomEngine(name="Random"),
])

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

# print the result of game 1
print(results.match_data[0].board)
# print(results.match_data[0].board)

# print the total real time the tournament took and the average duration of each match, in seconds
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(results.get_engine_rankings_display('win_counts', 'desc'))
print(results.get_engine_rankings_display('avg_scores', 'asc'))
# print the engine rankings sorted by win_counts desc and then by elo_error_margin asc
# print(results.get_engine_rankings_display('win_counts', 'desc'))
# print(results.get_engine_rankings_display('elo_error_margin', 'asc'))

if __name__ == '__main__':
run_tournament()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = [
]
description = "A tile-placing game for AI development fun"
readme = "README.md"
requires-python = "==3.10.*"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
Expand Down
4 changes: 4 additions & 0 deletions tilewe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,7 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool:
...

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

N_PIECE_TILES: list[int] = [n_piece_tiles(piece) for piece in range(PIECE_COUNT)] # noqa: E241, E272
N_PIECE_CORNERS: list[int] = [n_piece_corners(piece) for piece in range(PIECE_COUNT)] # noqa: E241, E272
N_PIECE_CONTACTS: list[int] = [n_piece_contacts(piece) for piece in range(PIECE_COUNT)] # noqa: E241, E272
78 changes: 52 additions & 26 deletions tilewe/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,23 @@ 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=None, style: str="max", estimated_elo: float=None):
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")

name = name or ("MostOpenCorners" if style == "max" else "LeastOpenCorners")
estimated_elo = (15.0 if style == "max" else -250.0) if estimated_elo is None else estimated_elo
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 +107,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 +120,31 @@ 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=None, style: str="max", estimated_elo: float=None):
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")

name = name or ("LargestPiece" if style == "max" else "SmallestPiece")
estimated_elo = (30.0 if style == "max" else -150.0) if estimated_elo is None else estimated_elo
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 = m.piece
return tilewe.n_piece_tiles(pc) * 100 + \
tilewe.n_piece_corners(pc) * 10 + \
tilewe.n_piece_contacts(pc)
pc = m.piece
return (tilewe.N_PIECE_TILES[pc] * 100 +
tilewe.N_PIECE_CORNERS[pc] * 10 +
tilewe.N_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 +155,15 @@ 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=None, style: str="max", estimated_elo: float=None):
if style not in ["max", "min"]:
raise ValueError("Invalid style, must be 'max' or 'min'")

name = name or ("MaxMoveDiff" if style == "max" else "MinMoveDiff")
estimated_elo = (50.0 if style == "max" else -200.0) if estimated_elo is None else estimated_elo
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 +180,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 Expand Up @@ -229,29 +250,34 @@ class TileWeightEngine(Engine):
'turtle': -40.0
}

names = {
'wall_crawl': 'WallCrawler',
'turtle': 'Turtle'
}

def __init__(self,
name: str="TileWeight",
weight_map: str='wall_crawl',
name: str=None,
style: str='wall_crawl',
custom_weights: list[int | float]=None,
estimated_elo: float=None):
"""
Current `weight_map` built-in options are 'wall_crawl' and 'turtle'
Current `style` built-in options are 'wall_crawl' and 'turtle'
Can optionally provide a custom set of weights instead
"""

est_elo: float = 0.0 if estimated_elo is None else estimated_elo

if custom_weights is not None:
if len(custom_weights) != 20 * 20:
raise Exception("TileWeightEngine custom_weights must be a list of exactly 400 values")
self.weights = custom_weights
name = name or "CustomTileWeights"
est_elo: float = 0.0 if estimated_elo is None else estimated_elo

else:
if weight_map not in self.weight_maps:
raise Exception("TileWeightEngine given invalid weight_map choice")
self.weights = self.weight_maps[weight_map]
if estimated_elo is None:
est_elo = self.weight_elos[weight_map]
if style not in self.weight_maps:
raise Exception("TileWeightEngine given invalid style choice")
self.weights = self.weight_maps[style]
name = name or self.names[style]
est_elo: float = self.weight_elos[style] if estimated_elo is None else estimated_elo

super().__init__(name, estimated_elo=est_elo)

Expand Down