Virus War

Game logic

Game Logic code
import random
MOVES_PER_TURN = 3
def init_game(game_settings):
    size = game_settings.get('Size')
    if size == 'Any':
        size = random.choice(['7x7', '11x11'])
    if size == '7x7':
        width = 7
        height = 7
        initial_virus_positions = [(1, 1), (5, 5)]
    elif size == '11x11':
        width = 11
        height = 11
        initial_virus_positions = [(2, 2), (8, 8)]

    viruses = [
        {
            'x': initial_virus_positions[0][0], 
            'y': initial_virus_positions[0][1], 
            'player_idx': 0, 
            'is_zombie': False
        },
        {
            'x': initial_virus_positions[1][0], 
            'y': initial_virus_positions[1][1], 
            'player_idx': 1, 
            'is_zombie': False
        }]

    state = {
        'player1_required_move_count': 1,
        'player2_required_move_count': 0,
        'viruses': viruses,
        'player_turn_idx': 0,
        'spots_visible_to_player1': __get_visible_spots(viruses, 0, width, height),
        'spots_visible_to_player2': __get_visible_spots(viruses, 1, width, height),
        'board_width': width,
        'board_height': height
        }
    return state

def move(state, x, y):
    if x < 0 or x >= state["board_width"] or y < 0 or y >= state["board_height"]:
        raise InvalidActionError("Input out of bound.")
    if state['player_turn_idx'] == 0:
        required_move_count = state['player1_required_move_count']
        spots_visible = state['spots_visible_to_player1']
    else:
        required_move_count = state['player2_required_move_count']
        spots_visible = state['spots_visible_to_player2']

    found = False
    for v in spots_visible:
        if x == v['x'] and y == v['y']:
            found = True
            break
    if not found:
        raise InvalidActionError("This is not a visible spot you can move to.")

    player_idx = state['player_turn_idx']
    new_viruses = []
    is_new_virus = True
    for v in state['viruses']:
        if x == v['x'] and y == v['y']:
            is_new_virus = False
            if v['is_zombie']:
                raise InvalidActionError("This is overlapping with a zombie virus.")
            if v['player_idx'] == player_idx:
                raise InvalidActionError( "This is overlapping with your own live virus.")
            else:
                new_viruses.append(
                    {
                        'x': x, 
                        'y': y, 
                        'player_idx': player_idx, 
                        'is_zombie': True
                    })
        else:
            new_viruses.append(v)
    if is_new_virus:
        new_viruses.append({
            'x': x, 
            'y': y, 
            'player_idx': player_idx, 
            'is_zombie': False
            })

    state['viruses'] = new_viruses
    state['spots_visible_to_player1'] = __get_visible_spots(
        state['viruses'], 0, state['board_width'], state['board_height'])
    state['spots_visible_to_player2'] = __get_visible_spots(
        state['viruses'], 1, state['board_width'], state['board_height'])

    if player_idx == 0:
        if state['player1_required_move_count'] == 1:
            state['player2_required_move_count'] = MOVES_PER_TURN
            state['player_turn_idx'] = 1
        state['player1_required_move_count'] = state['player1_required_move_count'] - 1
    else:
        if state['player2_required_move_count'] == 1:
            state['player1_required_move_count'] = MOVES_PER_TURN
            state['player_turn_idx'] = 0
        state['player2_required_move_count'] = state['player2_required_move_count'] - 1
    
    return state

def __get_visible_spots(viruses, player_idx, board_width, board_height):
    """
    Only spots that are adjacent to live viruses, or spots that are adjacent to zombie viruses that are connected to live viruses via other zombie viruses are visible. 
    Zombie viruses themselves are also visible, even if they aren't connected.
    Return those spots.
    """
    position_to_virus_map = {}
    positions_connected_to_live = set()
    zombie_positions = []
    for v in viruses:
        position = (v['x'], v['y'])
        position_to_virus_map[position] = v
        if v['player_idx'] == player_idx:
            if v['is_zombie']:
                zombie_positions.append(position)
            else:
                positions_connected_to_live.add(position)
    connected_zombies = __get_zombies_connected_to_live_helper(positions_connected_to_live, zombie_positions)

    for cz in connected_zombies:
        positions_connected_to_live.add(cz)

    visible_spots = set()
    for x, y in positions_connected_to_live:
        for offset_x in range(-1, 2):
            for offset_y in range(-1, 2):
                new_x = x + offset_x
                new_y = y + offset_y
                if new_x < 0 or new_x >= board_width or new_y < 0 or new_y >= board_height:
                        continue
                visible_spots.add((new_x, new_y))
    for x, y in zombie_positions:
        visible_spots.add((x, y))

    # Convert visible spots into virus model. Empty spots without a virus will be represented as a virus with player_idx = -1.
    visible_viruses = []
    for p in visible_spots:
        if p in position_to_virus_map:
            visible_viruses.append(position_to_virus_map[p])
        else:
            visible_viruses.append({
                'x': p[0], 
                'y': p[1], 
                'player_idx': -1,
                'is_zombie': False
            })
    return visible_viruses

def __get_zombies_connected_to_live_helper(positions_connected_to_live, zombie_positions):
    seen = set()
    zombies_connected = set()
    for p in zombie_positions:
        if p in seen:
            continue

        # A DFS for each zombie group to see if they are connected to live viruses at some point
        current_zombie_group = set([p])
        is_group_connected_to_live = False
        stack = [p]

        while stack:
            current = stack.pop()
            seen.add(current)
            if current in positions_connected_to_live:
                is_group_connected_to_live = True
                continue
            current_zombie_group.add(current)
            for offset_x in range(-1, 2):
                for offset_y in range(-1, 2):
                    neighbor = (current[0] + offset_x, current[1] + offset_y)
                    if neighbor in seen or (neighbor not in positions_connected_to_live and neighbor not in zombie_positions):
                        continue
                    stack.append(neighbor)
        if is_group_connected_to_live:
            for z in current_zombie_group:
                zombies_connected.add(z)
    return zombies_connected

def get_game_result(state):
    if state['player1_required_move_count'] > 0:
        for v in state['spots_visible_to_player1']:
            if v['player_idx'] == -1 or (v['player_idx'] == 1 and not v['is_zombie']):
                return {
                    "game_result": "NoWinnerYet"
                }
        return {
                "game_result": "Winner",
                "winner_idx": 1
            }
    else:
        for v in state['spots_visible_to_player2']:
            if v['player_idx'] == -1 or (v['player_idx'] == 0 and not v['is_zombie']):
                return {
                    "game_result": "NoWinnerYet"
                }
        return {
                "game_result": "Winner",
                "winner_idx": 0
            }

def get_player_states(state):
    player_states = []
    for i in range(2):
        if i == 0:
            required_move_count = state['player1_required_move_count']
            visible_viruses = state['spots_visible_to_player1']
        else:
            required_move_count = state['player2_required_move_count']
            visible_viruses = state['spots_visible_to_player2']
        
        player_states.append({
            'player_turn_idx': state['player_turn_idx'],
            'required_move_count': required_move_count,
            'visible_viruses': visible_viruses,
            'board_width': state['board_width'],
            'board_height': state['board_height']
        })
    return player_states

Game state schema

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

  // With the exception of the first move, players alternate taking 3 consecutive
  // moves. This field indicates how many moves are still required before switching
  // to the opponent. Player who cannot make the move loses.
  int32 required_move_count = 2;

  // A list of visible spots that a player can potentially take an action on.
  // Pick an empty spot to move to, or convert opponent's live virus into a zombie.
  repeated Virus visible_viruses = 3;

  // Width of board, configured by the game setting
  int32 board_width = 4;

  // Height of board, configured by the game setting
  int32 board_height = 5;
}
/*
Describes the status of a spot on the grid. A player may not see the complete
board.
*/
message Virus {
  // Coordinate on the grid
  int32 x = 1;
  int32 y = 2;

  // Can be -1, 0, or 1. If the board is empty at this coordinate, player_idx is -1. 
  int32 player_idx = 3;

  // A virus can be alive or zombie. If the coordinate is empty, this field is false.
  bool is_zombie = 4;
}

Action schema

move(int x, int y)

Start building now

Last updated