Skip to content
Open
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Python
.ipynb_checkpoints
*_cache
__pycache__
.venv/

# Hydra
outputs/

# Workspace
.vscode
39 changes: 39 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Commands to use pre-commit
# pip install pre-commit
# pre-commit install
# pre-commit run --all-files
# git commit -m "<message>" --no-verify

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
exclude: 'replays/'
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=1024']
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: requirements-txt-fixer
- repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.11
hooks:
- id: ruff
args: [ --fix ]
types_or: [ python, pyi, jupyter ]
exclude: 'constants.py'
- id: ruff-format
types_or: [ python, pyi, jupyter ]
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.8.0
# hooks:
# - id: mypy
# additional_dependencies: [types-all]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The following image is an example of a hop. The highlighted green piece may hop
- A game interface that supports 2 to 3 players. Any of them can be a human player.
- Export replay to file
- Load an existing replay file to watch the game (you may click the buttons or press the left and right arrow keys to navigate through the game)
- You may create **custom bots** under the `custom_bots` folder! It'll automatically be added into the game. For more information, check out the [custom bot guide](https://github.com/henrychess/pygame-chinese-checkers/blob/main/custom_bots/README.md).
- You may create **custom bots** under the `custom_bots` folder! It'll automatically be added into the game. For more information, check out the [custom bot guide](bots/README.md).

## Showcase video

Expand Down
23 changes: 14 additions & 9 deletions custom_bots/CustomBotTemplate.py → bots/BotTemplate.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
from game_logic.player import Player
from game_logic.game import *
from game_logic.helpers import add, mult
from game_logic.game import Game
from game_logic.helpers import subj_to_obj_coor


class CustomBotTemplate(Player):
def __init__(self):
super().__init__()
def pickMove(self, g:Game):

def pickMove(self, g: Game):
moves = g.allMovesDict(self.playerNum)
#board_state = g.getBoardState(self.playerNum)
#bool_board_state = g.getBoolBoardState(self.playerNum)
# board_state = g.getBoardState(self.playerNum)
# bool_board_state = g.getBoolBoardState(self.playerNum)
"""
The following code section is a simple example: it
randomly picks a valid move and return it.
"""
from random import choice

l = []
for coor in moves:
if moves[coor] != []: l.append(coor)
if moves[coor] != []:
l.append(coor)
start = choice(l)
end = choice(moves[start])
"""
This is the return section. `start` and `end` are
the starting and ending subjective coordinates.
"""
return [subj_to_obj_coor(start, self.playerNum),
subj_to_obj_coor(end, self.playerNum)]
return [
subj_to_obj_coor(start, self.playerNum),
subj_to_obj_coor(end, self.playerNum),
]
64 changes: 64 additions & 0 deletions bots/GreedyBot0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import random
from game_logic.game import Game
from game_logic.helpers import subj_to_obj_coor
from game_logic.player import Player


class GreedyBot0(Player):
"""
Choose a forward move randomly. Else, choose a sideway move randomly.
"""

def __init__(self):
super().__init__()

def pickMove(self, g: Game):
"""
Choose a forward move randomly. Else, choose a sideway move randomly.

Returns:
[start_coor, end_coor] : in objective coordinates
"""
print(f"[GreedyBot0] is player {self.playerNum}")
moves = g.allMovesDict(self.playerNum)
forwardMoves = dict()
sidewaysMoves = dict()
(start_coor, end_coor) = ((), ())

# Split moves into forward and sideways
for coor in moves:
# If there are moves
if moves[coor] != []:
forwardMoves[coor] = []
sidewaysMoves[coor] = []

# Check y-coordinate of destination
for dest in moves[coor]:
if dest[1] > coor[1]:
forwardMoves[coor].append(dest)
if dest[1] == coor[1]:
sidewaysMoves[coor].append(dest)

# Remove empty keys
for coor in list(forwardMoves):
if forwardMoves[coor] == []:
del forwardMoves[coor]
for coor in list(sidewaysMoves):
if sidewaysMoves[coor] == []:
del sidewaysMoves[coor]

# Choose a forward move randomly
if len(forwardMoves) != 0:
start_coor = random.choice(list(forwardMoves))
end_coor = random.choice(forwardMoves[start_coor])
# Else, choose a sideway move randomly
else:
start_coor = random.choice(list(sidewaysMoves))
end_coor = random.choice(sidewaysMoves[start_coor])

move = [
subj_to_obj_coor(start_coor, self.playerNum, g.layout),
subj_to_obj_coor(end_coor, self.playerNum, g.layout),
]
print(f"[GreedyBot0] Move: {move}\n")
return move
86 changes: 86 additions & 0 deletions bots/GreedyBot1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import random
from game_logic.player import Player
from game_logic.game import Game
from game_logic.helpers import subj_to_obj_coor


class GreedyBot1(Player):
"""
Choose the move that moves a piece to the topmost cell.
"""

def __init__(self):
super().__init__()

def pickMove(self, g: Game):
"""
Choose the forward move with the greatest y value. If there are no
forward moves, choose a sideway move randomly.

Returns:
[startCoor, endCoor] : in objective coordinates
"""
print(f"[GreedyBot1] is player {self.playerNum}")
moves = g.allMovesDict(self.playerNum)
forwardMoves = dict()
sidewaysMoves = dict()
(startCoor, endCoor) = ((), ())

# Split moves into forward and sideways
for startCoor in moves:
# Check y-coordinate of destination
for dest in moves[startCoor]:
if dest[1] > startCoor[1]:
forwardMoves[startCoor] = dest
if dest[1] == startCoor[1]:
sidewaysMoves[startCoor] = dest
# BUG: moves return int when there are no forward moves

# If there are no forward moves, move sideways randomly.
if len(forwardMoves) == 0:
print("[GreedyBot1] No forward moves")
print(sidewaysMoves)
startCoor = random.choice(list(sidewaysMoves))
endCoor = random.choice(sidewaysMoves[startCoor])

move = [
subj_to_obj_coor(startCoor, self.playerNum, g.layout),
subj_to_obj_coor(endCoor, self.playerNum, g.layout),
]
print(f"[GreedyBot1] Move: {move}\n")
return move

# Choose the furthest destination (biggest y value in dest),
# then backmost piece (smallest y value in coor)
biggestDestY = -8
smallestStartY = 8
for coor in forwardMoves:
dest = forwardMoves[coor]
# Find forward move with biggest y dest value
if dest[1] > biggestDestY:
(startCoor, endCoor) = (coor, dest)
biggestDestY = dest[1]
smallestStartY = coor[1]

elif dest[1] == biggestDestY:
startY = coor[1]

# If tiebreakers,
# choose forward move with smallest y start value
if startY < smallestStartY:
(startCoor, endCoor) = (coor, dest)
biggestDestY = dest[1]
smallestStartY = coor[1]
elif startY == smallestStartY:
startCoor, endCoor = random.choice(
[[startCoor, endCoor], [coor, dest]],
)
biggestDestY = endCoor[1]
smallestStartY = startCoor[1]

move = [
subj_to_obj_coor(startCoor, self.playerNum, g.layout),
subj_to_obj_coor(endCoor, self.playerNum, g.layout),
]
print(f"[GreedyBot1] Move: {move}\n")
return move
75 changes: 75 additions & 0 deletions bots/GreedyBot2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import random
from game_logic.player import Player
from game_logic.game import Game
from game_logic.helpers import subj_to_obj_coor


class GreedyBot2(Player):
"""Always finds a move that jumps through the maximum distance (dest[1] - coor[1])"""

def __init__(self):
super().__init__()

def pickMove(self, g: Game):
"""
Choose a forward move with the greatest distance travelled. If there
are no forward moves, choose a sideway move randomly.

Returns:
[start_coor, end_coor] : in objective coordinates
"""
print(f"[GreedyBot2] is player {self.playerNum}")

forwardMoves = dict()
sidewaysMoves = dict()
(start_coor, end_coor) = (None, None)

# Split moves into forward and sideways
moves = g.allMovesDict(self.playerNum)
for coor in moves:
if moves == []:
continue
forwardMoves[coor] = []
sidewaysMoves[coor] = []
for dest in moves[coor]:
if dest[1] > coor[1]:
forwardMoves[coor].append(dest)
if dest[1] == coor[1]:
sidewaysMoves[coor].append(dest)

# If forward is empty, move sideways
if len(forwardMoves) == 0:
start_coor = random.choice(list(sidewaysMoves))
end_coor = random.choice(sidewaysMoves[start_coor])
move = [
subj_to_obj_coor(start_coor, self.playerNum, g.layout),
subj_to_obj_coor(end_coor, self.playerNum, g.layout),
]
print(f"[GreedyBot2] Move: {move}\n")
return move

# Find forward with the max distance travelled
max_dist = 0
for coor in forwardMoves:
for dest in forwardMoves[coor]:
dist = dest[1] - coor[1]
if dist > max_dist:
max_dist = dist
(start_coor, end_coor) = (coor, dest)
elif dist == max_dist:
# Prefer to move the piece that is more backwards
if dest[1] < end_coor[1]:
max_dist = dist
(start_coor, end_coor) = (coor, dest)

if start_coor is None or end_coor is None:
print("[GreedyBot2] Error: No move found")
start_coor = random.choice(list(moves))
end_coor = random.choice(moves[start_coor])

move = [
subj_to_obj_coor(start_coor, self.playerNum, g.layout),
subj_to_obj_coor(end_coor, self.playerNum, g.layout),
]
print(f"[GreedyBot2] Move: {move}\n")
return move
File renamed without changes.
33 changes: 33 additions & 0 deletions bots/RandomBot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import random
from game_logic.player import Player
from game_logic.game import Game
from game_logic.helpers import subj_to_obj_coor


class RandomBot(Player):
def __init__(self):
super().__init__()

def pickMove(self, g: Game):
"""
Returns:
[start_coor, end_coor] : in objective coordinates
"""
print(f"[RandomBot] is player {self.playerNum}")
moves = g.allMovesDict(self.playerNum)

start_coords = []
for coor in moves:
if moves[coor] != []:
start_coords.append(coor)

# Choose a random start_coor
start_coor = random.choice(start_coords)
end_coor = random.choice(moves[start_coor])

move = [
subj_to_obj_coor(start_coor, self.playerNum, g.layout),
subj_to_obj_coor(end_coor, self.playerNum, g.layout),
]
print(f"[RandomBot] Move: {move}\n")
return move
13 changes: 13 additions & 0 deletions bots/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import importlib
from glob import glob

for module in glob("custom_bots/*py"):
if not module.endswith("__init__.py"):
importlib.import_module(
module.replace(
"/",
".",
)
.replace("\\", ".")
.removesuffix(".py"),
)
Loading