From 63e269c4d9d990198d96f2b4984fc4826e541afb Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Tue, 16 Nov 2021 21:51:48 -0500 Subject: [PATCH] add all the things --- .gitignore | 14 ++ .idea/codeStyles/Project.xml | 138 +++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/gradle.xml | 22 ++ .idea/jarRepositories.xml | 25 +++ .idea/misc.xml | 9 + .idea/runConfigurations.xml | 12 ++ app/.gitignore | 1 + app/build.gradle | 37 ++++ app/proguard-rules.pro | 21 ++ .../dottyrevised/ExampleInstrumentedTest.kt | 24 +++ app/src/main/AndroidManifest.xml | 24 +++ .../java/com/wyattjmiller/dottyrevised/Dot.kt | 32 +++ .../com/wyattjmiller/dottyrevised/DotsGame.kt | 170 ++++++++++++++++ .../com/wyattjmiller/dottyrevised/DotsGrid.kt | 188 ++++++++++++++++++ .../wyattjmiller/dottyrevised/MainActivity.kt | 91 +++++++++ .../wyattjmiller/dottyrevised/SoundEffects.kt | 70 +++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++ app/src/main/res/layout/activity_main.xml | 73 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes app/src/main/res/raw/game_over.mp3 | Bin 0 -> 76903 bytes app/src/main/res/raw/note_e.mp3 | Bin 0 -> 10866 bytes app/src/main/res/raw/note_f.mp3 | Bin 0 -> 9612 bytes app/src/main/res/raw/note_f_sharp.mp3 | Bin 0 -> 6268 bytes app/src/main/res/raw/note_g.mp3 | Bin 0 -> 8358 bytes app/src/main/res/values/colors.xml | 14 ++ app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 20 ++ .../dottyrevised/ExampleUnitTest.kt | 17 ++ build.gradle | 26 +++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++++++ gradlew.bat | 84 ++++++++ settings.gradle | 2 + 48 files changed, 1535 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/wyattjmiller/dottyrevised/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/wyattjmiller/dottyrevised/Dot.kt create mode 100644 app/src/main/java/com/wyattjmiller/dottyrevised/DotsGame.kt create mode 100644 app/src/main/java/com/wyattjmiller/dottyrevised/DotsGrid.kt create mode 100644 app/src/main/java/com/wyattjmiller/dottyrevised/MainActivity.kt create mode 100644 app/src/main/java/com/wyattjmiller/dottyrevised/SoundEffects.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/raw/game_over.mp3 create mode 100644 app/src/main/res/raw/note_e.mp3 create mode 100644 app/src/main/res/raw/note_f.mp3 create mode 100644 app/src/main/res/raw/note_f_sharp.mp3 create mode 100644 app/src/main/res/raw/note_g.mp3 create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/wyattjmiller/dottyrevised/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3cc336b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..9202674 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..37a7509 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3a4933b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.2" + + defaultConfig { + applicationId "com.wyattjmiller.dottyrevised" + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/wyattjmiller/dottyrevised/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wyattjmiller/dottyrevised/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f0d4e25 --- /dev/null +++ b/app/src/androidTest/java/com/wyattjmiller/dottyrevised/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wyattjmiller.dottyrevised + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.wyattjmiller.dottyrevised", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b5b2ab2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/dottyrevised/Dot.kt b/app/src/main/java/com/wyattjmiller/dottyrevised/Dot.kt new file mode 100644 index 0000000..bdce35f --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/dottyrevised/Dot.kt @@ -0,0 +1,32 @@ +package com.wyattjmiller.dottyrevised + +import java.util.* + +class Dot(row: Int, col: Int) { + var color = 0 + var row: Int + var col: Int + var centerX = 0f + var centerY = 0f + var radius: Float + var selected: Boolean + private val randomGen: Random = Random() + + fun setRandomColor() { + color = randomGen.nextInt(DotsGame.NUM_COLORS) + } + + fun isAdjacent(dot: Dot): Boolean { + val colDiff = Math.abs(col - dot.col) + val rowDiff = Math.abs(row - dot.row) + return colDiff + rowDiff == 1 + } + + init { + setRandomColor() + selected = false + radius = 1f + this.row = row + this.col = col + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGame.kt b/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGame.kt new file mode 100644 index 0000000..c66d3c5 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGame.kt @@ -0,0 +1,170 @@ +package com.wyattjmiller.dottyrevised + +import java.util.* + +class DotsGame private constructor() { + enum class AddDotStatus { + Added, Rejected, Removed + } + + var movesLeft = 0 + private set + var score = 0 + private set + private val mDots: Array> + + // Return list of selected dots + val selectedDots: ArrayList + + fun getDot(row: Int, col: Int): Dot? { + return if (row >= GRID_SIZE || row < 0 || col >= GRID_SIZE || col < 0) { + null + } else { + mDots[row][col] + } + } + + // Return the last selected dot + val lastSelectedDot: Dot? + get() = if (selectedDots.size > 0) { + selectedDots[selectedDots.size - 1] + } else { + null + } + + // Return the lowest selected dot in each column + val lowestSelectedDots: ArrayList + get() { + val dots = ArrayList() + for (col in 0 until GRID_SIZE) { + for (row in GRID_SIZE - 1 downTo 0) { + if (mDots[row][col]!!.selected) { + dots.add(mDots[row][col]) + break + } + } + } + return dots + } + + // Clear the list of selected dots + fun clearSelectedDots() { + + // Reset board so none selected + for (dot in selectedDots) { + dot!!.selected = false + } + selectedDots.clear() + } + + // Attempt to add the dot to the list of selected dots + fun addSelectedDot(dot: Dot): AddDotStatus { + var status = AddDotStatus.Rejected + + // Check if first dot selected + if (selectedDots.size == 0) { + selectedDots.add(dot) + dot.selected = true + status = AddDotStatus.Added + } else { + if (!dot.selected) { + // Make sure new is same color and adjacent to last selected dot + val lastDot = lastSelectedDot + if (lastDot!!.color == dot.color && lastDot.isAdjacent(dot)) { + selectedDots.add(dot) + dot.selected = true + status = AddDotStatus.Added + } + } else if (selectedDots.size > 1) { + // Dot is already selected, so remove last dot if backtracking + val secondLast = selectedDots[selectedDots.size - 2] + if (secondLast == dot) { + val removedDot = selectedDots.removeAt( + selectedDots.size - 1 + ) + removedDot!!.selected = false + status = AddDotStatus.Removed + } + } + } + return status + } + + // Sort by rows asc + private fun sortSelectedDots() { + Collections.sort(selectedDots, object : Comparator { + override fun compare(dot1: Dot?, dot2: Dot?): Int { + return dot1!!.row - dot2!!.row + } + }) + } + + // Call after completing a dot path to relocate the dots and update the score and moves + fun finishMove() { + if (selectedDots.size > 1) { + // Sort by row so dots are processed top-down + sortSelectedDots() + + // Move all dots above each selected dot down by changing color + for (dot in selectedDots) { + for (row in dot!!.row downTo 1) { + val dotCurrent = mDots[row][dot.col] + val dotAbove = mDots[row - 1][dot.col] + dotCurrent!!.color = dotAbove!!.color + } + + // Add new dot at top + val topDot = mDots[0][dot.col] + topDot!!.setRandomColor() + } + score += selectedDots.size + movesLeft-- + clearSelectedDots() + } + } + + // Start a new game + fun newGame() { + score = 0 + movesLeft = INIT_MOVES + for (row in 0 until GRID_SIZE) { + for (col in 0 until GRID_SIZE) { + mDots[row][col]!!.setRandomColor() + } + } + } + + // Determine if the game is over + val isGameOver: Boolean + get() = movesLeft == 0 + + companion object { + var NUM_COLORS = 5 + var GRID_SIZE = 6 + var INIT_MOVES = 10 + private var mDotsGame: DotsGame? = null + val instance: DotsGame? + get() { + if (mDotsGame == null) { + mDotsGame = DotsGame() + } + return mDotsGame + } + } + + init { + + // Create dots for the 2d array + mDots = Array(GRID_SIZE) { + arrayOfNulls( + GRID_SIZE + ) + } + for (row in 0 until GRID_SIZE) { + for (col in 0 until GRID_SIZE) { + mDots[row][col] = Dot(row, col) + } + } + selectedDots = ArrayList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGrid.kt b/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGrid.kt new file mode 100644 index 0000000..79c9416 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/dottyrevised/DotsGrid.kt @@ -0,0 +1,188 @@ +package com.wyattjmiller.dottyrevised + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.animation.AccelerateInterpolator +import android.view.animation.BounceInterpolator + + +class DotsGrid(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + private val mGame: DotsGame = DotsGame.instance!! + private val DOT_RADIUS = 40 + private var mAnimatorSet: AnimatorSet = AnimatorSet() + + enum class DotSelectionStatus { + First, Additional, Last + } + + interface DotsGridListener { + fun onDotSelected(dot: Dot?, status: DotSelectionStatus?) + fun onAnimationFinished() + } + + private var mGridListener: DotsGridListener? = null + private val mDotColors: IntArray = resources.getIntArray(R.array.dotColors) + private var mCellWidth = 0 + private var mCellHeight = 0 + private val mDotPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val mPathPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + + override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { + val boardWidth = width - paddingLeft - paddingRight + val boardHeight = height - paddingTop - paddingBottom + mCellWidth = boardWidth / DotsGame.GRID_SIZE + mCellHeight = boardHeight / DotsGame.GRID_SIZE + resetDots() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw dots + for (row in 0 until DotsGame.GRID_SIZE) { + for (col in 0 until DotsGame.GRID_SIZE) { + val dot = mGame.getDot(row, col) + mDotPaint.color = mDotColors[dot!!.color] + canvas.drawCircle(dot.centerX, dot.centerY, dot.radius, mDotPaint) + } + } + + if (!mAnimatorSet.isRunning) { + // Draw connector + val selectedDots = mGame.selectedDots + if (selectedDots.isNotEmpty()) { + val path = Path() + var dot = selectedDots[0] + path.moveTo(dot!!.centerX, dot.centerY) + for (i in 1 until selectedDots.size) { + dot = selectedDots[i] + path.lineTo(dot!!.centerX, dot.centerY) + } + mPathPaint.color = mDotColors[dot!!.color] + canvas.drawPath(path, mPathPaint) + } + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + + // Only execute when a listener exists + if (mGridListener == null || mAnimatorSet.isRunning) return true + + // Determine which dot is pressed + val x = event.x.toInt() + val y = event.y.toInt() + val col = x / mCellWidth + val row = y / mCellHeight + var selectedDot = mGame.getDot(row, col) + + // Return previously selected dot if touch moves outside the grid + if (selectedDot == null) { + selectedDot = mGame.lastSelectedDot + } + + // Notify activity that a dot is selected + if (event.action == MotionEvent.ACTION_DOWN) { + mGridListener!!.onDotSelected(selectedDot, DotSelectionStatus.First) + } else if (event.action == MotionEvent.ACTION_MOVE) { + mGridListener!!.onDotSelected(selectedDot, DotSelectionStatus.Additional) + } else if (event.action == MotionEvent.ACTION_UP) { + mGridListener!!.onDotSelected(selectedDot, DotSelectionStatus.Last) + } + return true + } + + fun setGridListener(gridListener: DotsGridListener?) { + mGridListener = gridListener + } + + fun animateDots() { + // For storing many animations + val animations: ArrayList = ArrayList() + + val lowestDots = mGame.lowestSelectedDots + for (dot in lowestDots) { + var rowsToMove = 1 + for (row in dot!!.row - 1 downTo 0) { + val dotToMove = mGame.getDot(row, dot.col) + if (dotToMove!!.selected) { + rowsToMove++ + } else { + val targetY = dotToMove!!.centerY + + rowsToMove * mCellHeight + animations.add( + getFallingAnimator( + dotToMove!!, + targetY + )!! + ) + } + } + } + + + // Get an animation to make selected dots disappear + animations.add(getDisappearingAnimator()) + + // Play animations (just one right now) together, then reset radius to full size + mAnimatorSet = AnimatorSet() + mAnimatorSet.playTogether(animations) + mAnimatorSet.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + resetDots() + mGridListener!!.onAnimationFinished() + } + }) + + mAnimatorSet.start() + } + + private fun getDisappearingAnimator(): ValueAnimator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator?.duration = 100 + animator?.interpolator = AccelerateInterpolator() + animator?.addUpdateListener { animation -> + for (dot in mGame.selectedDots) { + dot!!.radius = DOT_RADIUS * animation.animatedValue as Float + } + invalidate() + } + return animator + } + + private fun getFallingAnimator(dot: Dot, destinationY: Float): ValueAnimator? { + val animator = ValueAnimator.ofFloat(dot.centerY, destinationY) + animator?.duration = 300 + animator?.interpolator = BounceInterpolator() + animator?.addUpdateListener { animation -> + dot.centerY = animation.animatedValue as Float + invalidate() + } + return animator + } + + private fun resetDots() { + for (row in 0 until DotsGame.GRID_SIZE) { + for (col in 0 until DotsGame.GRID_SIZE) { + val dot = mGame.getDot(row, col) + dot!!.radius = DOT_RADIUS.toFloat() + dot.centerX = (col * mCellWidth + mCellWidth / 2).toFloat() + dot.centerY = (row * mCellHeight + mCellHeight / 2).toFloat() + } + } + } + + init { + mPathPaint.strokeWidth = 10f + mPathPaint.style = Paint.Style.STROKE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wyattjmiller/dottyrevised/MainActivity.kt b/app/src/main/java/com/wyattjmiller/dottyrevised/MainActivity.kt new file mode 100644 index 0000000..35d55a9 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/dottyrevised/MainActivity.kt @@ -0,0 +1,91 @@ +package com.wyattjmiller.dottyrevised + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.wyattjmiller.dottyrevised.DotsGrid.DotSelectionStatus +import com.wyattjmiller.dottyrevised.DotsGrid.DotsGridListener + + +class MainActivity : AppCompatActivity() { + private var mGame: DotsGame? = null + private var mDotsGrid: DotsGrid? = null + private var mMovesRemaining: TextView? = null + private var mScore: TextView? = null + private var mSoundEffects: SoundEffects? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + mMovesRemaining = findViewById(R.id.movesRemaining) + mScore = findViewById(R.id.score) + mDotsGrid = findViewById(R.id.gameGrid) + mDotsGrid?.setGridListener(mGridListener) + mGame = DotsGame.instance + mSoundEffects = SoundEffects.getInstance(applicationContext)!! + newGame() + } + + private val mGridListener: DotsGridListener = object : DotsGridListener { + override fun onDotSelected(dot: Dot?, status: DotSelectionStatus?) { + + // Ignore selections when game is over + if (mGame!!.isGameOver) return + + // Add to list of selected dots + mGame!!.addSelectedDot(dot!!) + + if (status == DotSelectionStatus.First) { + mSoundEffects!!.resetTones() + } + + val addStatus = mGame!!.addSelectedDot(dot) + + if (addStatus == DotsGame.AddDotStatus.Added) { + mSoundEffects!!.playTone(true) + } else if (addStatus == DotsGame.AddDotStatus.Removed) { + mSoundEffects!!.playTone(false) + } + + // If done selecting dots then replace selected dots and display new moves and score + if (status === DotSelectionStatus.Last) { + if (mGame!!.selectedDots.size > 1) { + mDotsGrid?.animateDots() + + // These methods must be called AFTER the animation completes + //mGame!!.finishMove() + //updateMovesAndScore() + } else { + mGame!!.clearSelectedDots() + } + } + + // Display changes to the game + mDotsGrid!!.invalidate() + } + + override fun onAnimationFinished() { + mGame!!.finishMove() + mDotsGrid!!.invalidate() + updateMovesAndScore() + + if (mGame!!.isGameOver) mSoundEffects!!.playGameOver() + } + } + + fun newGameClick(view: View?) { + newGame() + } + + private fun newGame() { + mGame!!.newGame() + mDotsGrid!!.invalidate() + updateMovesAndScore() + } + + private fun updateMovesAndScore() { + mMovesRemaining!!.text = String.format(mGame!!.movesLeft.toString()) + mScore!!.text = String.format(mGame!!.score.toString()) + } +} diff --git a/app/src/main/java/com/wyattjmiller/dottyrevised/SoundEffects.kt b/app/src/main/java/com/wyattjmiller/dottyrevised/SoundEffects.kt new file mode 100644 index 0000000..c9b5dc5 --- /dev/null +++ b/app/src/main/java/com/wyattjmiller/dottyrevised/SoundEffects.kt @@ -0,0 +1,70 @@ +package com.wyattjmiller.dottyrevised + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import java.util.* + + +class SoundEffects private constructor(private val mContext: Context) { + private var mSoundPool: SoundPool? = null + private val mSelectSoundIds: ArrayList + private var mSoundIndex = 0 + private val mEndGameSoundId: Int + + fun resetTones() { + mSoundIndex = -1 + } + + fun playTone(advance: Boolean) { + if (advance) { + mSoundIndex++ + } else { + mSoundIndex-- + } + if (mSoundIndex < 0) { + mSoundIndex = 0 + } else if (mSoundIndex >= mSelectSoundIds.size) { + mSoundIndex = 0 + } + mSoundPool!!.play(mSelectSoundIds[mSoundIndex], 1f, 1f, 1, 0, 1f) + } + + fun playGameOver() { + mSoundPool!!.play(mEndGameSoundId, 0.5f, 0.5f, 1, 0, 1f) + } + + companion object { + private var mSoundEffects: SoundEffects? = null + fun getInstance(context: Context): SoundEffects? { + if (mSoundEffects == null) { + mSoundEffects = SoundEffects(context) + } + return mSoundEffects + } + } + + init { + mSoundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + SoundPool.Builder() + .setAudioAttributes(attributes) + .build() + } else { + SoundPool(1, AudioManager.STREAM_MUSIC, 0) + } + + mSelectSoundIds = ArrayList() + mSelectSoundIds.add(mSoundPool!!.load(mContext, R.raw.note_e, 1)) + mSelectSoundIds.add(mSoundPool!!.load(mContext, R.raw.note_f, 1)) + mSelectSoundIds.add(mSoundPool!!.load(mContext, R.raw.note_f_sharp, 1)) + mSelectSoundIds.add(mSoundPool!!.load(mContext, R.raw.note_g, 1)) + mEndGameSoundId = mSoundPool!!.load(mContext, R.raw.game_over, 1) + resetTones() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f08b9f4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + +