public class GeneralsApi {
    private static final String ONE_VS_ONE_QUEUE = "1v1";
    private static final String TWO_VS_TWO_QUEUE = "2v2";
    private static final String FFA_QUEUE = "";
    private static final Logger LOGGER = LoggerFactory.getLogger(GeneralsApi.class);
    private Socket socket;

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.registerModule(new JsonOrgModule());
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    private GeneralsApi(Socket socket) {
        this.socket = socket;

    }

    public static GeneralsApi create() {
        return new GeneralsApi(new RealSocket());
    }

    /**
     * For testing purposes.
     */
    public static GeneralsApi createWithCustomSocket(Socket socket) {
        return new GeneralsApi(socket);
    }

    public boolean isConnected() {
        return socket.connected();
    }

    /* LISTENERS */

    public GeneralsApi onEvent(String event, Consumer<Object[]> listener, boolean onlyOnce) {
        Consumer<Object[]> socketListener = args -> {
            LOGGER.trace("Received {}: {}", event, Arrays.toString(args));
            listener.accept(args);
        };
        if (onlyOnce) {
            socket.once(event, socketListener);
        } else {
            socket.on(event, socketListener);
        }
        return this;
    }

    public GeneralsApi onEvent(String event, Consumer<Object[]> listener) {
        return onEvent(event, listener, false);
    }


    public GeneralsApi onConnected(NoArgsListener listener) {
        return onEvent("connect", (args) -> listener.onEvent());
    }

    public GeneralsApi onceConnected(NoArgsListener listener) {
        return onEvent("connect", (args) -> listener.onEvent(), true);
    }

    public GeneralsApi onDisconnected(NoArgsListener listener) {
        return onEvent("disconnect", (args) -> listener.onEvent());
    }

    public GeneralsApi onGameStarted(OneArgListener<GameStartApiResponse> listener) {
        return onEvent("game_start", (args) -> listener.onEvent(objectMapper.convertValue(args[0], GameStartApiResponse.class)));
    }

    public GeneralsApi onGameUpdated(OneArgListener<GameUpdateApiResponse> listener) {
        return onEvent("game_update", (args) -> listener.onEvent(objectMapper.convertValue(args[0], GameUpdateApiResponse.class)));
    }

    public GeneralsApi onGameLost(OneArgListener<GameLostApiResponse> listener) {
        return onEvent("game_lost", (args) -> listener.onEvent(objectMapper.convertValue(args[0], GameLostApiResponse.class)));
    }

    public GeneralsApi onGameWon(NoArgsListener listener) {
        return onEvent("game_won", (args) -> listener.onEvent());
    }

    public GeneralsApi onChatMessage(ChatMessageListener listener) {
        return onEvent("chat_message", (args) -> listener.onEvent((String) args[0], objectMapper.convertValue(args[1], ChatMessageApiResponse.class)));
    }

    public GeneralsApi onRanksReceived(OneArgListener<RanksApiResponse> listener) {
        return onEvent("rank", (args) -> listener.onEvent(objectMapper.convertValue(args[0], RanksApiResponse.class)));
    }

    public GeneralsApi onStarsReceived(OneArgListener<StarsApiResponse> listener) {
        return onEvent("stars", (args) -> listener.onEvent(objectMapper.convertValue(args[0], StarsApiResponse.class)));
    }

    public GeneralsApi onSetUsernameError(OneArgListener<String> listener) {
        return onEvent("error_set_username", (args) -> {
            if (args.length > 0 && ((String) args[0]).length() > 0) {
                listener.onEvent((String) args[0]);
            }
        });
    }


    /* ACTIONS*/

    private void send(String event, Object... args) {
        LOGGER.trace("Sending {}: {}", event, Arrays.toString(args));
        socket.emit(event, args);
    }

    public void setUsername(String userId, String userName) {
        send("set_username", userId, userName);
    }

    public void joinCustomGameQueue(String userId, String gameId) {
        send("join_private", gameId, userId);
    }

    public void joinFreeForAllQueue(String userId) {
        send("play", userId);
    }

    public void joinOneVsOneQueue(String userId) {
        send("join_1v1", userId);
    }

    public void setTeamInCustomGame(String customGameId, int team) {
        send("set_custom_team", team);
    }

    public void joinTwoVsTwoQueue(String userId, String team) {
        send("join_team", team, userId);
    }

    public void leaveCustomGameQueue(String gameId) {
        leaveNonFreeForAllQueue(gameId);
    }

    public void leaveFreeForAllQueue() {
        send("cancel");
    }

    public void leaveOneVsOneQueue() {
        leaveNonFreeForAllQueue(ONE_VS_ONE_QUEUE);
    }

    public void leaveTwoVsTwoQueue() {
        leaveNonFreeForAllQueue(TWO_VS_TWO_QUEUE);
    }

    public void forceStartFreeForAllQueue(boolean forceStart) {
        send("set_force_start", FFA_QUEUE, forceStart);
    }

    public void forceStartOneVsOneQueue(boolean forceStart) {
        send("set_force_start", ONE_VS_ONE_QUEUE, forceStart);
    }

    public void forceStartTwoVsTwoQueue(boolean forceStart) {
        send("set_force_start", TWO_VS_TWO_QUEUE, forceStart);

    }

    public void forceStartCustomGameQueue(boolean forceStart, String gameId) {
        send("set_force_start", gameId, forceStart);

    }

    public void clearMoves() {
        send("clear_moves");
    }

    public void attack(int start, int end, boolean is50) {
        send("attack", start, end, is50);
    }

    public void ping(int index) {
        send("ping_tile", index);
    }

    public void sendChatMessage(String chatRoom, String text) {
        send("chat_message", chatRoom, text);
    }

    public void leaveGame() {
        send("leave_game");
    }

    public void requestStarsAndRank(String userId) {
        send("stars_and_rank", userId);
    }

    private void leaveNonFreeForAllQueue(String queueId) {
        send("cancel", queueId);
    }

    public void connect() {
        if (!isConnected()) {
            LOGGER.trace("Connecting socket");
            socket.connect();
        }
    }

    public void disconnect() {
        if (isConnected()) {
            LOGGER.trace("Disconnecting socket");
            socket.disconnect();
        }
    }
} @FunctionalInterface
public interface NoArgsListener {
    void onEvent();
} @Value
public class ChatMessageApiResponse {
    /**
     * Name of the author of the message.
     */
    private String username;
    /**
     * Player index of the author of the message.
     */
    private int playerIndex;
    /**
     * Actual content of the message
     */
    private String text;
} @Value
public class GameLostApiResponse {
    /**
     * Index of the player that killed the bot.
     */
    private int killer;
} @Value
public class GameStartApiResponse {
    /**
     * Index of bot's player.
     */
    private int playerIndex;
    /**
     * Replay identifier. Use http://bot.generals.io/replays/{replayId} to see the replay in your browser.
     */
    @JsonProperty("replay_id")
    private String replayId;
    /**
     * Identifier of the main chat room, useful when sending a chat message ({@link GeneralsApi#sendChatMessage(String, String)}).
     */
    @JsonProperty("chat_room")
    private String chatRoom;
    /**
     * Identifier of the team chat room, useful when sending a chat message ({@link GeneralsApi#sendChatMessage(String, String)}).
     * Can be null if there are no teams in the game.
     */
    @JsonProperty("team_chat_room")
    private String teamChatRoom;
    /**
     * Array of player names, indexed by player index.
     */
    private String[] usernames;
    /**
     * Array if team identifiers of each player, indexed by player index. Can be null if there are no teams/
     */
    private Integer[] teams;

} @Value
public class GameUpdateApiResponse {
    private int turn;
    /**
     * Diff to update the array with map (tiles/armies) representation. Use {@link MapPatcher} to apply the diff.
     */
    @JsonProperty("map_diff")
    private int[] mapDiff;
    /**
     * Diff to update the array with cities representation. Use {@link MapPatcher} to apply the diff.
     */
    @JsonProperty("cities_diff")
    private int[] citiesDiff;
    /**
     * Array of indexes of generals, indexed with player index. If general of player is not visible/does not exist, contains -1 in appropriate position.
     */
    private int[] generals;
    /**
     * Officially "the attackIndex field is unnecessary for bots -- it's used by our client to show/hide attack arrows when appropriate."
     * Some people say it can be used to find out state of moves queue and plan more complex moves ;-)
     */
    private int attackIndex;
    /**
     * Array indexed by player index, includes things shown in the score table (titles, army, is dead). Does not include stars.
     */
    private ScoreApiResponse[] scores;
    private double[] stars;
} @EqualsAndHashCode
@ToString
public class GameStartedApiResponseImpl implements GameStarted {

    private final GameStartApiResponse gameStartApiResponse;

    public GameStartedApiResponseImpl(GameStartApiResponse gameStartApiResponse) {
        this.gameStartApiResponse = gameStartApiResponse;
    }

    @Override
    public String getReplayId() {
        return gameStartApiResponse.getReplayId();
    }

    @Override
    public String[] getUsernames() {
        return gameStartApiResponse.getUsernames();
    }

    @Override
    public int getPlayerIndex(){
        return gameStartApiResponse.getPlayerIndex();
    }

    @Override
    public String getChatRoom(){
        return gameStartApiResponse.getChatRoom();
    }

    @Override
    public String getTeamChatRoom(){
        return gameStartApiResponse.getTeamChatRoom();
    }
} @ToString
@EqualsAndHashCode
public class UpdatableGameState implements GameState {
    /*Actual data returned to clients*/
    private final int columns;
    private final int rows;
    private final ImmutableList<Player> players;
    private final ImmutableMap<Position, Field> fieldsMap;
    /*caching visibleFieldsMap even though GameState calculates it by default as this map is very likely to be used intensively */
    private final ImmutableMap<Position, VisibleField> visibleFieldsMap;
    private final int turn;
    private final int attackIndex;


    /*Data persisted so that it's possible to update the game state */
    private final GameStartApiResponse startData;
    private final int[] rawMapArray;
    private final int[] rawCitiesArray;
    private final int[] rawGeneralsArray;


    /**
     * Creates the first game state just after the game is started. Can be done only after both messages are received from server:
     * {@link GameStartApiResponse} and {@link GameUpdateApiResponse}. Cannot be use for further updates, as later game updates
     * from server contain only differences.
     */
    public static UpdatableGameState createInitialGameState(GameStartApiResponse startData, GameUpdateApiResponse firstUpdateData) {
        return new UpdatableGameState(
                firstUpdateData.getTurn(), startData, MapPatcher.patch(firstUpdateData.getMapDiff(), new int[]{}),
                MapPatcher.patch(firstUpdateData.getCitiesDiff(), new int[]{}),
                firstUpdateData.getGenerals(),
                createPlayersInfo(startData, firstUpdateData),
                firstUpdateData.getAttackIndex());
    }


    /**
     * Updates game state from previous turn with new information received from server. The method only works fine if
     * updates are applied in sequence, as server is only sending map differences in update messages.
     */
    public UpdatableGameState update(GameUpdateApiResponse gameUpdateData) {
        return new UpdatableGameState(
                gameUpdateData.getTurn(), startData, MapPatcher.patch(gameUpdateData.getMapDiff(), rawMapArray),
                MapPatcher.patch(gameUpdateData.getCitiesDiff(), rawCitiesArray),
                gameUpdateData.getGenerals(),
                createPlayersInfo(startData, gameUpdateData),
                gameUpdateData.getAttackIndex());
    }


    @Override
    public int getColumns() {
        return columns;
    }

    @Override
    public int getRows() {
        return rows;
    }


    @Override
    public int getTurn() {
        return turn;
    }

    @Override
    public int getAttackIndex() {
        return attackIndex;
    }

    @Override
    public ImmutableMap<Position, Field> getFieldsMap() {
        return fieldsMap;
    }

    @Override
    public ImmutableMap<Position, VisibleField> getVisibleFieldsMap() {
        return visibleFieldsMap;
    }

    @Override
    public ImmutableList<Player> getPlayers() {
        return players;
    }

    @Override
    public GameStarted getGameStartedData() {
        return new GameStartedApiResponseImpl(startData);
    }


    private static ImmutableList<Player> createPlayersInfo(GameStartApiResponse startData, GameUpdateApiResponse firstUpdateData) {
        int numberOfPlayers = startData.getUsernames().length;
        Map<Integer, ScoreApiResponse> scoresMap = createScoresMap(firstUpdateData);
        List<Player> players = IntStream.range(0, numberOfPlayers).mapToObj(playerIndex -> {
            Optional<ScoreApiResponse> scoreForPlayer = Optional.ofNullable(scoresMap.get(playerIndex));
            return new Player(playerIndex,
                    startData.getUsernames()[playerIndex],
                    Optional.ofNullable(startData.getTeams()).map(teams -> teams[playerIndex]).orElse(playerIndex),
                    scoreForPlayer.map(ScoreApiResponse::getTiles).orElse(null),
                    scoreForPlayer.map(ScoreApiResponse::getTotal).orElse(null),
                    scoreForPlayer.map(ScoreApiResponse::isDead).orElse(false));
        }).collect(Collectors.toList());
        return ImmutableList.copyOf(players);
    }

    private static Map<Integer, ScoreApiResponse> createScoresMap(GameUpdateApiResponse firstUpdateData) {
        return Arrays.stream(firstUpdateData.getScores()).collect(Collectors.toMap(ScoreApiResponse::getIndex, Function.identity()));
    }


    private UpdatableGameState(int turn, GameStartApiResponse startData, int[] fields, int[] cities, int[] generals, ImmutableList<Player> players, int attackIndex) {
        this.turn = turn;
        this.rawCitiesArray = cities;
        this.rawMapArray = fields;
        this.rawGeneralsArray = generals;
        this.players = players;
        this.startData = startData;
        this.attackIndex = attackIndex;
        this.columns = rawMapArray[0];
        this.rows = rawMapArray[1];
        this.fieldsMap = createFieldsMapFromApiArrays();
        this.visibleFieldsMap = GameState.super.getVisibleFieldsMap();
    }


    private ImmutableMap<Position, Field> createFieldsMapFromApiArrays() {
        int size = columns * rows;
        int[] armies = new int[size];
        int[] terrains = new int[size];

        for (int i = 0; i < size; ++i) {
            armies[i] = rawMapArray[i + 2];
            terrains[i] = rawMapArray[size + 2 + i];
        }
        ImmutableMap.Builder<Position, Field> fieldsMapBuilder = new ImmutableMap.Builder<>();
        for (int i = 0; i < size; ++i) {
            Field field = createField(i, armies[i], terrains[i]);
            fieldsMapBuilder.put(field.getPosition(), field);

        }
        return fieldsMapBuilder.build();
    }

    private Field createField(int index, int army, int terrain) {
        Position position = positionFromIndex(index);
        FieldTerrainType terrainType = FieldTerrainType.NUMBER_TO_TERRAIN_TYPE.getOrDefault(terrain, FieldTerrainType.OWNED);
        Optional<Integer> ownerPlayerIndex = terrainType.equals(FieldTerrainType.OWNED) ? Optional.of(terrain) : Optional.empty();
        if (terrainType.equals(FieldTerrainType.FOG) || terrainType.equals(FieldTerrainType.FOG_OBSTACLE)) {
            return new Field(this, position, terrainType);
        } else {
            return new VisibleField(this, position, terrainType, ownerPlayerIndex, army,
                    Arrays.stream(rawCitiesArray).anyMatch(cityIndex -> cityIndex == positionToIndex(position)),
                    Arrays.stream(rawGeneralsArray).anyMatch(generalIndex -> generalIndex == positionToIndex(position))

            );
        }
    }


} public abstract class QueueConfiguration {
    protected final boolean force;

    protected QueueConfiguration(boolean force) {
        this.force = force;
    }

    public abstract void joinQueue(GeneralsApi api, String botUserId);

    public static QueueConfiguration oneVsOne() {
        return new OneVsOneQueueConfiguration();
    }

    /**
     * Joins Free For All game queue.
     *
     * @param force indicates if player should vote for "force start"
     */
    public static QueueConfiguration freeForAll(boolean force) {
        return new FreeForAllQueueConfiguration(force);
    }

    /**
     * Joins 2vs2 game queue.
     *
     * @param force indicates if player should vote for "force start"
     * @param teamId identifier of the team to join (displayed in browser URL when starting 2v2)
     */
    public static QueueConfiguration twoVsTwo(String teamId, boolean force) {
        return new TwoVsTwoQueueConfiguration(teamId, force);
    }

    /**
     * Joins custom game.
     * @param force indicates if player should vote for "force start"
     * @param gameId identifier of the game to join (displayed in browser URL when starting custom game)
     */
    public static QueueConfiguration customGame(boolean force, String gameId) {
        return new CustomGameQueueConfiguration(force, gameId);
    }

    /**
     * Joins custom game.
     * @param force indicates if player should vote for "force start"
     * @param gameId identifier of the game to join (displayed in browser URL when starting custom game)
     * @param team number of team that bot will join in the custom game
     */
    public static QueueConfiguration customGame(boolean force, String gameId, int team) {
        return new CustomGameQueueConfiguration(force, gameId, team);
    }
} public abstract class UserConfiguration {

    private String userId;

    protected UserConfiguration(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }

    abstract public void configureUsername(GeneralsApi api);


    /**
     * Use when name for that ID is already set or you want to remain anonymous.
     * This configuration will not try to set user name.
     */
    public static UserConfiguration onlyUserId(String id) {
        return new OnlyIdUserConfiguration(id);
    }

    /**
     * Use when you want to set the name for the given ID. [Bot] prefix will be added automatically.
     * This configuration will try to set user name. Errors will be ignored, therefore you can keep using this configuration even if the name is already set (it will do nothing).
     */
    public static UserConfiguration idAndName(String id, String name) {
        return new IdAndNameUserConfiguration(id, name);
    }

    /**
     * Equal to {@link UserConfiguration#idAndName(String, String)} but creates a random ID and name.
     * If you want to know the id, call {@link UserConfiguration#getUserId()} on the return value.
     */
    public static UserConfiguration random() {
        return new RandomIdAndNameUserConfiguration();
    }
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.joegreen.sergeants.api.GeneralsApi;
import pl.joegreen.sergeants.api.listener.NoArgsListener;
import pl.joegreen.sergeants.api.response.ChatMessageApiResponse;
import pl.joegreen.sergeants.api.response.GameLostApiResponse;
import pl.joegreen.sergeants.api.response.GameStartApiResponse;
import pl.joegreen.sergeants.api.response.GameUpdateApiResponse;
import pl.joegreen.sergeants.framework.model.*;
import pl.joegreen.sergeants.framework.model.api.GameStartedApiResponseImpl;
import pl.joegreen.sergeants.framework.model.api.UpdatableGameState;
import pl.joegreen.sergeants.framework.queue.QueueConfiguration;
import pl.joegreen.sergeants.framework.user.UserConfiguration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
package pl.joegreen.sergeants.framework;



public class Games {
    private final static Logger LOGGER = LoggerFactory.getLogger(Games.class);
    private static final String HELLO_MESSAGE = "Bot created with the Sergeants framework: https://git.io/sergeants";
    private GeneralsApi api = null;
    private Function<Actions, Bot> botProvider;
    private QueueConfiguration queueConfiguration;
    private UpdatableGameState currentGameState;
    private GameStartApiResponse gameStartApiResponse;
    private Bot bot;
    private Actions actions;
    private int gamesToPlay = 0;
    private UserConfiguration userConfiguration;
    private List<GameResult> gameResults = new ArrayList<>();
    private CompletableFuture<List<GameResult>> gameResultsFuture;

    private boolean inGame;


    public static CompletableFuture<List<GameResult>> playAsynchronously(int games, Function<Actions, Bot> botProvider, QueueConfiguration queueConfiguration, UserConfiguration userConfiguration) {
        if (games <= 0) {
            throw new IllegalArgumentException("Number of games must be positive");
        }
        CompletableFuture<List<GameResult>> resultsFuture = new CompletableFuture<>();
        new Games(GeneralsApi.create(), games, botProvider, queueConfiguration, userConfiguration, resultsFuture).playRound();
        return resultsFuture;
    }

    public static CompletableFuture<List<GameResult>> playAsynchronouslyWithCustomApi(GeneralsApi api, int games, Function<Actions, Bot> botProvider, QueueConfiguration queueConfiguration, UserConfiguration userConfiguration) {
        if (games <= 0) {
            throw new IllegalArgumentException("Number of games must be positive");
        }
        CompletableFuture<List<GameResult>> resultsFuture = new CompletableFuture<>();
        new Games(api, games, botProvider, queueConfiguration, userConfiguration, resultsFuture).playRound();
        return resultsFuture;
    }


    public static List<GameResult> play(int games, Function<Actions, Bot> botProvider, QueueConfiguration queueConfiguration, UserConfiguration userConfiguration) {
        try {
            return playAsynchronously(games, botProvider, queueConfiguration, userConfiguration).get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private Games(GeneralsApi api, int games, Function<Actions, Bot> botProvider, QueueConfiguration queueConfiguration,
                  UserConfiguration userConfiguration, CompletableFuture<List<GameResult>> resultsFuture) {
        this.gamesToPlay = games;
        this.botProvider = botProvider;
        this.userConfiguration = userConfiguration;
        this.queueConfiguration = queueConfiguration;
        this.api = configureApi(api);
        this.actions = new BotActions();
        this.gameResultsFuture = resultsFuture;
    }

    private GeneralsApi configureApi(GeneralsApi api) {
        return api
                .onDisconnected(this::onDisconnected)
                .onGameStarted(this::gameStarted)
                .onGameUpdated(this::onGameUpdated)
                .onGameLost(this::onGameLost)
                .onGameWon(this::onGameWon)
                .onSetUsernameError(LOGGER::error)
                .onChatMessage(this::onChatMessage);
    }

    private void onChatMessage(String room, ChatMessageApiResponse chatMessageApiResponse) {
        if (bot != null) {
            runBotMethodCatchingExceptions(
                    () -> bot.onChatMessage(new ChatMessage(
                            determineRoomType(room, gameStartApiResponse),
                            chatMessageApiResponse.getText(),
                            chatMessageApiResponse.getUsername()
                    ))
            );
        }
    }

    private ChatMessage.ChatType determineRoomType(String room, GameStartApiResponse gameStartApiResponse) {
        if (room.equals(gameStartApiResponse.getChatRoom())) {
            return ChatMessage.ChatType.GAME;
        } else if (room.equals(gameStartApiResponse.getTeamChatRoom())) {
            return ChatMessage.ChatType.TEAM;
        } else {
            return ChatMessage.ChatType.UNKNOWN;
        }
    }

    private void onGameFinished(GameResult gameResult) {
        api.leaveGame();
        if (inGame) {
            inGame = false;
            gamesToPlay--;
            LOGGER.debug("Game finished, {} games left, result: ", gamesToPlay, gameResult);
            runBotMethodCatchingExceptions(() -> bot.onGameFinished(gameResult));
            gameResults.add(gameResult);
            if (gamesToPlay > 0) {
                playRound();
            } else {
                LOGGER.debug("All games finished, disconnecting from the API");
                gameResultsFuture.complete(gameResults);
                api.disconnect();
            }
        }
    }

    private void onDisconnected() {
        GameResult gameResult = new GameResult(GameResult.Result.DISCONNECTED, currentGameState, Optional.empty());
        onGameFinished(gameResult);
    }

    private void onGameWon() {
        GameResult gameResult = new GameResult(GameResult.Result.WON, currentGameState, Optional.empty());
        onGameFinished(gameResult);
    }

    private void onGameLost(GameLostApiResponse gameLostApiResponse) {