Battleship

Game logic

Game Logic code
def init_game(game_settings):
    state = {
        'phase': "PLACE_SHIP",
        'player_info': [
            {
                "ships": [],
                "sunken_ship_sizes": [],
                "shots_at_this_player": []
            },
            {
                "ships": [],
                "sunken_ship_sizes": [],
                "shots_at_this_player": []
            }
        ],
        'player_turn_idx': 0
        }
    return state

def place_ships(state, ships):
    """
    <ships> should be an array of 2+3+3+4+5 = 17 coordinates expressed as object {'x': x, 'y': y}. Elements 0~1 represent the first ship of size 2. Elements 2~4 represent the second ship of size 3, etc..., until elements 12~16 represent the fifth ship of size 5.
    """
    BOARD_WIDTH = 10
    BOARD_HEIGHT = 10

    ships = [(int(coord["x"]), int(coord["y"])) for coord in ships]
    player_idx = state["player_turn_idx"]
    other_player_idx = (player_idx + 1) % 2

    if len(state['player_info'][player_idx]['ships']) > 0:
        raise InvalidActionError("Ships already placed.")
    if __are_ships_valid(ships):
        __place_ships(state, player_idx, ships)

    
    if len(state['player_info'][other_player_idx]['ships']) > 0:
        state['phase'] = "FIRE_SHOT"

    state["player_turn_idx"] = other_player_idx
    return state

def fire_shot(state, x, y):
    BOARD_WIDTH = 10
    BOARD_HEIGHT = 10
    x = int(x)
    y = int(y)
    if state['phase'] == "PLACE_SHIP":
        raise InvalidActionError("Still waiting for ships to be placed.")
    if x < 0 or x >= BOARD_WIDTH or y < 0 or y >= BOARD_HEIGHT:
        raise InvalidActionError("Out of bound.")

    player_idx = state["player_turn_idx"]
    other_player_idx = (player_idx + 1) % 2

    other_player_info = state["player_info"][other_player_idx]
    
    target_positions = [(pos['x'], pos['y']) for ship in other_player_info['ships'] for pos in ship['positions']]
    shots_fired = set([(shot['x'], shot['y']) for shot in other_player_info['shots_at_this_player']])

    if (x, y) in shots_fired:
        raise InvalidActionError("Already placed a shot at this location.")
    if (x, y) in target_positions:
        did_hit = True
    else:
        did_hit = False

    state['player_info'][other_player_idx]['shots_at_this_player'].append({
        'x': x,
        'y': y, 
        'did_hit': did_hit
        })
    shots_fired.add((x, y))
    state['player_info'][other_player_idx]['sunken_ship_sizes'] = __get_sunken_ships(shots_fired, state['player_info'][other_player_idx]['ships'])
    state["player_turn_idx"] = other_player_idx
    return state

def __get_sunken_ships(shots, ships):
    sunken_ships = []
    for ship in ships:
        is_sunk = True
        for pos in ship['positions']:
            if (pos['x'], pos['y']) not in shots:
                is_sunk = False
                continue
        if is_sunk:
            sunken_ships.append(len(ship['positions']))
    return sunken_ships


def __place_ships(state, player_idx, ships):
    ship_models = [__make_ship_model(ships[0:2]), __make_ship_model(ships[2:5]), __make_ship_model(ships[5:8]
        ), __make_ship_model(ships[8:12]), __make_ship_model(ships[12:17])]
    state['player_info'][player_idx]['ships'] = ship_models

def __make_ship_model(coordinates):
    ship_positions = [{'x': coord[0], 'y': coord[1]} for coord in coordinates]
    return {'positions': ship_positions}

def __are_ships_valid(ships):
    if len(ships) != 17:
        raise InvalidActionError("Ships should be a list of 17 coordinates representing ships of sizes 2, 3, 3, 4, 5.")
    if len(set(ships)) != 17:
        raise InvalidActionError("Ships are overlapping.")
    if (__is_valid_ship(ships[0:2]) and 
        __is_valid_ship(ships[2:5]) and 
        __is_valid_ship(ships[5:8]) and 
        __is_valid_ship(ships[8:12]) and 
        __is_valid_ship(ships[12:17])):
        return True
    return False

def __is_valid_ship(ship):
    """
    Verify if <ship> is a list of adjacent tuples in a straight line that are within the bounds of the board.
    """
    BOARD_WIDTH = 10
    BOARD_HEIGHT = 10
    for x, y in ship:
        if x < 0 or x >= BOARD_WIDTH or y < 0 or y >= BOARD_HEIGHT:
            raise InvalidActionError("Out of bound.")
    xs = [coordinate[0] for coordinate in ship]
    ys = [coordinate[1] for coordinate in ship]
    diff_x = max(xs) - min(xs)
    diff_y = max(ys) - min(ys)
    if diff_x == 0: # vertical ship
        adjacent_coordinates = sorted(ys)
    elif diff_y == 0:
        adjacent_coordinates = sorted(xs)
    else:
        raise InvalidActionError("Ship has to be vertical or horizontal.")
    for i in range(len(adjacent_coordinates) - 1):
        if adjacent_coordinates[i+1] - adjacent_coordinates[i] != 1:
            raise InvalidActionError("Ship coordinates aren't consecutive.")
    return True

def get_game_result(state):
    if len(state['player_info'][0]['sunken_ship_sizes']) == 5:
        if len(state['player_info'][1]['sunken_ship_sizes']) == 5:
            return {
                "game_result": "Draw"
            }
        else:
            return {
                "game_result": "Winner",
                "winner_idx": 1
            }
    elif len(state['player_info'][1]['sunken_ship_sizes']) == 5 and state['player_turn_idx'] == 0:
        return {
            "game_result": "Winner",
            "winner_idx": 0
        }
    return {
        "game_result": "NoWinnerYet"
    }

def get_player_states(state):
    player_states = []
    for i in range(2):
        player_info =  [
            {
                "ships": state['player_info'][0]["ships"] if i == 0 else [],
                "sunken_ship_sizes": state['player_info'][0]["sunken_ship_sizes"],
                "shots_at_this_player": state['player_info'][0]["shots_at_this_player"]
            },
            {
                "ships": state['player_info'][1]["ships"] if i == 1 else [],
                "sunken_ship_sizes": state['player_info'][1]["sunken_ship_sizes"],
                "shots_at_this_player": state['player_info'][1]["shots_at_this_player"]
            }
        ]
        player_states.append({
            'player_info': player_info,
            'phase': state['phase'],
            'player_turn_idx': state['player_turn_idx']
            })
    return player_states

Game state schema

message State {
  enum Phase {
    PLACE_SHIP = 0;
    FIRE_SHOT = 1;
  }

  // Required field to indicate the player who should be making the next move
  // Values = 0 or 1
  int32 player_turn_idx = 1;

  // This will be length 2 for the two players. The player can only see their own ships
  // but will have information on the other fields such as the shots and sunken ships.
  repeated PlayerInfo player_info = 2;

  // The game starts off with PLACE_SHIP phase. Once both players placed their ships,
  // the phase changes to FIRE_SHOT until one of the player's ships are all sunk.
  Phase phase = 3;
}
message PlayerInfo {
  // Initially length 0. Once the ships are placed, this will be length 5.
  // The ships will have Coordinate lengths of 2, 3, 3, 4, 5.
  repeated Ship ships = 1;

  // Opponents fire shots at this player. Once a ship is sunk, it gets added
  // to this list, which stores the length of the ship that was sunk. This 
  // array will be size 0 to 5, and the game is over when all ships are sunk.
  repeated int32 sunken_ship_sizes = 2;

  // Stores all the shots at this player.
  repeated Shot shots_at_this_player = 3;
}
message Ship {
  repeated Coordinate positions = 1;
}
message Shot {
  int32 x = 1;
  int32 y = 2;
  bool did_hit = 3;
}
message Coordinate {
  int32 x = 1;
  int32 y = 2;
}

Action schema

/* 
Takes in a list of length 2+3+3+4+5 = 17 coordinates
Elements 0~1 represent the first ship of size 2. 
Elements 2~4 represent the second ship of size 3, etc..., 
until elements 12~16 represent the fifth ship of size 5.
*/
place_ships(list<coordinate> ships)

// The action needed for the second phase of the game
fire_shot(int x, int y)

Start building now

Last updated