439 lines
15 KiB
Python
439 lines
15 KiB
Python
import base as Base
|
|
import copy, random, time, math
|
|
from PIL import Image, ImageDraw
|
|
from collections import deque
|
|
|
|
|
|
# Player template for HIVE --- ALP semestral work
|
|
# Vojta Vonasek, 2023
|
|
|
|
|
|
# PUT ALL YOUR IMPLEMENTATION INTO THIS FILE
|
|
|
|
|
|
def get_neighbors(p, q):
|
|
directions = [(0, -1), (1, -1), (1, 0), (0, 1), (-1, 1), (-1, 0)]
|
|
return [(p + dp, q + dq) for dp, dq in directions]
|
|
|
|
|
|
class Piece:
|
|
def get_piece_info(self, myisupper):
|
|
class_to_letter = {
|
|
"Bee": "Q",
|
|
"Beetle": "B",
|
|
"Spider": "S",
|
|
"Grasshopper": "G",
|
|
"Ant": "A",
|
|
}
|
|
letter = class_to_letter[self.__class__.__name__]
|
|
if not myisupper:
|
|
letter = letter.lower()
|
|
return [letter, self.p, self.q]
|
|
|
|
def __repr__(self):
|
|
return ",".join(map(str, self.get_piece_info(True)+[self.team]))
|
|
|
|
def __str__(self):
|
|
return self.__repr__()
|
|
|
|
def validate_jumps(self, board):
|
|
valid_jumps = self.get_valid_jumps(board)
|
|
validated_jumps = []
|
|
|
|
for jump in valid_jumps:
|
|
if self.hive_integrity_check(board):
|
|
validated_jumps.append(jump)
|
|
return validated_jumps
|
|
|
|
def hive_integrity_check(self, board):
|
|
board_copy = {p: {q: board[p][q] for q in board[p]} for p in board}
|
|
|
|
# See if the hive falls apart without the piece (if so, then the move wasnt valid)
|
|
board_copy[self.p][self.q] = None # Remove the piece from its current position
|
|
|
|
# Get all remaining pieces on the board
|
|
remaining_pieces = [
|
|
piece for row in board_copy.values() for piece in row.values() if piece
|
|
]
|
|
|
|
# Start BFS from a random piece
|
|
start_piece = remaining_pieces[0]
|
|
visited = set()
|
|
queue = deque([start_piece])
|
|
|
|
while queue:
|
|
current_piece = queue.popleft()
|
|
visited.add(current_piece)
|
|
|
|
# Get all neighbors of the current piece
|
|
for neighbor_p, neighbor_q in get_neighbors(
|
|
current_piece.p, current_piece.q
|
|
):
|
|
neighbor_piece = board_copy.get(neighbor_p, {}).get(neighbor_q)
|
|
if neighbor_piece and neighbor_piece not in visited:
|
|
queue.append(neighbor_piece)
|
|
|
|
# Check if all pieces are connected
|
|
return len(visited) == len(remaining_pieces)
|
|
|
|
def get_valid_jumps() -> list:
|
|
raise NotImplementedError
|
|
|
|
|
|
class Bee(Piece):
|
|
def __init__(self, p, q, team):
|
|
self.p = p
|
|
self.q = q
|
|
self.team = team
|
|
|
|
def get_valid_jumps(self, board) -> list:
|
|
valid_moves = []
|
|
for neighbor in get_neighbors(self.p, self.q):
|
|
if neighbor in board and board[neighbor] == "":
|
|
valid_moves.append(neighbor)
|
|
return valid_moves
|
|
|
|
|
|
class Beetle(Piece):
|
|
def __init__(self, p, q, team):
|
|
self.p = p
|
|
self.q = q
|
|
self.team = team
|
|
|
|
def get_valid_jumps(self, board):
|
|
valid_moves = get_neighbors(self.p, self.q)
|
|
return valid_moves
|
|
|
|
|
|
class Spider(Piece):
|
|
def __init__(self, p, q, team):
|
|
self.p = p
|
|
self.q = q
|
|
self.team = team
|
|
|
|
def get_valid_jumps(self, board):
|
|
start = (self.p, self.q)
|
|
visited = set()
|
|
queue = deque([(start, 0)]) # Queue of (position, distance)
|
|
valid_moves = []
|
|
|
|
while queue:
|
|
current_position, current_distance = queue.popleft()
|
|
if current_distance == 3:
|
|
valid_moves.append(current_position)
|
|
continue
|
|
|
|
if current_position in visited:
|
|
continue
|
|
|
|
visited.add(current_position)
|
|
|
|
for neighbor in get_neighbors(*current_position):
|
|
if (
|
|
neighbor in board
|
|
and board[neighbor] == ""
|
|
and neighbor not in visited
|
|
):
|
|
queue.append((neighbor, current_distance + 1))
|
|
|
|
return valid_moves
|
|
|
|
|
|
class Grasshopper(Piece):
|
|
def __init__(self, p, q, team):
|
|
self.p = p
|
|
self.q = q
|
|
self.team = team
|
|
|
|
def get_valid_jumps(self, board):
|
|
# Generator function to yield valid moves
|
|
def generate_moves():
|
|
for dp, dq in get_neighbors(0, 0):
|
|
pos = (self.p + dp, self.q + dq)
|
|
while pos in board and board[pos] != "":
|
|
pos = (pos[0] + dp, pos[1] + dq)
|
|
if (
|
|
pos in board
|
|
and board[pos] == ""
|
|
and (pos[0] != self.p or pos[1] != self.q)
|
|
):
|
|
yield pos
|
|
|
|
valid_moves = list(generate_moves())
|
|
return valid_moves
|
|
|
|
|
|
class Ant(Piece):
|
|
def __init__(self, p, q, team):
|
|
self.p = p
|
|
self.q = q
|
|
self.team = team
|
|
|
|
def get_valid_jumps(self, board):
|
|
visited = set() # Keep track of visited nodes to prevent cycles
|
|
|
|
# Just a recursive, eh?
|
|
def explore(p, q):
|
|
for dp, dq in get_neighbors(0, 0):
|
|
new_p, new_q = p + dp, q + dq
|
|
next_pos = (new_p, new_q)
|
|
while (
|
|
next_pos in board
|
|
and board[next_pos] != ""
|
|
and next_pos not in visited
|
|
):
|
|
visited.add(next_pos)
|
|
next_pos = (next_pos[0] + dp, next_pos[1] + dq)
|
|
if next_pos in board and board[next_pos] == "":
|
|
visited.add(next_pos)
|
|
yield next_pos
|
|
yield from explore(next_pos[0], next_pos[1])
|
|
|
|
valid_moves = list(explore(self.p, self.q))
|
|
return valid_moves
|
|
|
|
|
|
class Player(Base.Board):
|
|
def __init__(
|
|
self, playerName, myIsUpper, size, myPieces, rivalPieces
|
|
): # do not change this line
|
|
Base.Board.__init__(
|
|
self, myIsUpper, size, myPieces, rivalPieces
|
|
) # do not change this line
|
|
self.playerName = playerName
|
|
self.myIsUpper = myIsUpper
|
|
self.algorithmName = "just roll the dice, eh?"
|
|
self.transated_board = dict()
|
|
self.can_place = lambda: sum([v for v in self.myPieces.values()])
|
|
|
|
def getAllEmptyCells(self):
|
|
result = []
|
|
for p in self.board:
|
|
for q in self.board[p]:
|
|
if self.isEmpty(p, q, self.board):
|
|
result.append([p, q])
|
|
return result
|
|
|
|
def getAllNonemptyCells(self):
|
|
result = []
|
|
for p in self.board:
|
|
for q in self.board[p]:
|
|
if not self.isEmpty(p, q, self.board):
|
|
result.append([p, q])
|
|
return result
|
|
|
|
def translate_board(self, board):
|
|
# mapper dict
|
|
piece_class_mapping = {
|
|
"Q": Bee,
|
|
"B": Beetle,
|
|
"S": Spider,
|
|
"G": Grasshopper,
|
|
"A": Ant,
|
|
}
|
|
|
|
translated_board = {p: {} for p in board}
|
|
total_pieces_count = 0
|
|
my_pieces_count = 0
|
|
|
|
for p, row in board.items():
|
|
for q, tile_content in row.items():
|
|
if tile_content.isalpha():
|
|
topmost_piece_letter = tile_content[-1]
|
|
is_upper = topmost_piece_letter.isupper()
|
|
piece_class = piece_class_mapping.get(topmost_piece_letter.upper())
|
|
if piece_class:
|
|
piece_instance = piece_class(p, q, is_upper == self.myIsUpper)
|
|
translated_board[p][q] = piece_instance
|
|
total_pieces_count += 1
|
|
if is_upper == self.myIsUpper:
|
|
my_pieces_count += 1
|
|
else:
|
|
translated_board[p][q] = topmost_piece_letter
|
|
else:
|
|
translated_board[p][q] = tile_content
|
|
|
|
return translated_board, total_pieces_count, my_pieces_count
|
|
|
|
@property
|
|
def queen_placed(self):
|
|
return any(isinstance(p, Bee) for p in self.myPieces)
|
|
|
|
|
|
def random_piece(self, pieces):
|
|
return random.choice(pieces) if pieces else None
|
|
|
|
def get_piece_class(self, letter):
|
|
return {"Q": Bee, "B": Beetle, "S": Spider, "G": Grasshopper, "A": Ant}.get(
|
|
letter.upper()
|
|
)
|
|
|
|
def get_unplaced_pieces(self):
|
|
unplaced_pieces = []
|
|
for piece_letter, count in self.myPieces.items():
|
|
for _ in range(count):
|
|
piece_class = self.get_piece_class(piece_letter)
|
|
unplaced_pieces.append(piece_class(None, None, True))
|
|
return unplaced_pieces
|
|
def get_valid_placements(self, translated_board, piece_to_place):
|
|
valid_placements = []
|
|
|
|
# If the board is empty, place the piece at the center.
|
|
if not any(row.values() for row in translated_board.values()):
|
|
return [(3, 6)]
|
|
|
|
# Iterate over each tile in the board
|
|
for p, row in translated_board.items():
|
|
for q, tile_content in row.items():
|
|
# Check if the tile is empty
|
|
if tile_content == "":
|
|
# Check if any neighbors are your pieces and none are opponent's pieces
|
|
neighbors = get_neighbors(p, q)
|
|
if any(
|
|
# Check if the neighboring tile is occupied by your team's piece
|
|
isinstance(translated_board.get(np, {}).get(nq), Piece) and
|
|
translated_board[np][nq].team == piece_to_place.team
|
|
for np, nq in neighbors
|
|
) and not any(
|
|
# Check if the neighboring tile is occupied by the opponent's piece
|
|
isinstance(translated_board.get(np, {}).get(nq), Piece) and
|
|
translated_board[np][nq].team != piece_to_place.team
|
|
for np, nq in neighbors
|
|
):
|
|
valid_placements.append((p, q))
|
|
|
|
return valid_placements
|
|
|
|
def mover(self):
|
|
movable_pieces = [
|
|
piece
|
|
for row in self.translated_board.values()
|
|
for piece in row.values()
|
|
if piece and piece.team and piece.validate_jumps(self.translated_board)
|
|
]
|
|
chosen_piece = self.random_piece(movable_pieces) # -> can be None, usually the cause for an error
|
|
if chosen_piece:
|
|
new_p, new_q = random.choice(
|
|
chosen_piece.validate_jumps(self.translated_board)
|
|
)
|
|
return chosen_piece.get_piece_info(self.myIsUpper) + [new_p, new_q]
|
|
|
|
def placer(self):
|
|
piece_to_place = self.random_piece(self.get_unplaced_pieces())
|
|
if piece_to_place is None:
|
|
return self.mover()
|
|
if piece_to_place:
|
|
valid_placements = self.get_valid_placements(self.translated_board, piece_to_place)
|
|
new_p, new_q = random.choice(valid_placements)
|
|
return piece_to_place.get_piece_info(self.myIsUpper)[:1] + [
|
|
None,
|
|
None,
|
|
new_p,
|
|
new_q,
|
|
]
|
|
|
|
def update(self):
|
|
self.translated_board, total_pieces_count, my_pieces_count = self.translate_board(self.board)
|
|
return total_pieces_count, my_pieces_count
|
|
def move(self):
|
|
total_pieces_count, my_pieces_count = self.update()
|
|
|
|
bee_unplaced = "q" in [k.lower() for k in self.myPieces.keys()] and {k.lower(): v for k,v in self.myPieces.items()}["q"]!=0
|
|
|
|
if bee_unplaced and (total_pieces_count > 3 or random.choice([True, False])):
|
|
queen_bee = self.get_piece_class('Q')(None, None, True)
|
|
valid_placements = self.get_valid_placements(self.translated_board, queen_bee)
|
|
if valid_placements:
|
|
new_p, new_q = random.choice(valid_placements)
|
|
return queen_bee.get_piece_info(self.myIsUpper)[:1]+ [None, None, new_p, new_q]
|
|
|
|
if total_pieces_count == 0:
|
|
piece_to_place = self.random_piece(self.get_unplaced_pieces())
|
|
return (
|
|
piece_to_place.get_piece_info(self.myIsUpper)[:1] + [None, None, 3, 6]
|
|
)
|
|
|
|
elif total_pieces_count == 1:
|
|
for _, row in self.translated_board.items():
|
|
for _, piece in row.items():
|
|
if piece:
|
|
adjacent_positions = get_neighbors(piece.p, piece.q)
|
|
random_position = self.random_piece(adjacent_positions)
|
|
piece_to_place = self.random_piece(self.get_unplaced_pieces())
|
|
return (
|
|
piece_to_place.get_piece_info(self.myIsUpper)[:1]
|
|
+ [None, None, *random_position]
|
|
)
|
|
elif self.myMove <= 4:
|
|
return self.placer()
|
|
else:
|
|
move_or_place = random.choice(["move", "place"]) if self.can_place else "move"
|
|
if move_or_place == "place":
|
|
return self.placer()
|
|
else:
|
|
return self.mover()
|
|
return []
|
|
|
|
|
|
def updatePlayers(move, activePlayer, passivePlayer):
|
|
"""write move made by activePlayer player
|
|
this method assumes that all moves are correct, no checking is made
|
|
"""
|
|
if len(move) == 0:
|
|
return
|
|
|
|
animal, p, q, newp, newq = move
|
|
if p == None and q == None:
|
|
# placing new animal
|
|
activePlayer.myPieces[animal] -= 1
|
|
passivePlayer.rivalPieces = activePlayer.myPieces.copy()
|
|
else:
|
|
# just moving animal
|
|
# delete its old position
|
|
activePlayer.board[p][q] = activePlayer.board[p][q][:-1]
|
|
passivePlayer.board[p][q] = passivePlayer.board[p][q][:-1]
|
|
|
|
activePlayer.board[newp][newq] += animal
|
|
passivePlayer.board[newp][newq] += animal
|
|
|
|
|
|
if __name__ == "__main__":
|
|
boardSize = 13
|
|
smallFigures = {
|
|
"q": 1,
|
|
"a": 2,
|
|
"b": 2,
|
|
"s": 2,
|
|
"g": 2,
|
|
} # key is animal, value is how many is available for placing
|
|
bigFigures = {
|
|
figure.upper(): smallFigures[figure] for figure in smallFigures
|
|
} # same, but with upper case
|
|
|
|
P1 = Player("player1", False, 13, smallFigures, bigFigures)
|
|
P2 = Player("player2", True, 13, bigFigures, smallFigures)
|
|
|
|
filename = "begin.png"
|
|
P1.saveImage(filename)
|
|
|
|
moveIdx = 0
|
|
while True:
|
|
move = P1.move()
|
|
print("P1 returned", move)
|
|
updatePlayers(move, P1, P2) # update P1 and P2 according to the move
|
|
filename = "move-{:03d}-player1.png".format(moveIdx)
|
|
P1.saveImage(filename)
|
|
|
|
move = P2.move()
|
|
print("P2 returned", move)
|
|
updatePlayers(move, P2, P1) # update P2 and P1 according to the move
|
|
filename = "move-{:03d}-player2.png".format(moveIdx)
|
|
P1.saveImage(filename)
|
|
|
|
moveIdx += 1
|
|
P1.myMove = moveIdx
|
|
P2.myMove = moveIdx
|
|
|
|
if moveIdx > 50:
|
|
print("End of the test game")
|
|
break
|