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)
Last updated