diff --git a/.idea/artifacts/app.xml b/.idea/artifacts/app.xml new file mode 100644 index 0000000..15e9e38 --- /dev/null +++ b/.idea/artifacts/app.xml @@ -0,0 +1,19 @@ + + + $PROJECT_DIR$/build/classes/artifacts/app + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..892046b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b762e5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Through the maze diff --git a/app/build.gradle b/app/build.gradle index 3ed0b6f..439537d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { - applicationId "com.example.danil.throughthemaze" + applicationId "ru.hse.throughthemaze" minSdkVersion 19 targetSdkVersion 28 - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "0.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -15,14 +15,31 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + debuggable true + signingConfig signingConfigs.debug + } + } + signingConfigs { + debug { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile file('/home/danil/.android/debug.keystore') + storePassword 'android' + } } } dependencies { + api group: 'org.xerial', name: 'sqlite-jdbc', version: '3.23.1' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.google.android.gms:play-services-identity:16.0.0' + implementation 'com.google.android.gms:play-services-games:17.0.0' + implementation 'com.google.android.gms:play-services-auth:16.0.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'org.jetbrains:annotations-java5:15.0' } diff --git a/app/src/androidTest/java/com/example/danil/throughthemaze/ExampleInstrumentedTest.java b/app/src/androidTest/java/ru/hse/throughthemaze/ExampleInstrumentedTest.java similarity index 82% rename from app/src/androidTest/java/com/example/danil/throughthemaze/ExampleInstrumentedTest.java rename to app/src/androidTest/java/ru/hse/throughthemaze/ExampleInstrumentedTest.java index 4c471c1..03f6a60 100644 --- a/app/src/androidTest/java/com/example/danil/throughthemaze/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/ru/hse/throughthemaze/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package com.example.danil.throughthemaze; +package ru.hse.throughthemaze; import android.content.Context; import android.support.test.InstrumentationRegistry; @@ -21,6 +21,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("com.example.danil.throughthemaze", appContext.getPackageName()); + assertEquals("ru.hse.throughthemaze", appContext.getPackageName()); } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15f7658..3151c27 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="ru.hse.throughthemaze"> - + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/databases/maps.sqlite3 b/app/src/main/assets/databases/maps.sqlite3 new file mode 100644 index 0000000..dc114c8 Binary files /dev/null and b/app/src/main/assets/databases/maps.sqlite3 differ diff --git a/app/src/main/java/com/example/danil/throughthemaze/MainActivity.java b/app/src/main/java/com/example/danil/throughthemaze/MainActivity.java deleted file mode 100644 index 8c5b4d0..0000000 --- a/app/src/main/java/com/example/danil/throughthemaze/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.danil.throughthemaze; - -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - } -} diff --git a/app/src/main/java/ru/hse/throughthemaze/AccelerometerService.java b/app/src/main/java/ru/hse/throughthemaze/AccelerometerService.java new file mode 100644 index 0000000..8d5d50d --- /dev/null +++ b/app/src/main/java/ru/hse/throughthemaze/AccelerometerService.java @@ -0,0 +1,74 @@ +package ru.hse.throughthemaze; + +import android.app.Service; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.IBinder; +import android.support.annotation.Nullable; +import ru.hse.throughthemaze.gameplay.Ball; + +import java.util.Timer; +import java.util.TimerTask; + +public class AccelerometerService extends Service { + + private SensorManager sensorManager; + private Sensor sensorAcceleration; + private SensorEventListener listener; + private Timer timer; + private Ball ball; + + private double ax; + private double ay; + + @Override + public int onStartCommand(final Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + ball = intent.getParcelableExtra(Ball.class.getName()); + sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); + sensorAcceleration = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); + listener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + ax = -event.values[0] * 20; + ay = event.values[1] * 20; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + }; + sensorManager.registerListener(listener, sensorAcceleration, SensorManager.SENSOR_DELAY_GAME); + + timer = new Timer(); + TimerTask task = new TimerTask() { + @Override + public void run() { + ball.ax = ax; + ball.ay = ay; + Intent intent = new Intent(Service.SENSOR_SERVICE); + intent.putExtra(Ball.class.getName(), ball); + sendBroadcast(intent); + } + }; + timer.schedule(task, 0, MainActivity.UPDATE_FREQUENCY); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + sensorManager.unregisterListener(listener); + timer.cancel(); + } + + @org.jetbrains.annotations.Nullable + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + +} diff --git a/app/src/main/java/ru/hse/throughthemaze/MainActivity.java b/app/src/main/java/ru/hse/throughthemaze/MainActivity.java new file mode 100644 index 0000000..7c80295 --- /dev/null +++ b/app/src/main/java/ru/hse/throughthemaze/MainActivity.java @@ -0,0 +1,1096 @@ +package ru.hse.throughthemaze; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Service; +import android.content.*; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.LinearLayout; +import android.widget.ListView; +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.games.*; +import com.google.android.gms.games.multiplayer.Participant; +import com.google.android.gms.games.multiplayer.realtime.*; +import com.google.android.gms.tasks.*; +import ru.hse.throughthemaze.database.MapDBHandler; +import ru.hse.throughthemaze.database.MapDBManager; +import ru.hse.throughthemaze.gameplay.Ball; +import ru.hse.throughthemaze.gameplay.Balls; +import ru.hse.throughthemaze.gameplay.PhysicsEngine; +import ru.hse.throughthemaze.map.Map; +import ru.hse.throughthemaze.view.Draw2D; +import ru.hse.throughthemaze.view.Item; +import ru.hse.throughthemaze.view.Standings; + +import java.nio.ByteBuffer; +import java.util.*; + +public class MainActivity extends AppCompatActivity implements View.OnClickListener { + private final static String TAG = "Through the maze"; + + // Request codes for the UIs that we show with startActivityForResult: + private final static int RC_WAITING_ROOM = 10002; + + // Request code used to invoke sign in user interactions. + private static final int RC_SIGN_IN = 9001; + + // Client used to sign in with Google APIs + private GoogleSignInClient mGoogleSignInClient = null; + + // Client used to interact with the real time multiplayer system. + private RealTimeMultiplayerClient mRealTimeMultiplayerClient = null; + + // Room ID where the currently active game is taking place; null if we're + // not playing. + private String mRoomId = null; + + // Holds the configuration of the current room. + private RoomConfig mRoomConfig; + + // Are we playing in multiplayer mode? + private boolean mMultiplayer = false; + + // The participants in the currently active game + private ArrayList 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 @@ - + -