mParticipants = null;
+
+ // My participant ID in the currently active game
+ private String mMyId = null;
+
+ private String mHostId = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+
+ // Create the client used to sign in.
+ mGoogleSignInClient = GoogleSignIn.getClient(this, GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);
+
+ // set up a click listener for everything we care about
+ for (int id : CLICKABLES) {
+ findViewById(id).setOnClickListener(this);
+ }
+
+ switchToMainScreen();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume()");
+
+ // Since the state of the signed in user can change when the activity is not active
+ // it is recommended to try and sign in silently from when the app resumes.
+ signInSilently();
+
+ registerReceiver(accelerometerReceiver, new IntentFilter(Service.SENSOR_SERVICE));
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.button_sign_in:
+ // start the sign-in flow
+ Log.d(TAG, "Sign-in button clicked");
+ startSignInIntent();
+ break;
+ case R.id.button_sign_out:
+ // user wants to sign out
+ // sign out.
+ Log.d(TAG, "Sign-out button clicked");
+ signOut();
+ switchToScreen(R.id.screen_sign_in);
+ break;
+ case R.id.button_singleplayer:
+ mMultiplayer = false;
+ startGame();
+ break;
+ case R.id.button_multiplayer:
+ // user wants to play against a random opponent right now
+ mMultiplayer = true;
+ startQuickGame();
+ break;
+ case R.id.close_standings:
+ if (gamesLeft > 0) {
+ startGame();
+ } else {
+ leaveRoom();
+ }
+ break;
+ }
+ }
+
+ private void startQuickGame() {
+ // quick-start a game with 1 randomly selected opponent
+ final int MIN_OPPONENTS = 1, MAX_OPPONENTS = 3;
+ Bundle autoMatchCriteria = RoomConfig.createAutoMatchCriteria(MIN_OPPONENTS,
+ MAX_OPPONENTS, 0);
+ switchToScreen(R.id.screen_wait);
+ keepScreenOn();
+
+ mRoomConfig = RoomConfig.builder(mRoomUpdateCallback)
+ .setOnMessageReceivedListener(mOnRealTimeMessageReceivedListener)
+ .setRoomStatusUpdateCallback(mRoomStatusUpdateCallback)
+ .setAutoMatchCriteria(autoMatchCriteria)
+ .build();
+ mRealTimeMultiplayerClient.create(mRoomConfig);
+ }
+
+ /**
+ * Start a sign in activity. To properly handle the result, call tryHandleSignInResult from
+ * your Activity's onActivityResult function
+ */
+ private void startSignInIntent() {
+ startActivityForResult(mGoogleSignInClient.getSignInIntent(), RC_SIGN_IN);
+ }
+
+ /**
+ * Try to sign in without displaying dialogs to the user.
+ *
+ * If the user has already signed in previously, it will not show dialog.
+ */
+ private void signInSilently() {
+ Log.d(TAG, "signInSilently()");
+
+ mGoogleSignInClient.silentSignIn().addOnCompleteListener(this,
+ new OnCompleteListener() {
+ @Override
+ public void onComplete(@NonNull Task task) {
+ if (task.isSuccessful()) {
+ Log.d(TAG, "signInSilently(): success");
+ onConnected(task.getResult());
+ } else {
+ Log.d(TAG, "signInSilently(): failure", task.getException());
+ onDisconnected();
+ }
+ }
+ });
+ }
+
+ private void signOut() {
+ Log.d(TAG, "signOut()");
+
+ mGoogleSignInClient.signOut().addOnCompleteListener(this,
+ new OnCompleteListener() {
+ @Override
+ public void onComplete(@NonNull Task task) {
+
+ if (task.isSuccessful()) {
+ Log.d(TAG, "signOut(): success");
+ } else {
+ handleException(task.getException(), "signOut() failed!");
+ }
+
+ onDisconnected();
+ }
+ });
+ }
+
+ /**
+ * Since a lot of the operations use tasks, we can use a common handler for whenever one fails.
+ *
+ * @param exception The exception to evaluate. Will try to display a more descriptive reason for the exception.
+ * @param details Will display alongside the exception if you wish to provide more details for why the exception
+ * happened
+ */
+ private void handleException(Exception exception, String details) {
+ int status = 0;
+
+ if (exception instanceof ApiException) {
+ ApiException apiException = (ApiException) exception;
+ status = apiException.getStatusCode();
+ }
+
+ String errorString = null;
+ switch (status) {
+ case GamesCallbackStatusCodes.OK:
+ break;
+ case GamesClientStatusCodes.MULTIPLAYER_ERROR_NOT_TRUSTED_TESTER:
+ errorString = getString(R.string.status_multiplayer_error_not_trusted_tester);
+ break;
+ case GamesClientStatusCodes.MATCH_ERROR_ALREADY_REMATCHED:
+ errorString = getString(R.string.match_error_already_rematched);
+ break;
+ case GamesClientStatusCodes.NETWORK_ERROR_OPERATION_FAILED:
+ errorString = getString(R.string.network_error_operation_failed);
+ break;
+ case GamesClientStatusCodes.INTERNAL_ERROR:
+ errorString = getString(R.string.internal_error);
+ break;
+ case GamesClientStatusCodes.MATCH_ERROR_INACTIVE_MATCH:
+ errorString = getString(R.string.match_error_inactive_match);
+ break;
+ case GamesClientStatusCodes.MATCH_ERROR_LOCALLY_MODIFIED:
+ errorString = getString(R.string.match_error_locally_modified);
+ break;
+ default:
+ errorString = getString(R.string.unexpected_status, GamesClientStatusCodes.getStatusCodeString(status));
+ break;
+ }
+
+ if (errorString == null) {
+ return;
+ }
+
+ String message = getString(R.string.status_exception_error, details, status, exception);
+
+ new AlertDialog.Builder(MainActivity.this)
+ .setTitle("Error")
+ .setMessage(message + "\n" + errorString)
+ .setNeutralButton(android.R.string.ok, null)
+ .show();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+
+ if (requestCode == RC_SIGN_IN) {
+
+ Task task =
+ GoogleSignIn.getSignedInAccountFromIntent(intent);
+
+ try {
+ GoogleSignInAccount account = task.getResult(ApiException.class);
+ onConnected(account);
+ } catch (ApiException apiException) {
+ String message = apiException.getMessage();
+ if (message == null || message.isEmpty()) {
+ message = getString(R.string.signin_other_error);
+ }
+
+ onDisconnected();
+
+ new AlertDialog.Builder(this)
+ .setMessage(message)
+ .setNeutralButton(android.R.string.ok, null)
+ .show();
+ }
+ } else if (requestCode == RC_WAITING_ROOM) {
+ // we got the result from the "waiting room" UI.
+ if (resultCode == Activity.RESULT_OK) {
+ // ready to start playing
+ Log.d(TAG, "Starting game (waiting room returned OK).");
+ startSession();
+ } else if (resultCode == GamesActivityResultCodes.RESULT_LEFT_ROOM) {
+ // player indicated that they want to leave the room
+ leaveRoom();
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ // Dialog was cancelled (user pressed back key, for instance). In our game,
+ // this means leaving the room too. In more elaborate games, this could mean
+ // something else (like minimizing the waiting room UI).
+ leaveRoom();
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, intent);
+ }
+
+ @Override
+ protected void onPause() {
+ unregisterReceiver(accelerometerReceiver);
+
+ super.onPause();
+ }
+
+ // Activity is going to the background. We have to leave the current room.
+ @Override
+ public void onStop() {
+ Log.d(TAG, "**** got onStop");
+
+ // if we're in a room, leave it.
+ leaveRoom();
+
+ // stop trying to keep the screen on
+ stopKeepingScreenOn();
+
+ switchToMainScreen();
+
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (bound) {
+ bound = false;
+ unbindService(connection);
+ }
+
+ super.onDestroy();
+ }
+
+ // Handle back key to make sure we cleanly leave a game if we are in the middle of one
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent e) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && mCurScreen == R.id.screen_game) {
+ leaveRoom();
+ return true;
+ }
+ return super.onKeyDown(keyCode, e);
+ }
+
+ // Leave the room.
+ private void leaveRoom() {
+ Log.d(TAG, "Leaving room.");
+ stopKeepingScreenOn();
+ if (bound) {
+ bound = false;
+ unbindService(connection);
+ }
+ winner = -1;
+ if (mRoomId != null) {
+ mRealTimeMultiplayerClient.leave(mRoomConfig, mRoomId)
+ .addOnCompleteListener(new OnCompleteListener() {
+ @Override
+ public void onComplete(@NonNull Task task) {
+ mRoomId = null;
+ mRoomConfig = null;
+ }
+ });
+ switchToScreen(R.id.screen_wait);
+ } else {
+ switchToMainScreen();
+ }
+ }
+
+ // Show the waiting room UI to track the progress of other players as they enter the
+ // room and get connected.
+ private void showWaitingRoom(Room room) {
+ // minimum number of players required for our game
+ // For simplicity, we require everyone to join the game before we start it
+ // (this is signaled by Integer.MAX_VALUE).
+ final int MIN_PLAYERS = 2;
+ mRealTimeMultiplayerClient.getWaitingRoomIntent(room, MIN_PLAYERS)
+ .addOnSuccessListener(new OnSuccessListener() {
+ @Override
+ public void onSuccess(Intent intent) {
+ // show waiting room UI
+ startActivityForResult(intent, RC_WAITING_ROOM);
+ }
+ })
+ .addOnFailureListener(createFailureListener("There was a problem getting the waiting room!"));
+ }
+
+
+ /*
+ * CALLBACKS SECTION. This section shows how we implement the several games
+ * API callbacks.
+ */
+
+ private String mPlayerId;
+
+ // The currently signed in account, used to check the account has changed outside of this activity when resuming.
+ private GoogleSignInAccount mSignedInAccount = null;
+
+ private void onConnected(GoogleSignInAccount googleSignInAccount) {
+ Log.d(TAG, "onConnected(): connected to Google APIs");
+ if (mSignedInAccount != googleSignInAccount) {
+
+ mSignedInAccount = googleSignInAccount;
+
+ // update the clients
+ mRealTimeMultiplayerClient = Games.getRealTimeMultiplayerClient(this, googleSignInAccount);
+ // get the playerId from the PlayersClient
+ PlayersClient playersClient = Games.getPlayersClient(this, googleSignInAccount);
+ playersClient.getCurrentPlayer()
+ .addOnSuccessListener(new OnSuccessListener() {
+ @Override
+ public void onSuccess(Player player) {
+ mPlayerId = player.getPlayerId();
+
+ switchToMainScreen();
+ }
+ })
+ .addOnFailureListener(createFailureListener("There was a problem getting the player id!"));
+ }
+
+ }
+
+ private OnFailureListener createFailureListener(final String string) {
+ return new OnFailureListener() {
+ @Override
+ public void onFailure(@NonNull Exception e) {
+ handleException(e, string);
+ }
+ };
+ }
+
+ private void onDisconnected() {
+ Log.d(TAG, "onDisconnected()");
+
+ mRealTimeMultiplayerClient = null;
+
+ switchToMainScreen();
+ }
+
+ private RoomStatusUpdateCallback mRoomStatusUpdateCallback = new RoomStatusUpdateCallback() {
+ // Called when we are connected to the room. We're not ready to play yet! (maybe not everybody
+ // is connected yet).
+ @Override
+ public void onConnectedToRoom(Room room) {
+ Log.d(TAG, "onConnectedToRoom.");
+
+ //get participants and my ID:
+ mParticipants = room.getParticipants();
+ mMyId = room.getParticipantId(mPlayerId);
+
+ // save room ID if its not initialized in onRoomCreated() so we can leave cleanly before the game starts.
+ if (mRoomId == null) {
+ mRoomId = room.getRoomId();
+ }
+
+ // print out the list of participants (for debug purposes)
+ Log.d(TAG, "Room ID: " + mRoomId);
+ Log.d(TAG, "My ID " + mMyId);
+ Log.d(TAG, "<< CONNECTED TO ROOM>>");
+ }
+
+ // Called when we get disconnected from the room. We return to the main screen.
+ @Override
+ public void onDisconnectedFromRoom(Room room) {
+ mRoomId = null;
+ mRoomConfig = null;
+ showGameError();
+ }
+
+
+ // We treat most of the room update callbacks in the same way: we update our list of
+ // participants and update the display. In a real game we would also have to check if that
+ // change requires some action like removing the corresponding player avatar from the screen,
+ // etc.
+ @Override
+ public void onPeerDeclined(Room room, @NonNull List arg1) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onPeerInvitedToRoom(Room room, @NonNull List arg1) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onP2PDisconnected(@NonNull String participant) {
+ }
+
+ @Override
+ public void onP2PConnected(@NonNull String participant) {
+ }
+
+ @Override
+ public void onPeerJoined(Room room, @NonNull List arg1) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onPeerLeft(Room room, @NonNull List peersWhoLeft) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onRoomAutoMatching(Room room) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onRoomConnecting(Room room) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onPeersConnected(Room room, @NonNull List peers) {
+ updateRoom(room);
+ }
+
+ @Override
+ public void onPeersDisconnected(Room room, @NonNull List peers) {
+ updateRoom(room);
+ }
+ };
+
+ // Show error message about game being cancelled and return to main screen.
+ private void showGameError() {
+ new AlertDialog.Builder(this)
+ .setMessage(getString(R.string.game_problem))
+ .setNeutralButton(android.R.string.ok, null).create();
+
+ switchToMainScreen();
+ }
+
+ private RoomUpdateCallback mRoomUpdateCallback = new RoomUpdateCallback() {
+
+ // Called when room has been created
+ @Override
+ public void onRoomCreated(int statusCode, Room room) {
+ Log.d(TAG, "onRoomCreated(" + statusCode + ", " + room + ")");
+ if (statusCode != GamesCallbackStatusCodes.OK) {
+ Log.e(TAG, "*** Error: onRoomCreated, status " + statusCode);
+ showGameError();
+ return;
+ }
+
+ // save room ID so we can leave cleanly before the game starts.
+ mRoomId = room.getRoomId();
+
+ // show the waiting room UI
+ showWaitingRoom(room);
+ }
+
+ // Called when room is fully connected.
+ @Override
+ public void onRoomConnected(int statusCode, Room room) {
+ Log.d(TAG, "onRoomConnected(" + statusCode + ", " + room + ")");
+ if (statusCode != GamesCallbackStatusCodes.OK) {
+ Log.e(TAG, "*** Error: onRoomConnected, status " + statusCode);
+ showGameError();
+ return;
+ }
+ updateRoom(room);
+ mHostId = mParticipants.get(0).getParticipantId();
+ }
+
+ @Override
+ public void onJoinedRoom(int statusCode, Room room) {
+ Log.d(TAG, "onJoinedRoom(" + statusCode + ", " + room + ")");
+ if (statusCode != GamesCallbackStatusCodes.OK) {
+ Log.e(TAG, "*** Error: onRoomConnected, status " + statusCode);
+ showGameError();
+ return;
+ }
+
+ // show the waiting room UI
+ showWaitingRoom(room);
+ }
+
+ // Called when we've successfully left the room (this happens a result of voluntarily leaving
+ // via a call to leaveRoom(). If we get disconnected, we get onDisconnectedFromRoom()).
+ @Override
+ public void onLeftRoom(int statusCode, @NonNull String roomId) {
+ // we have left the room; return to main screen.
+ Log.d(TAG, "onLeftRoom, code " + statusCode);
+ switchToMainScreen();
+ }
+ };
+
+ private void updateRoom(Room room) {
+ if (room != null) {
+ if ((mCurScreen == R.id.screen_game || mCurScreen == R.id.game_end) &&
+ mParticipants.size() > room.getParticipants().size()) {
+ winner = -2;
+ leaveRoom();
+ }
+ mParticipants = room.getParticipants();
+ }
+ }
+
+ private RealTimeMultiplayerClient.ReliableMessageSentCallback reliable = new RealTimeMultiplayerClient.ReliableMessageSentCallback() {
+ @Override
+ public void onRealTimeMessageSent(int statusCode, int tokenId, String recipientParticipantId) {
+ Log.d(TAG, "RealTime message sent");
+ Log.d(TAG, " statusCode: " + statusCode);
+ Log.d(TAG, " tokenId: " + tokenId);
+ Log.d(TAG, " recipientParticipantId: " + recipientParticipantId);
+ }
+ };
+
+ /*
+ * GAME LOGIC SECTION. Methods that implement the game's rules.
+ */
+ private ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ PhysicsEngine.EngineBinder binder = (PhysicsEngine.EngineBinder) service;
+ engine = binder.getService();
+ bound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ bound = false;
+ }
+ };
+
+
+
+ public static final long UPDATE_FREQUENCY = 20;
+ private static final int[] COLOR = {Color.RED, Color.BLUE, Color.YELLOW, Color.MAGENTA};
+ private static final String[] COLORNAMES = {"RED", "BLUE", "YELLOW", "PURPLE"};
+ private static final int GAMES = 5;
+ private int gamesLeft;
+ private PhysicsEngine engine;
+ private SQLiteDatabase db;
+ private MapDBManager manager;
+ private volatile boolean bound;
+ private Intent accelerometer;
+ private Draw2D draw;
+ private volatile int mapId;
+ private Map map;
+ private int notReady;
+ private int curStage;
+ private volatile int arrayIndex;
+ private Ball[] balls;
+ private volatile int winner;
+ private int[] wins;
+
+ private void resetVars() {
+ engine = null;
+ db = null;
+ manager = null;
+ bound = false;
+ accelerometer = null;
+ draw = null;
+ mapId = -1;
+ map = null;
+ curStage = 0;
+ balls = null;
+ winner = -1;
+ }
+
+ private BroadcastReceiver accelerometerReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (balls == null) {
+ return;
+ }
+ Ball ball = intent.getParcelableExtra(Ball.class.getName());
+ synchronized (balls[arrayIndex]) {
+ balls[arrayIndex].ax = ball.ax;
+ balls[arrayIndex].ay = ball.ay;
+ }
+ }
+ };
+
+
+ // Start the gameplay phase of the game.
+ private void startSession() {
+ int index = 0;
+ for (Participant p: mParticipants) {
+ if (p.getParticipantId().equals(mMyId)) {
+ arrayIndex = index;
+ }
+ index++;
+ }
+ wins = new int[mParticipants.size()];
+ gamesLeft = GAMES;
+ startGame();
+ }
+
+ private void startGame() {
+ resetVars();
+ if (!mMultiplayer) {
+ gamesLeft = 1;
+ }
+ gameStage(0);
+ }
+
+ private void gameStage(int stage) {
+ if (stage == 0) {
+ ((LinearLayout)findViewById(R.id.screen_game)).removeAllViews();
+ switchToScreen(R.id.screen_game);
+ if (!mMultiplayer || mHostId.equals(mMyId)) {
+ MapDBHandler dbLoader = new MapDBHandler(this);
+ db = dbLoader.getReadableDatabase();
+ manager = new MapDBManager(db);
+ int mapCount = manager.getMapCount();
+ Random random = new Random();
+ mapId = random.nextInt(mapCount);
+
+ if (mMultiplayer) {
+ ByteBuffer buffer = ByteBuffer.allocate(4).putInt(mapId);
+ buffer.flip();
+ byte[] bytes = new byte[4];
+ buffer.get(bytes);
+
+ sendReliableMessageToOthers(bytes);
+ }
+
+ gameStage(++curStage);
+ }
+ } else if (stage == 1) {
+ if (!mMultiplayer || !mHostId.equals(mMyId)) {
+ MapDBHandler dbLoader = new MapDBHandler(this);
+ db = dbLoader.getReadableDatabase();
+ manager = new MapDBManager(db);
+ }
+ map = manager.loadMap(mapId);
+ db.close();
+ if (mMultiplayer) {
+ map.start = new int[mParticipants.size()];
+ } else {
+ map.start = new int[1];
+ }
+ if (!mMultiplayer || mHostId.equals(mMyId)) {
+
+ if (mMultiplayer) {
+ map.pickStartAndEnd(mParticipants.size());
+
+ notReady = mParticipants.size() - 1;
+
+ ByteBuffer buffer = ByteBuffer.allocate((mParticipants.size() + 1) * 4);
+ for (int i = 0; i < mParticipants.size(); i++) {
+ buffer.putInt(map.start[i]);
+ }
+ buffer.putInt(map.end);
+
+ buffer.flip();
+
+ byte[] array = new byte[buffer.remaining()];
+ buffer.get(array);
+ sendReliableMessageToOthers(array);
+ } else {
+ map.pickStartAndEnd(1);
+ }
+
+ balls = new Ball[map.start.length];
+ for (int i = 0; i < map.start.length; i++) {
+ balls[i] = new Ball(map.vertexes[map.start[i]].x, map.vertexes[map.start[i]].y);
+ balls[i].color = COLOR[i];
+ }
+
+ if (!mMultiplayer) {
+ gameStage(++curStage);
+ }
+ }
+ } else if (stage == 2) {
+ if (!mMultiplayer) {
+ PhysicsEngine.map = map;
+ Intent intent = new Intent(MainActivity.this, PhysicsEngine.class);
+ intent.putExtra(Balls.class.getName(), new Balls(balls));
+ bindService(intent, connection, Context.BIND_AUTO_CREATE);
+ }
+ if (!mMultiplayer || mHostId.equals(mMyId)) {
+ GameCycleThread server = new GameCycleThread();
+ server.start();
+ }
+ accelerometer = new Intent(this, AccelerometerService.class);
+ accelerometer.putExtra(Ball.class.getName(), balls[arrayIndex]);
+ startService(accelerometer);
+ ViewThread view = new ViewThread();
+ view.start();
+ }
+ }
+
+ class ViewThread implements Runnable {
+
+ private void start() {
+ draw = new Draw2D(MainActivity.this);
+ draw.map = map;
+ draw.balls = balls;
+ draw.index = arrayIndex;
+ ((LinearLayout)findViewById(R.id.screen_game)).addView(draw);
+ Thread worker = new Thread(this);
+ worker.start();
+ }
+
+ @Override
+ public void run() {
+ while (winner == -1) {
+ long time = System.currentTimeMillis();
+
+ if (draw == null) {
+ break;
+ }
+
+ for (int i = 0; i < balls.length; i++) {
+ synchronized (balls[i]) {
+ draw.balls[i] = new Ball(balls[i]);
+ }
+ }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (draw != null) {
+ draw.invalidate();
+ }
+ }
+ });
+
+ if (mMultiplayer && !mHostId.equals(mMyId)) {
+ byte[] bytes = new byte[Ball.SIZE + 4];
+ ByteBuffer buffer = ByteBuffer.allocate(Ball.SIZE + 4).putInt(arrayIndex);
+ byte[] ballBytes;
+ synchronized (balls[arrayIndex]) {
+ ballBytes = balls[arrayIndex].write();
+ }
+ buffer.put(ballBytes);
+ buffer.flip();
+ buffer.get(bytes);
+ mRealTimeMultiplayerClient.sendUnreliableMessage(bytes, mRoomId, mHostId);
+ }
+
+ long cycleTime = System.currentTimeMillis() - time;
+ if (cycleTime < UPDATE_FREQUENCY) {
+ try {
+ Thread.sleep(UPDATE_FREQUENCY - cycleTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ gamesLeft--;
+ if (!mMultiplayer) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ switchToMainScreen();
+ }
+ });
+ return;
+ }
+ wins[winner]++;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showStandings();
+ }
+ });
+ }
+ }
+
+
+ class GameCycleThread implements Runnable {
+
+ private void start() {
+ Thread worker = new Thread(this);
+ worker.start();
+ }
+
+ @Override
+ public void run() {
+ while (winner == -1) {
+ long time = System.currentTimeMillis();
+
+ if (bound) {
+ Ball[] ballsEngine = new Ball[map.start.length];
+ for (int i = 0; i < map.start.length; i++) {
+ Ball ball = engine.getBall(i);
+ if (ball.color == -1) {
+ winner = i;
+
+ if (mMultiplayer) {
+ byte[] array = new byte[4];
+ ByteBuffer buffer = ByteBuffer.allocate(4).putInt(i);
+ buffer.flip();
+ buffer.get(array);
+ sendReliableMessageToOthers(array);
+ }
+ if (bound) {
+ bound = false;
+ unbindService(connection);
+ }
+ return;
+ }
+ ballsEngine[i] = ball;
+ }
+
+ if (mMultiplayer) {
+ mRealTimeMultiplayerClient.sendUnreliableMessageToOthers(Ball.toByteArray(ballsEngine), mRoomId);
+ }
+
+ synchronized (balls[arrayIndex]) {
+ double ax = balls[arrayIndex].ax;
+ double ay = balls[arrayIndex].ay;
+ balls = ballsEngine;
+ balls[arrayIndex].ax = ax;
+ balls[arrayIndex].ay = ay;
+ engine.updateBall(arrayIndex, balls[arrayIndex]);
+ }
+
+ }
+
+ long cycleTime = System.currentTimeMillis() - time;
+ if (cycleTime < UPDATE_FREQUENCY) {
+ try {
+ Thread.sleep(UPDATE_FREQUENCY - cycleTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
+
+ /*
+ * COMMUNICATIONS SECTION. Methods that implement the game's network
+ * protocol.
+ */
+
+
+ // Called when we receive a real-time message from the network.
+ // Messages in our game are made up of 2 bytes: the first one is 'F' or 'U'
+ // indicating
+ // whether it's a final or interim score. The second byte is the score.
+ // There is also the
+ // 'S' message, which indicates that the game should start.
+ private OnRealTimeMessageReceivedListener mOnRealTimeMessageReceivedListener = new OnRealTimeMessageReceivedListener() {
+ @Override
+ public void onRealTimeMessageReceived(@NonNull RealTimeMessage realTimeMessage) {
+ if (winner != -1) {
+ return;
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(realTimeMessage.getMessageData().length);
+ buffer.put(realTimeMessage.getMessageData());
+ buffer.flip();
+ if (realTimeMessage.isReliable()) {
+ if (curStage == 0) {
+ mapId = buffer.getInt();
+ gameStage(++curStage);
+ } else if (curStage == 1) {
+ if (!mHostId.equals(mMyId)) {
+ if (buffer.remaining() > 1) {
+ balls = new Ball[mParticipants.size()];
+ for (int i = 0; i < map.start.length; i++) {
+ map.start[i] = buffer.getInt();
+ balls[i] = new Ball(map.vertexes[map.start[i]].x, map.vertexes[map.start[i]].y);
+ }
+ map.end = buffer.getInt();
+ byte[] bytes = new byte[1];
+ mRealTimeMultiplayerClient.sendReliableMessage(bytes, mRoomId, mHostId, reliable);
+ } else {
+ gameStage(++curStage);
+ }
+ } else {
+ notReady--;
+ if (notReady == 0) {
+ PhysicsEngine.map = map;
+ Intent intent = new Intent(MainActivity.this, PhysicsEngine.class);
+ intent.putExtra(Balls.class.getName(), new Balls(balls));
+ bindService(intent, connection, Context.BIND_AUTO_CREATE);
+ byte[] bytes = new byte[1];
+ sendReliableMessageToOthers(bytes);
+ gameStage(++curStage);
+ }
+ }
+ } else if (curStage == 2) {
+ if (mHostId.equals(mMyId)) {
+ if (bound) {
+ bound = false;
+ unbindService(connection);
+ }
+ }
+ stopService(accelerometer);
+ winner = buffer.getInt();
+ }
+ } else {
+ if (!mHostId.equals(mMyId)) {
+ synchronized (balls[arrayIndex]) {
+ double ax = balls[arrayIndex].ax;
+ double ay = balls[arrayIndex].ay;
+ balls = Ball.fromByteArray(realTimeMessage.getMessageData());
+ balls[arrayIndex].ax = ax;
+ balls[arrayIndex].ay = ay;
+ }
+ } else {
+ int index = buffer.getInt();
+ Ball ball = new Ball();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ ball.read(bytes);
+ if (engine != null) {
+ engine.updateBall(index, ball);
+ }
+ }
+ }
+ }
+ };
+
+ private void sendReliableMessageToOthers(byte[] bytes) {
+ for (Participant p: mParticipants) {
+ if (p.getStatus() == Participant.STATUS_JOINED && !p.getParticipantId().equals(mMyId)) {
+ mRealTimeMultiplayerClient.sendReliableMessage(bytes, mRoomId, p.getParticipantId(), reliable);
+ }
+ }
+ }
+
+ /*
+ * UI SECTION. Methods that implement the game's UI.
+ */
+
+ // This array lists everything that's clickable, so we can install click
+ // event handlers.
+ private final static int[] CLICKABLES = {
+ R.id.button_singleplayer,
+ R.id.button_multiplayer,
+ R.id.button_sign_in,
+ R.id.button_sign_out,
+ R.id.close_standings
+ };
+
+ // This array lists all the individual screens our game has.
+ private final static int[] SCREENS = {
+ R.id.screen_game, R.id.screen_main, R.id.screen_sign_in,
+ R.id.screen_wait, R.id.game_end
+ };
+ private int mCurScreen = -1;
+
+ private void switchToScreen(int screenId) {
+ // make the requested screen visible; hide all others.
+ for (int id : SCREENS) {
+ findViewById(id).setVisibility(screenId == id ? View.VISIBLE : View.GONE);
+ }
+ mCurScreen = screenId;
+ }
+
+ private void switchToMainScreen() {
+ if (mRealTimeMultiplayerClient != null) {
+ switchToScreen(R.id.screen_main);
+ } else {
+ switchToScreen(R.id.screen_sign_in);
+ }
+ }
+
+ private void showStandings() {
+ List indexes = new ArrayList<>();
+ for (int i = 0; i < mParticipants.size(); i++) {
+ indexes.add(i);
+ }
+ Collections.sort(indexes, new Comparator() {
+ @Override
+ public int compare(Integer a, Integer b) {
+ return wins[b] - wins[a];
+ }
+ });
+
+ List- list = new ArrayList<>();
+ ListView listView = findViewById(R.id.standings);
+ Standings standings = new Standings(this, list);
+ listView.setAdapter(standings);
+
+ for (int i: indexes) {
+ Item item = new Item(COLORNAMES[i], String.valueOf(wins[i]));
+ list.add(item);
+ }
+
+ standings.notifyDataSetChanged();
+ switchToScreen(R.id.game_end);
+ }
+
+ /*
+ * MISC SECTION. Miscellaneous methods.
+ */
+
+
+ // Sets the flag to keep this screen on. It's recommended to do that during
+ // the
+ // handshake when setting up a game, because if the screen turns off, the
+ // game will be
+ // cancelled.
+ private void keepScreenOn() {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ // Clears the flag that keeps the screen on.
+ private void stopKeepingScreenOn() {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+}
diff --git a/app/src/main/java/ru/hse/throughthemaze/database/MapDBHandler.java b/app/src/main/java/ru/hse/throughthemaze/database/MapDBHandler.java
new file mode 100644
index 0000000..033171a
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/database/MapDBHandler.java
@@ -0,0 +1,72 @@
+package ru.hse.throughthemaze.database;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import java.io.*;
+
+public class MapDBHandler extends SQLiteOpenHelper {
+
+ private static final String ASSETS_PATH = "databases";
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "maps";
+ private Context context;
+ private SharedPreferences preferences;
+
+ private boolean installedDatabaseIsOutdated() {
+ return preferences.getInt(DATABASE_NAME, 0) < DATABASE_VERSION;
+ }
+
+ private void writeDatabaseVersionInPreferences() {
+ preferences.edit().putInt(DATABASE_NAME, DATABASE_VERSION).apply();
+ }
+
+ private synchronized void installOrUpdateIfNecessary() {
+ if (installedDatabaseIsOutdated()) {
+ context.deleteDatabase(DATABASE_NAME);
+ installDatabaseFromAssets();
+ writeDatabaseVersionInPreferences();
+ }
+ }
+
+ private void installDatabaseFromAssets() {
+ try (InputStream in = context.getAssets().open(ASSETS_PATH + "/" + DATABASE_NAME + ".sqlite3")) {
+ File output = new File(context.getDatabasePath(DATABASE_NAME).getPath());
+ try (OutputStream out = new FileOutputStream(output)) {
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("The " + DATABASE_NAME + " couldn't be installed", e);
+ }
+ }
+
+ public MapDBHandler(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ this.context = context;
+ preferences = context.getSharedPreferences(
+ context.getPackageName() + ".database_versions", Context.MODE_PRIVATE);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {}
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ throw new RuntimeException("The " + DATABASE_NAME + " database is not writable");
+ }
+
+ @Override
+ public SQLiteDatabase getReadableDatabase() {
+ installOrUpdateIfNecessary();
+ return super.getReadableDatabase();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/database/MapDBManager.java b/app/src/main/java/ru/hse/throughthemaze/database/MapDBManager.java
new file mode 100644
index 0000000..83099af
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/database/MapDBManager.java
@@ -0,0 +1,96 @@
+package ru.hse.throughthemaze.database;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import ru.hse.throughthemaze.map.Map;
+import ru.hse.throughthemaze.map.Vertex;
+import org.sqlite.JDBC;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MapDBManager {
+
+ private SQLiteDatabase database;
+ private static Connection connection;
+ private static final String CON_STR = "jdbc:sqlite:app/src/main/assets/databases/maps.sqlite3";
+
+ public MapDBManager(SQLiteDatabase database) {
+ this.database = database;
+ }
+
+ public Map loadMap(int mapId) {
+ Map map = null;
+ try (Cursor cursor = database.rawQuery("SELECT * FROM Vertexes" + mapId, null)) {
+ map = new Map(cursor.getCount());
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(0);
+ double x = cursor.getDouble(1);
+ double y = cursor.getDouble(2);
+ map.vertexes[id] = new Vertex(x, y);
+ }
+ }
+ try (Cursor cursor = database.rawQuery("SELECT * FROM Edges" + mapId, null)) {
+ while (cursor.moveToNext()) {
+ int i = cursor.getInt(0);
+ int j = cursor.getInt(1);
+ map.edges.get(i).add(j);
+ }
+ }
+ return map;
+ }
+
+ public int getMapCount() {
+ String query = "SELECT * FROM sqlite_master WHERE type='table'" +
+ "AND name != 'android_metadata' AND name != 'sqlite_sequence'";
+ try (Cursor cursor = database.rawQuery(query, null)) {
+ return cursor.getCount() / 2;
+ }
+ }
+
+ public static void createOrClear() throws SQLException {
+ DriverManager.registerDriver(new JDBC());
+ connection = DriverManager.getConnection(CON_STR);
+ ResultSet res = connection.getMetaData().getTables(
+ null, null, "%", null);
+ List names = new ArrayList<>();
+ while (res.next()) {
+ names.add(res.getString(3));
+ }
+ res.close();
+ for (String name: names) {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("DROP TABLE " + name);
+ }
+ }
+ }
+
+ public static void addMap(Map map, int id) throws SQLException {
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE Vertexes" + id + "(ID INTEGER, x FLOAT, y FLOAT)");
+ }
+ for (int i = 0; i < map.size; i++) {
+ try (PreparedStatement statement = connection.prepareStatement(
+ "INSERT INTO Vertexes" + id + " VALUES (?, ?, ?)")) {
+ statement.setInt(1, i);
+ statement.setDouble(2, map.vertexes[i].x);
+ statement.setDouble(3, map.vertexes[i].y);
+ statement.execute();
+ }
+ }
+ try (Statement statement = connection.createStatement()) {
+ statement.execute("CREATE TABLE Edges" + id + "(v INTEGER, u INTEGER)");
+ }
+ for (int i = 0; i < map.size; i++) {
+ for (int j: map.edges.get(i)) {
+ try (PreparedStatement statement = connection.prepareStatement(
+ "INSERT INTO Edges" + id + " VALUES (?, ?)")) {
+ statement.setInt(1, i);
+ statement.setInt(2, j);
+ statement.execute();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/gameplay/Ball.java b/app/src/main/java/ru/hse/throughthemaze/gameplay/Ball.java
new file mode 100644
index 0000000..07160a7
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/gameplay/Ball.java
@@ -0,0 +1,122 @@
+package ru.hse.throughthemaze.gameplay;
+
+import android.graphics.Color;
+import android.os.Parcel;
+import android.os.Parcelable;
+import ru.hse.throughthemaze.map.Map;
+
+import java.nio.ByteBuffer;
+
+public class Ball implements Parcelable {
+ public static final int SIZE = 52;
+ public static final double RADIUS = Map.CORRIDOR_WIDTH / 2;
+ public double x;
+ public double y;
+ public double vx;
+ public double vy;
+ public double ax;
+ public double ay;
+ public int color;
+
+ public Ball() {}
+
+ public Ball(double x, double y) {
+ this.x = x;
+ this.y = y;
+ vx = 0;
+ vy = 0;
+ ax = 0;
+ ay = 0;
+ color = Color.RED;
+ }
+
+ public Ball(Ball other) {
+ x = other.x;
+ y = other.y;
+ vx = other.vx;
+ vy = other.vy;
+ ax = other.ax;
+ ay = other.ay;
+ color = other.color;
+ }
+
+ public Ball(Parcel in) {
+ byte[] data = new byte[SIZE];
+ in.readByteArray(data);
+ read(data);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(write());
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+
+ @Override
+ public Ball createFromParcel(Parcel source) {
+ return new Ball(source);
+ }
+
+ @Override
+ public Ball[] newArray(int size) {
+ return new Ball[size];
+ }
+ };
+
+ public byte[] write() {
+ ByteBuffer out = ByteBuffer.allocate(SIZE);
+ out.putDouble(x);
+ out.putDouble(y);
+ out.putDouble(vx);
+ out.putDouble(vy);
+ out.putDouble(ax);
+ out.putDouble(ay);
+ out.putInt(color);
+ out.flip();
+ byte[] bytes = new byte[out.remaining()];
+ out.get(bytes);
+ return bytes;
+ }
+
+ public void read(byte[] data) {
+ ByteBuffer buffer = ByteBuffer.allocate(data.length).put(data);
+ buffer.flip();
+ x = buffer.getDouble();
+ y = buffer.getDouble();
+ vx = buffer.getDouble();
+ vy = buffer.getDouble();
+ ax = buffer.getDouble();
+ ay = buffer.getDouble();
+ color = buffer.getInt();
+ }
+
+ public static byte[] toByteArray(Ball[] balls) {
+ ByteBuffer buffer = ByteBuffer.allocate(SIZE * balls.length);
+ for (Ball ball: balls) {
+ buffer.put(ball.write());
+ }
+ buffer.flip();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ public static Ball[] fromByteArray(byte[] array) {
+ ByteBuffer buffer = ByteBuffer.allocate(array.length).put(array);
+ buffer.flip();
+ Ball[] res = new Ball[buffer.remaining() / SIZE];
+ for (int i = 0; i < res.length; i++) {
+ res[i] = new Ball();
+ byte[] bytes = new byte[SIZE];
+ buffer.get(bytes);
+ res[i].read(bytes);
+ }
+ return res;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/gameplay/Balls.java b/app/src/main/java/ru/hse/throughthemaze/gameplay/Balls.java
new file mode 100644
index 0000000..5cae72e
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/gameplay/Balls.java
@@ -0,0 +1,39 @@
+package ru.hse.throughthemaze.gameplay;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Balls implements Parcelable {
+
+ public Ball[] balls;
+
+ public Balls(Ball[] balls) {
+ this.balls = balls;
+ }
+
+ protected Balls(Parcel in) {
+ balls = in.createTypedArray(Ball.CREATOR);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Balls createFromParcel(Parcel in) {
+ return new Balls(in);
+ }
+
+ @Override
+ public Balls[] newArray(int size) {
+ return new Balls[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedArray(balls, 0);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/gameplay/PhysicsEngine.java b/app/src/main/java/ru/hse/throughthemaze/gameplay/PhysicsEngine.java
new file mode 100644
index 0000000..6a09f4f
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/gameplay/PhysicsEngine.java
@@ -0,0 +1,171 @@
+package ru.hse.throughthemaze.gameplay;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import ru.hse.throughthemaze.MainActivity;
+import ru.hse.throughthemaze.map.Map;
+import ru.hse.throughthemaze.map.Segment;
+import ru.hse.throughthemaze.map.Vertex;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class PhysicsEngine extends Service {
+
+ public class EngineBinder extends Binder {
+ public PhysicsEngine getService() {
+ return PhysicsEngine.this;
+ }
+ }
+
+ private final IBinder binder = new EngineBinder();
+ public static Map map;
+ private Ball[] balls;
+ private Timer timer;
+
+ private synchronized void passTime(long time) {
+ for (Ball ball: balls) {
+ double t = (double)time / 1000;
+ ball.x += ball.vx * t + ball.ax * t * t / 2;
+ ball.y += ball.vy * t + ball.ay * t * t / 2;
+ ball.vx *= 0.99;
+ ball.vy *= 0.99;
+ ball.vx += ball.ax * t;
+ ball.vy += ball.ay * t;
+ Vertex curPoint = new Vertex(ball.x, ball.y);
+ Vertex end = new Vertex(map.vertexes[map.end].x, map.vertexes[map.end].y);
+ if (end.dist(curPoint) + Ball.RADIUS < Map.VERTEX_RADIUS) {
+ ball.color = -1;
+ }
+ }
+ checkForClash();
+ }
+
+ private void checkForClash() {
+ for (int k = 0; k < balls.length; k++) {
+ Ball ball = balls[k];
+ Vertex curPoint = new Vertex(ball.x, ball.y);
+ for (int j = k + 1; j < balls.length; j++) {
+ Vertex secondPoint = new Vertex(balls[j].x, balls[j].y);
+ if (curPoint.dist(secondPoint) < 2 * Ball.RADIUS) {
+ processClash(ball, balls[j]);
+ }
+ }
+ boolean next = false;
+ for (int i = 0; i < map.size; i++) {
+ Vertex v = map.vertexes[i];
+ if (v.dist(curPoint) < Map.VERTEX_RADIUS) {
+ if (Map.VERTEX_RADIUS < Ball.RADIUS + v.dist(curPoint)) {
+ boolean inCorridor = false;
+ for (int j : map.edges.get(i)) {
+ Vertex a = map.vertexes[i];
+ Vertex b = map.vertexes[j];
+ Segment l = new Segment(a, b);
+ if (l.dist(curPoint) < Map.CORRIDOR_WIDTH) {
+ inCorridor = true;
+ break;
+ }
+ }
+ if (inCorridor) {
+ break;
+ }
+ double deltax = curPoint.x - v.x;
+ double deltay = curPoint.y - v.y;
+ double multiplier = Map.VERTEX_RADIUS / v.dist(curPoint);
+ deltax *= multiplier;
+ deltay *= multiplier;
+ Vertex onBorder = new Vertex(v.x + deltax, v.y + deltay);
+ processClash(new Segment(onBorder, new Vertex(onBorder.x - deltay, onBorder.y + deltax)), ball);
+ }
+ next = true;
+ break;
+ }
+ }
+ if (next) {
+ continue;
+ }
+ for (int i = 0; i < map.size; i++) {
+ for (int j : map.edges.get(i)) {
+ Vertex a = map.vertexes[i];
+ Vertex b = map.vertexes[j];
+ Segment l = new Segment(a, b);
+ if (l.dist(curPoint) < Map.CORRIDOR_WIDTH) {
+ if (Map.CORRIDOR_WIDTH < l.dist(curPoint) + Map.CORRIDOR_WIDTH / 2) {
+ Segment border = l.move(Map.CORRIDOR_WIDTH);
+ if (border.dist(curPoint) < Map.CORRIDOR_WIDTH / 2) {
+ processClash(border, ball);
+ } else {
+ processClash(l.move(-Map.CORRIDOR_WIDTH), ball);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void processClash(Segment l, Ball ball) {
+ Vertex curPoint = new Vertex(ball.x, ball.y);
+ Vertex futurePoint = new Vertex(ball.x + ball.vx, ball.y + ball.vy);
+ if (l.side(curPoint) + l.side(futurePoint) != 1 && l.dist(curPoint) < l.dist(futurePoint)) {
+ return;
+ }
+ double angle = Math.atan2(ball.vy, ball.vx) - Math.atan2(l.b.y - l.a.y, l.b.x - l.a.x);
+ if (angle < 0) {
+ angle += 2 * Math.PI;
+ }
+ if (angle > Math.PI) {
+ angle -= Math.PI;
+ }
+ angle *= -2;
+ double newvx = Math.cos(angle) * ball.vx - Math.sin(angle) * ball.vy;
+ double newvy = Math.cos(angle) * ball.vy + Math.sin(angle) * ball.vx;
+ ball.vx = newvx;
+ ball.vy = newvy;
+ }
+
+ private void processClash(Ball a, Ball b) {
+ double vx = a.vx;
+ double vy = a.vy;
+ a.vx = b.vx / 2;
+ a.vy = b.vy / 2;
+ b.vx = vx / 2;
+ b.vy = vy / 2;
+ }
+
+ public synchronized void updateBall(int i, Ball newBall) {
+ balls[i].ax = newBall.ax;
+ balls[i].ay = newBall.ay;
+ }
+
+ public synchronized Ball getBall(int i) {
+ return new Ball(balls[i]);
+ }
+
+ @org.jetbrains.annotations.Nullable
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ Balls balls = intent.getParcelableExtra(Balls.class.getName());
+ this.balls = balls.balls;
+ timer = new Timer();
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ passTime(MainActivity.UPDATE_FREQUENCY / 10);
+ }
+ };
+ timer.schedule(task, 0, MainActivity.UPDATE_FREQUENCY / 10);
+ return binder;
+ }
+
+ @Override
+ public void onDestroy() {
+ timer.cancel();
+ super.onDestroy();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/map/Map.java b/app/src/main/java/ru/hse/throughthemaze/map/Map.java
new file mode 100644
index 0000000..7bd98e6
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/map/Map.java
@@ -0,0 +1,123 @@
+package ru.hse.throughthemaze.map;
+
+import java.util.*;
+
+public class Map {
+ public static final double VERTEX_RADIUS = 1;
+ public static final double CORRIDOR_WIDTH = VERTEX_RADIUS / 4;
+ public static final double BORDER = 200;
+ public int size;
+ public Vertex[] vertexes;
+ public ArrayList> edges;
+ public int[] start;
+ public int end;
+
+ public Map(int size) {
+ this.size = size;
+ vertexes = new Vertex[size];
+ edges = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ edges.add(new TreeSet());
+ }
+ }
+
+ public void generate() {
+ generateVertexes();
+ addSomeEdges();
+ }
+
+ private void generateVertexes() {
+ while (true) {
+ for (int i = 0; i < size; i++) {
+ vertexes[i] = new Vertex(Math.random() * BORDER, Math.random() * BORDER);
+ }
+ boolean correct = true;
+ for (int i = 0; i < size; i++) {
+ for (int j = i + 1; j < size; j++) {
+ if (vertexes[i].dist(vertexes[j]) < 2 * VERTEX_RADIUS) {
+ correct = false;
+ }
+ }
+ }
+ if (correct) {
+ break;
+ }
+ }
+ }
+
+ private boolean intersects(Vertex a, Vertex b, Vertex c) {
+ Segment p = new Segment(a, b);
+ if (p.dist(c) < VERTEX_RADIUS + CORRIDOR_WIDTH) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean intersects(Vertex a, Vertex b, Vertex c, Vertex d) {
+ Segment p = new Segment(a, b);
+ Segment q = new Segment(c, d);
+ int sum = 0;
+ sum += p.side(c);
+ sum += p.side(d);
+ if (sum != 1) {
+ return false;
+ }
+ sum += q.side(a);
+ sum += q.side(b);
+ return (sum == 2);
+ }
+
+ private void addSomeEdges() {
+ ArrayList edgesToAdd = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ for (int j = i + 1; j < size; j++) {
+ if (!edges.get(i).contains(j)) {
+ edgesToAdd.add(i * size + j);
+ }
+ }
+ }
+ Collections.shuffle(edgesToAdd);
+ for (int e: edgesToAdd) {
+ int i = e / size;
+ int j = e % size;
+ if ((edges.get(i).size() > 4 || edges.get(j).size() > 4) && !edges.get(i).isEmpty() && !edges.get(j).isEmpty()) {
+ continue;
+ }
+ boolean notIntersects = true;
+ for (int k = 0; k < size; k++) {
+ if (k == i || k == j) {
+ continue;
+ }
+ if (intersects(vertexes[i], vertexes[j], vertexes[k])) {
+ notIntersects = false;
+ break;
+ }
+ for (int l: edges.get(k)) {
+ if (l == i || l == j) {
+ continue;
+ }
+ if (intersects(vertexes[i], vertexes[j], vertexes[k], vertexes[l])) {
+ notIntersects = false;
+ break;
+ }
+ }
+ }
+ if (notIntersects) {
+ edges.get(i).add(j);
+ edges.get(j).add(i);
+ }
+ }
+ }
+
+ public void pickStartAndEnd(int n) {
+ ArrayList starts = new ArrayList<>();
+ for (int i = 0; i <= n; i++) {
+ starts.add(i);
+ }
+ Collections.shuffle(starts);
+ for (int i = 0; i < n; i++) {
+ start[i] = starts.get(i);
+ }
+ end = starts.get(n);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/map/MapGenerator.java b/app/src/main/java/ru/hse/throughthemaze/map/MapGenerator.java
new file mode 100644
index 0000000..aca37e3
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/map/MapGenerator.java
@@ -0,0 +1,18 @@
+package ru.hse.throughthemaze.map;
+
+import ru.hse.throughthemaze.database.MapDBManager;
+
+import java.sql.SQLException;
+
+public class MapGenerator {
+ public static void main(String[] args) throws SQLException {
+ MapDBManager.createOrClear();
+ int numberOfMaps = Integer.parseInt(args[0]);
+ int size = Integer.parseInt(args[1]);
+ for (int i = 0; i < numberOfMaps; i++) {
+ Map map = new Map(size);
+ map.generate();
+ MapDBManager.addMap(map, i);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/map/Segment.java b/app/src/main/java/ru/hse/throughthemaze/map/Segment.java
new file mode 100644
index 0000000..7718cde
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/map/Segment.java
@@ -0,0 +1,40 @@
+package ru.hse.throughthemaze.map;
+
+public class Segment {
+ private static final double EPS = 1e-8;
+ public Vertex a;
+ public Vertex b;
+
+ public Segment(Vertex a, Vertex b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ public double dist(Vertex c) {
+ double scalar1 = (c.x - a.x) * (b.x - a.x) + (c.y - a.y) * (b.y - a.y);
+ double scalar2 = (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y);
+ if (scalar1 < EPS) {
+ return a.dist(c);
+ }
+ if (scalar2 < scalar1 + EPS) {
+ return b.dist(c);
+ }
+ double coefficient = scalar1 / scalar2;
+ return c.dist(new Vertex(a.x + coefficient * (b.x - a.x), a.y + coefficient * (b.y - a.y)));
+ }
+
+ public int side(Vertex c) {
+ double vector = (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x);
+ if (vector > EPS) {
+ return 1;
+ }
+ return 0;
+ }
+
+ public Segment move(double delta) {
+ Vertex v = new Vertex(a.y - b.y, b.x - a.x);
+ v.x *= delta / a.dist(b);
+ v.y *= delta / a.dist(b);
+ return new Segment(new Vertex(a.x + v.x, a.y + v.y), new Vertex(b.x + v.x, b.y + v.y));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/map/Vertex.java b/app/src/main/java/ru/hse/throughthemaze/map/Vertex.java
new file mode 100644
index 0000000..83aefea
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/map/Vertex.java
@@ -0,0 +1,29 @@
+package ru.hse.throughthemaze.map;
+
+import java.util.Comparator;
+
+public class Vertex {
+ public static Comparator compareX = new Comparator() {
+ @Override
+ public int compare(Vertex o1, Vertex o2) {
+ return Double.compare(o2.x, o1.x);
+ }
+ };
+
+ public double x;
+ public double y;
+
+ public Vertex(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public double dist(Vertex other) {
+ return Math.sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
+ }
+
+ public Vertex midTo(Vertex other) {
+ return new Vertex((x + other.x) / 2, (y + other.y) / 2);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/view/Draw2D.java b/app/src/main/java/ru/hse/throughthemaze/view/Draw2D.java
new file mode 100644
index 0000000..5ebe3fb
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/view/Draw2D.java
@@ -0,0 +1,91 @@
+package ru.hse.throughthemaze.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import ru.hse.throughthemaze.R;
+import ru.hse.throughthemaze.gameplay.Ball;
+import ru.hse.throughthemaze.map.Map;
+import ru.hse.throughthemaze.map.Vertex;
+
+public class Draw2D extends View {
+
+ public Draw2D(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public Draw2D(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public Draw2D(Context context) {
+ super(context);
+ }
+
+ private static final double SCALE = 80;
+
+ private Paint paint = new Paint();
+ public Map map;
+ public int index;
+ public Ball[] balls;
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (balls == null) {
+ return;
+ }
+ double x = balls[index].x;
+ double y = balls[index].y;
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(Color.GREEN);
+ canvas.drawPaint(paint);
+ double width = getWidth();
+ double height = getHeight();
+ width /= 2;
+ height /= 2;
+ canvas.save();
+ canvas.translate((float)width, (float)height);
+ canvas.scale((float)SCALE, (float)SCALE);
+ paint.setColor(Color.WHITE);
+ for (Vertex v: map.vertexes) {
+ double dx = v.x - x;
+ double dy = v.y - y;
+ canvas.drawCircle((float)dx, (float)dy, (float)Map.VERTEX_RADIUS, paint);
+ }
+ for (int i = 0; i < map.size; i++) {
+ for (int j: map.edges.get(i)) {
+ if (i > j) {
+ continue;
+ }
+ Vertex a = map.vertexes[i];
+ Vertex b = map.vertexes[j];
+ double dist = a.dist(b);
+ double ax = a.x - x;
+ double ay = a.y - y;
+ double bx = b.x - x;
+ double by = b.y - y;
+ double angle = Math.atan2(by - ay, bx - ax);
+ canvas.save();
+ canvas.translate((float)ax, (float)ay);
+ canvas.rotate((float)Math.toDegrees(angle));
+ canvas.drawRect(0, (float)(-Map.CORRIDOR_WIDTH),
+ (float)(dist), (float)(Map.CORRIDOR_WIDTH), paint);
+ canvas.restore();
+ }
+ }
+ paint.setColor(Color.BLACK);
+ canvas.drawCircle((float)(map.vertexes[map.end].x - x), (float)(map.vertexes[map.end].y - y),
+ (float)Map.VERTEX_RADIUS, paint);
+ for (Ball ball: balls) {
+ paint.setColor(ball.color);
+ canvas.drawCircle((float)(ball.x - x), (float)(ball.y - y), (float)Ball.RADIUS, paint);
+ }
+ canvas.restore();
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/view/Item.java b/app/src/main/java/ru/hse/throughthemaze/view/Item.java
new file mode 100644
index 0000000..1481a34
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/view/Item.java
@@ -0,0 +1,15 @@
+package ru.hse.throughthemaze.view;
+
+public class Item {
+
+ public String color;
+ public String wins;
+
+ public Item() {}
+
+ public Item(String color, String wins) {
+ this.color = color;
+ this.wins = wins;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hse/throughthemaze/view/Standings.java b/app/src/main/java/ru/hse/throughthemaze/view/Standings.java
new file mode 100644
index 0000000..4b1ef15
--- /dev/null
+++ b/app/src/main/java/ru/hse/throughthemaze/view/Standings.java
@@ -0,0 +1,68 @@
+package ru.hse.throughthemaze.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+import ru.hse.throughthemaze.R;
+
+import java.util.List;
+import java.util.Objects;
+
+public class Standings extends BaseAdapter {
+
+ private Context mContext;
+ private LayoutInflater inflater;
+ private List
- items;
+
+ public Standings(Context context, List
- items) {
+ this.mContext = context;
+ this.items = items;
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (inflater == null) {
+ inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+ if (convertView == null) {
+ convertView = Objects.requireNonNull(inflater).inflate(R.layout.standings, parent, false);
+ holder = new ViewHolder();
+ holder.color = convertView.findViewById(R.id.color);
+ holder.wins = convertView.findViewById(R.id.wins);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ Item m = items.get(position);
+ holder.color.setText(m.color);
+ holder.wins.setText(m.wins);
+
+ return convertView;
+ }
+
+ static class ViewHolder {
+ public TextView color;
+ public TextView wins;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index be431fb..4046535 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,50 @@
-
+
-
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/standings.xml b/app/src/main/res/layout/standings.xml
new file mode 100644
index 0000000..4b1a185
--- /dev/null
+++ b/app/src/main/res/layout/standings.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index 8a49ce6..0000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- #53d06c
- #303F9F
- #FF4081
- #f8e540
- #f8e552
-
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..e1ddbf2
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,5 @@
+
+
+
+ 1032886400260
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0ee3fc..db410f2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,27 @@
Through the maze
- Singleplayer
+
+ GameCycleActivity
+ Dummy Button
+ DUMMY\nCONTENT
+ Multiplayer
+ Please, wait...
+
+ An error occurred while starting the game. Please try again.
+ There was an issue with sign in. Please try again later.
+ %1$s (status %2$d). %3$s.
+
+ Anyone you invite must be a trusted tester
+ This rematch has already been started!
+ Games client reconnect required
+ Network error: Operation failed
+ Internal error
+ Unexpected status: %1$s
+ This match is inactive.
+ This match has locally-modified data. This operation cannot be performed until the match is sent to the server.
+ Sign out
+ Through the maze
+ standings
+ Continue
+ singleplayer
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e5c30ba..68d86c6 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,16 +1,67 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/com/example/danil/throughthemaze/ExampleUnitTest.java b/app/src/test/java/ru/hse/throughthemaze/ExampleUnitTest.java
similarity index 89%
rename from app/src/test/java/com/example/danil/throughthemaze/ExampleUnitTest.java
rename to app/src/test/java/ru/hse/throughthemaze/ExampleUnitTest.java
index 1eae7f9..8d6fe5a 100644
--- a/app/src/test/java/com/example/danil/throughthemaze/ExampleUnitTest.java
+++ b/app/src/test/java/ru/hse/throughthemaze/ExampleUnitTest.java
@@ -1,4 +1,4 @@
-package com.example.danil.throughthemaze;
+package ru.hse.throughthemaze;
import org.junit.Test;
diff --git a/build.gradle b/build.gradle
index 8d3ef8e..42d5c2d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
-
+ classpath 'com.google.gms:google-services:4.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files