Blokus

Game logic

Game Logic code
def init_game(game_settings, ref_data):
    player_count = int(game_settings["Player Count"])
    required_positions = [
        {
            "positions": [{"x": 0, "y": 0}]
        },
        {
            "positions": [{"x": 19, "y": 0}]
        }, 
        {
            "positions": [{"x": 19, "y": 19}]
        },
        {
            "positions": [{"x": 0, "y": 19}]
        }
    ]
    
    return {
        "player_hands": [list(range(len(ref_data["piece_templates"]))) for i in range(4)],
        "game_board": [-1]*(20*20),
        "players_unavailable_spots": [{
            "unavailable_spots": [0]*(20*20)
        } for i in range(4)],
        "scores": [0]*player_count,
        "skips": 0,
        "required_positions": required_positions,
        "player_turn_idx": 0,
        "player_idx_to_play": 0,
        "player_idx_to_play_fourth_color": 0 if player_count == 3 else None
    }

def place_piece(state, ref_data, piece_idx, orientation_idx, offset_x, offset_y):
    player_idx = state["player_idx_to_play"]
    if piece_idx not in state["player_hands"][player_idx]:
        raise InvalidActionError("piece_idx not in the player's hand")

    if orientation_idx < 0 or orientation_idx >= len(ref_data["piece_templates"][piece_idx]["all_orientations"]):
        raise InvalidActionError("orientation_idx out of bound")
    piece = ref_data["piece_templates"][piece_idx]["all_orientations"][orientation_idx]

    board = state["game_board"]
    positions, sides, corners = _convert_pos_to_coordinate_tuples(piece, offset_x, offset_y)
    required_positions = state["required_positions"][player_idx]["positions"]
    required_positions = set([(pos["x"], pos["y"]) for pos in required_positions])

    is_required_position_met = False
    for x, y in positions:
        if x < 0 or y < 0 or x >= 20 or y >= 20:
            raise InvalidActionError("Placing this piece here goes out of the board")
        if board[_coordinate_to_index(x, y)] != -1:
            raise InvalidActionError("Piece overlaps with another on the board")
        if (x, y) in required_positions:
            is_required_position_met = True
    if not is_required_position_met:
        raise InvalidActionError("Piece must be placed in one of the required positions (check if it touches the corner of another one of your piece)")

    for x, y in sides:
        if x < 0 or y < 0 or x >= 20 or y >= 20:
            continue
        if board[_coordinate_to_index(x, y)] == player_idx:
            raise InvalidActionError("The side of this piece touches another one of your own pieces")

    # Place the piece and update all the other helper fields
    for x, y in positions:
        index = _coordinate_to_index(x, y)
        board[index] = player_idx
        for unavailable_spots in state["players_unavailable_spots"]:
            unavailable_spots["unavailable_spots"][index] = 1

    for x, y in sides:
        if x < 0 or y < 0 or x >= 20 or y >= 20:
            continue
        state["players_unavailable_spots"][player_idx]["unavailable_spots"][_coordinate_to_index(x, y)] = 1

    for x, y in corners:
        if x >= 0 and y >= 0 and x < 20 and y < 20:
            state["required_positions"][player_idx]["positions"].append({"x": x, "y": y})

    state["player_hands"][player_idx].remove(piece_idx)
    state = _sanitize_required_positions(state, player_idx)
    state["game_board"] = board
    state["skips"] = 0
    state = _update_score(state, len(positions))
    state = _update_player_turns(state)
    return state

def pass_turn(state, ref_data):
    state["skips"] += 1
    state = _update_player_turns(state)
    return state

def get_game_result(state, ref_data):
    if state["skips"] == len(state["scores"]):
        reversed_scores = state["scores"][::-1] # if scores are equal, last player to move wins
        return {
            "game_result": "Winner",
            "winner_idx": len(reversed_scores) - 1 - reversed_scores.index(max(reversed_scores))
        }
    return {
            "game_result": "NoWinnerYet"
        }

def _convert_pos_to_coordinate_tuples(piece, offset_x, offset_y):
    positions = [(pos["x"] + offset_x, pos["y"] + offset_y) for pos in piece["pos"]]
    sides = [(side["x"] + offset_x, side["y"] + offset_y) for side in piece["side"]]
    corners = [(corner["x"] + offset_x, corner["y"] + offset_y) for corner in piece["corner"]]
    return positions, sides, corners

def _coordinate_to_index(x, y):
    return x + y*20

def _sanitize_required_positions(state, player_idx):
    required_positions = state["required_positions"]
    players_unavailable_spots = state["players_unavailable_spots"]
    board = state["game_board"]

    new_required_positions = []
    for i in range(4):
        required = required_positions[i]
        unavailable_spots = players_unavailable_spots[i]["unavailable_spots"]
        new_required = []
        for pos in required["positions"]:
            index = _coordinate_to_index(pos["x"], pos["y"])
            if unavailable_spots[index] == 1:
                continue # this spot is no longer available, so it is not required
            
            if board[index] == -1:
                new_required.append(pos)
        new_required_positions.append({
            "positions": new_required
            })
    state["required_positions"] = new_required_positions
    return state

def _update_score(state, score):
    num_of_players = len(state["scores"])
    if num_of_players == 2 or num_of_players == 4:
        state["scores"][state["player_idx_to_play"] % num_of_players] += score
    elif num_of_players == 3:
        if state["player_idx_to_play"] != 3:
            state["scores"][state["player_idx_to_play"]] += score
    
    return state

def _update_player_turns(state):
    state["player_idx_to_play"] = (state["player_idx_to_play"] + 1) % 4 # colors always increment

    num_of_players = len(state["scores"])
    if num_of_players == 2 or num_of_players == 4:
        state["player_turn_idx"] = state["player_idx_to_play"] % num_of_players
    elif num_of_players == 3:
        if state["player_idx_to_play"] == 3:
            state["player_turn_idx"] = state["player_idx_to_play_fourth_color"]
            state["player_idx_to_play_fourth_color"] = (state["player_idx_to_play_fourth_color"] + 1) % 3
        else:
            state["player_turn_idx"] = state["player_idx_to_play"]
    
    return state

Game state schema

message State {
  repeated int32 player_hands = 1;
  repeated int32 game_board = 2;
  repeated UnavailableSpots players_unavailable_spots = 3;
  int32 player_turn_idx = 4;
  repeated int32 scores = 5;
  int32 skips = 6;
  repeated RequiredPositions required_positions = 7;
  int32 player_idx_to_play = 8;
  int32 player_idx_to_play_fourth_color = 9;
}
message Orientation {
  repeated Position pos = 1;
  repeated Position side = 2;
  repeated Position corner = 3;
}
message PieceTemplate {
  repeated Orientation all_orientations = 1;
}
message RefData {
  repeated PieceTemplate piece_templates = 1;
}
message RequiredPositions {
  repeated Position positions = 1;
}
message Position {
  int32 x = 1;
  int32 y = 1;
}
message UnavailableSpots {
  repeated int32 unavailable_spots = 1;
}

Action schema

place_piece(int piece_idx, int orientation_idx, int offset_x, int offset_y)
pass_turn()

Start building now

Last updated