Skip to content

Commit

Permalink
Merge pull request #38 from nhamil/conard/moar-sample-bots
Browse files Browse the repository at this point in the history
Convert most sample bots to support maximizing and minimizing their heuristic
  • Loading branch information
nhamil authored Nov 28, 2023
2 parents 3073119 + 1b4cbba commit 627a14d
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 38 deletions.
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

0 comments on commit 627a14d

Please sign in to comment.