Merge pull request #88 from lucasnlm/add-retry

Add retry
This commit is contained in:
Lucas Nunes 2020-06-12 09:18:53 -03:00 committed by GitHub
commit f6c30a29c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 598 additions and 277 deletions

View file

@ -83,6 +83,10 @@
<data
android:host="load-game"
android:scheme="antimine" />
<data
android:host="retry-game"
android:scheme="antimine" />
</intent-filter>
<intent-filter
@ -126,15 +130,6 @@
<!-- </intent-filter>-->
</activity>
<activity
android:name="dev.lucasnlm.antimine.about.views.thirds.ThirdPartiesFragment"
android:label="@string/licenses"
android:theme="@style/AppTheme" />
<activity
android:name="dev.lucasnlm.antimine.about.views.translators.TranslatorsFragment"
android:theme="@style/AppTheme" />
<activity
android:name="dev.lucasnlm.antimine.about.TextActivity"
android:theme="@style/AppTheme">

View file

@ -0,0 +1,14 @@
package dev.lucasnlm.antimine
object DeepLink {
const val SCHEME = "antimine"
const val NEW_GAME_AUTHORITY = "new-game"
const val RETRY_HOST_AUTHORITY = "retry-game"
const val LOAD_GAME_AUTHORITY = "load-game"
const val BEGINNER_PATH = "beginner"
const val INTERMEDIATE_PATH = "intermediate"
const val EXPERT_PATH = "expert"
const val STANDARD_PATH = "standard"
}

View file

@ -71,6 +71,7 @@ class GameActivity : DaggerAppCompatActivity() {
private var totalArea: Int = 0
private var rightMines: Int = 0
private var currentTime: Long = 0
private var currentSaveId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -95,38 +96,60 @@ class GameActivity : DaggerAppCompatActivity() {
private fun bindViewModel() = viewModel.apply {
var lastEvent: Event? = null // TODO use distinctUntilChanged when available
eventObserver.observe(this@GameActivity, Observer {
if (lastEvent != it) {
onGameEvent(it)
lastEvent = it
eventObserver.observe(
this@GameActivity,
Observer {
if (lastEvent != it) {
onGameEvent(it)
lastEvent = it
}
}
})
)
elapsedTimeSeconds.observe(this@GameActivity, Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
elapsedTimeSeconds.observe(
this@GameActivity,
Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
}
currentTime = it
}
currentTime = it
})
)
mineCount.observe(this@GameActivity, Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
mineCount.observe(
this@GameActivity,
Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
}
}
})
)
difficulty.observe(this@GameActivity, Observer {
onChangeDifficulty(it)
})
difficulty.observe(
this@GameActivity,
Observer {
onChangeDifficulty(it)
}
)
field.observe(this@GameActivity, Observer { area ->
val mines = area.filter { it.hasMine }
totalArea = area.count()
totalMines = mines.count()
rightMines = mines.map { if (it.mark.isFlag()) 1 else 0 }.sum()
})
field.observe(
this@GameActivity,
Observer { area ->
val mines = area.filter { it.hasMine }
totalArea = area.count()
totalMines = mines.count()
rightMines = mines.map { if (it.mark.isFlag()) 1 else 0 }.sum()
}
)
saveId.observe(
this@GameActivity,
Observer {
currentSaveId = it
}
)
}
override fun onBackPressed() {
@ -399,7 +422,8 @@ class GameActivity : DaggerAppCompatActivity() {
victory,
score?.rightMines ?: 0,
score?.totalMines ?: 0,
currentGameStatus.time
currentGameStatus.time,
currentSaveId
).apply {
showAllowingStateLoss(supportFragmentManager, EndGameDialogFragment.TAG)
}
@ -409,9 +433,13 @@ class GameActivity : DaggerAppCompatActivity() {
private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) {
if (await && viewModel.explosionDelay() != 0L) {
postDelayed(Handler(), {
showEndGameDialog(victory)
}, null, (viewModel.explosionDelay() * 0.3).toLong())
postDelayed(
Handler(),
{
showEndGameDialog(victory)
},
null, (viewModel.explosionDelay() * 0.3).toLong()
)
} else {
showEndGameDialog(victory)
}

View file

@ -61,35 +61,50 @@ class TvGameActivity : DaggerAppCompatActivity() {
}
private fun bindViewModel() = viewModel.apply {
eventObserver.observe(this@TvGameActivity, Observer {
onGameEvent(it)
})
elapsedTimeSeconds.observe(this@TvGameActivity, Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
eventObserver.observe(
this@TvGameActivity,
Observer {
onGameEvent(it)
}
currentTime = it
})
)
mineCount.observe(this@TvGameActivity, Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
elapsedTimeSeconds.observe(
this@TvGameActivity,
Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
}
currentTime = it
}
})
)
difficulty.observe(this@TvGameActivity, Observer {
// onChangeDifficulty(it)
})
mineCount.observe(
this@TvGameActivity,
Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
}
}
)
field.observe(this@TvGameActivity, Observer { area ->
val mines = area.filter { it.hasMine }
totalArea = area.count()
totalMines = mines.count()
rightMines = mines.map { if (it.mark.isFlag()) 1 else 0 }.sum()
})
difficulty.observe(
this@TvGameActivity,
Observer {
// onChangeDifficulty(it)
}
)
field.observe(
this@TvGameActivity,
Observer { area ->
val mines = area.filter { it.hasMine }
totalArea = area.count()
totalMines = mines.count()
rightMines = mines.map { if (it.mark.isFlag()) 1 else 0 }.sum()
}
)
}
override fun onResume() {
@ -208,10 +223,36 @@ class TvGameActivity : DaggerAppCompatActivity() {
private fun waitAndShowConfirmNewGame() {
if (keepConfirmingNewGame) {
HandlerCompat.postDelayed(Handler(), {
HandlerCompat.postDelayed(
Handler(),
{
if (status is Status.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.new_game)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
keepConfirmingNewGame = false
}
},
null, DateUtils.SECOND_IN_MILLIS
)
}
}
private fun waitAndShowGameOverConfirmNewGame() {
HandlerCompat.postDelayed(
Handler(),
{
if (status is Status.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.new_game)
setTitle(R.string.you_lost)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
@ -220,28 +261,10 @@ class TvGameActivity : DaggerAppCompatActivity() {
}
setNegativeButton(R.string.cancel, null)
}.show()
keepConfirmingNewGame = false
}
}, null, DateUtils.SECOND_IN_MILLIS)
}
}
private fun waitAndShowGameOverConfirmNewGame() {
HandlerCompat.postDelayed(Handler(), {
if (status is Status.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.you_lost)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
}
}, null, DateUtils.SECOND_IN_MILLIS)
},
null, DateUtils.SECOND_IN_MILLIS
)
}
private fun changeDifficulty(newDifficulty: Difficulty) {

View file

@ -25,22 +25,25 @@ class AboutActivity : AppCompatActivity() {
aboutViewModel = ViewModelProviders.of(this).get(AboutViewModel::class.java)
aboutViewModel.eventObserver.observe(this, Observer { event ->
when (event) {
AboutEvent.ThirdPartyLicenses -> {
replaceFragment(ThirdPartiesFragment(), ThirdPartiesFragment::class.simpleName)
}
AboutEvent.SourceCode -> {
openSourceCode()
}
AboutEvent.Translators -> {
replaceFragment(TranslatorsFragment(), TranslatorsFragment::class.simpleName)
}
else -> {
replaceFragment(AboutInfoFragment(), null)
aboutViewModel.eventObserver.observe(
this,
Observer { event ->
when (event) {
AboutEvent.ThirdPartyLicenses -> {
replaceFragment(ThirdPartiesFragment(), ThirdPartiesFragment::class.simpleName)
}
AboutEvent.SourceCode -> {
openSourceCode()
}
AboutEvent.Translators -> {
replaceFragment(TranslatorsFragment(), TranslatorsFragment::class.simpleName)
}
else -> {
replaceFragment(AboutInfoFragment(), null)
}
}
}
})
)
replaceFragment(AboutInfoFragment(), null)
}

View file

@ -1,14 +1,18 @@
package dev.lucasnlm.antimine.history.views
import android.content.Intent
import android.graphics.PorterDuff
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.DeepLink
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.SaveStatus
import dev.lucasnlm.antimine.common.level.models.Difficulty
import java.text.DateFormat
class HistoryAdapter(
private val saveHistory: List<Save>
@ -23,24 +27,60 @@ class HistoryAdapter(
override fun getItemCount(): Int = saveHistory.size
override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) = with(saveHistory[position]) {
holder.difficulty.text = holder.itemView.context.getString(when (difficulty) {
Difficulty.Beginner -> R.string.beginner
Difficulty.Intermediate -> R.string.intermediate
Difficulty.Expert -> R.string.expert
Difficulty.Standard -> R.string.standard
Difficulty.Custom -> R.string.custom
})
holder.difficulty.text = holder.itemView.context.getString(
when (difficulty) {
Difficulty.Beginner -> R.string.beginner
Difficulty.Intermediate -> R.string.intermediate
Difficulty.Expert -> R.string.expert
Difficulty.Standard -> R.string.standard
Difficulty.Custom -> R.string.custom
}
)
val context = holder.itemView.context
holder.flag.setColorFilter(
when (status) {
SaveStatus.VICTORY -> ContextCompat.getColor(context, R.color.victory)
SaveStatus.ON_GOING -> ContextCompat.getColor(context, R.color.ongoing)
SaveStatus.DEFEAT -> ContextCompat.getColor(context, R.color.lose)
}, PorterDuff.Mode.SRC_IN
)
holder.minefieldSize.text = String.format("%d x %d", minefield.width, minefield.height)
holder.minesCount.text = holder.itemView.context.getString(R.string.mines_remaining, minefield.mines)
holder.date.text = DateFormat.getDateInstance().format(startDate)
holder.minesCount.text = context.getString(R.string.mines_remaining, minefield.mines)
holder.itemView.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
data = Uri.parse("antimine://load-game/$uid")
}
it.context.startActivity(intent)
if (status != SaveStatus.VICTORY) {
holder.replay.setImageResource(R.drawable.replay)
holder.replay.setOnClickListener { replayGame(it, uid) }
} else {
holder.replay.setImageResource(R.drawable.play)
holder.replay.setOnClickListener { loadGame(it, uid) }
}
holder.itemView.setOnClickListener { loadGame(it, uid) }
}
private fun replayGame(view: View, uid: Int) {
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
data = Uri.Builder()
.scheme(DeepLink.SCHEME)
.authority(DeepLink.RETRY_HOST_AUTHORITY)
.appendPath(uid.toString())
.build()
}
view.context.startActivity(intent)
}
private fun loadGame(view: View, uid: Int) {
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
data = Uri.Builder()
.scheme(DeepLink.SCHEME)
.authority(DeepLink.LOAD_GAME_AUTHORITY)
.appendPath(uid.toString())
.build()
}
view.context.startActivity(intent)
}
}

View file

@ -50,9 +50,12 @@ class HistoryFragment : DaggerFragment() {
)
layoutManager = LinearLayoutManager(view.context)
historyViewModel?.saves?.observe(viewLifecycleOwner, Observer {
adapter = HistoryAdapter(it)
})
historyViewModel?.saves?.observe(
viewLifecycleOwner,
Observer {
adapter = HistoryAdapter(it)
}
)
}
}
}

View file

@ -2,12 +2,14 @@ package dev.lucasnlm.antimine.history.views
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.R
class HistoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val flag: AppCompatImageView = view.findViewById(R.id.badge)
val difficulty: TextView = view.findViewById(R.id.difficulty)
val minefieldSize: TextView = view.findViewById(R.id.minefieldSize)
val minesCount: TextView = view.findViewById(R.id.minesCount)
val date: TextView = view.findViewById(R.id.date)
val replay: AppCompatImageView = view.findViewById(R.id.replay)
}

View file

@ -36,6 +36,7 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
private var time: Long = 0L
private var rightMines: Int = 0
private var totalMines: Int = 0
private var saveId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -48,10 +49,11 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
arguments?.run {
isVictory = getBoolean(DIALOG_IS_VICTORY) == true
time = getLong(DIALOG_TIME)
rightMines = getInt(DIALOG_RIGHT_MINES)
totalMines = getInt(DIALOG_TOTAL_MINES)
time = getLong(DIALOG_TIME, 0L)
rightMines = getInt(DIALOG_RIGHT_MINES, 0)
totalMines = getInt(DIALOG_TOTAL_MINES, 0)
hasValidData = (totalMines > 0)
saveId = getLong(DIALOG_SAVE_ID, 0L)
}
}
@ -118,7 +120,7 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
instantAppManager.showInstallPrompt(this, null, 0, null)
}
}
} else {
} else if (isVictory) {
setNeutralButton(R.string.share) { _, _ ->
val setup = viewModel.levelSetup.value
val field = viewModel.field.value
@ -127,17 +129,24 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
shareViewModel.share(setup, field, time)
}
}
} else {
setNeutralButton(R.string.retry) { _, _ ->
GlobalScope.launch {
viewModel.retryGame(saveId.toInt())
}
}
}
}.create()
companion object {
fun newInstance(victory: Boolean, rightMines: Int, totalMines: Int, time: Long): EndGameDialogFragment =
fun newInstance(victory: Boolean, rightMines: Int, totalMines: Int, time: Long, saveId: Long) =
EndGameDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(DIALOG_IS_VICTORY, victory)
putInt(DIALOG_RIGHT_MINES, rightMines)
putInt(DIALOG_TOTAL_MINES, totalMines)
putLong(DIALOG_TIME, time)
putLong(DIALOG_SAVE_ID, saveId)
}
}
@ -145,6 +154,7 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
private const val DIALOG_TIME = "dialog_time"
private const val DIALOG_RIGHT_MINES = "dialog_right_mines"
private const val DIALOG_TOTAL_MINES = "dialog_total_mines"
private const val DIALOG_SAVE_ID = "dialog_save_id"
val TAG = EndGameDialogFragment::class.simpleName
}

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.RecyclerView
import dagger.android.support.DaggerFragment
import dev.lucasnlm.antimine.DeepLink
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
@ -67,10 +68,12 @@ open class LevelFragment : DaggerFragment() {
GlobalScope.launch {
val loadGameUid = checkLoadGameDeepLink()
val newGameDeepLink = checkNewGameDeepLink()
val retryDeepLink = checkRetryGameDeepLink()
val levelSetup = when {
loadGameUid != null -> viewModel.loadGame(loadGameUid)
newGameDeepLink != null -> viewModel.startNewGame(newGameDeepLink)
retryDeepLink != null -> viewModel.retryGame(retryDeepLink)
else -> viewModel.loadLastGame()
}
@ -92,41 +95,53 @@ open class LevelFragment : DaggerFragment() {
}
viewModel.run {
field.observe(viewLifecycleOwner, Observer {
areaAdapter.bindField(it)
})
field.observe(
viewLifecycleOwner,
Observer {
areaAdapter.bindField(it)
}
)
levelSetup.observe(viewLifecycleOwner, Observer {
recyclerGrid.layoutManager = makeNewLayoutManager(it.width, it.height)
})
levelSetup.observe(
viewLifecycleOwner,
Observer {
recyclerGrid.layoutManager = makeNewLayoutManager(it.width, it.height)
}
)
fieldRefresh.observe(viewLifecycleOwner, Observer {
areaAdapter.notifyItemChanged(it)
})
fieldRefresh.observe(
viewLifecycleOwner,
Observer {
areaAdapter.notifyItemChanged(it)
}
)
eventObserver.observe(viewLifecycleOwner, Observer {
when (it) {
Event.ResumeGameOver,
Event.GameOver,
Event.Victory,
Event.ResumeVictory -> areaAdapter.setClickEnabled(false)
Event.Running,
Event.ResumeGame,
Event.StartNewGame -> areaAdapter.setClickEnabled(true)
else -> {
eventObserver.observe(
viewLifecycleOwner,
Observer {
when (it) {
Event.ResumeGameOver,
Event.GameOver,
Event.Victory,
Event.ResumeVictory -> areaAdapter.setClickEnabled(false)
Event.Running,
Event.ResumeGame,
Event.StartNewGame -> areaAdapter.setClickEnabled(true)
else -> {
}
}
}
})
)
}
}
private fun checkNewGameDeepLink(): Difficulty? = activity?.intent?.data?.let { uri ->
if (uri.scheme == DEFAULT_SCHEME) {
when (uri.schemeSpecificPart.removePrefix(DEEP_LINK_NEW_GAME_HOST)) {
DEEP_LINK_BEGINNER -> Difficulty.Beginner
DEEP_LINK_INTERMEDIATE -> Difficulty.Intermediate
DEEP_LINK_EXPERT -> Difficulty.Expert
DEEP_LINK_STANDARD -> Difficulty.Standard
if (uri.scheme == DeepLink.SCHEME && uri.authority == DeepLink.NEW_GAME_AUTHORITY) {
when (uri.pathSegments.firstOrNull()) {
DeepLink.BEGINNER_PATH -> Difficulty.Beginner
DeepLink.INTERMEDIATE_PATH -> Difficulty.Intermediate
DeepLink.EXPERT_PATH -> Difficulty.Expert
DeepLink.STANDARD_PATH -> Difficulty.Standard
else -> null
}
} else {
@ -135,8 +150,16 @@ open class LevelFragment : DaggerFragment() {
}
private fun checkLoadGameDeepLink(): Int? = activity?.intent?.data?.let { uri ->
if (uri.scheme == DEFAULT_SCHEME) {
uri.schemeSpecificPart.removePrefix(DEEP_LINK_LOAD_GAME_HOST).toIntOrNull()
if (uri.scheme == DeepLink.SCHEME && uri.authority == DeepLink.LOAD_GAME_AUTHORITY) {
uri.pathSegments.firstOrNull()?.toIntOrNull()
} else {
null
}
}
private fun checkRetryGameDeepLink(): Int? = activity?.intent?.data?.let { uri ->
if (uri.scheme == DeepLink.SCHEME && uri.authority == DeepLink.RETRY_HOST_AUTHORITY) {
uri.pathSegments.firstOrNull()?.toIntOrNull()
} else {
null
}
@ -154,15 +177,4 @@ open class LevelFragment : DaggerFragment() {
((recyclerGrid.measuredHeight - dimensionRepository.areaSizeWithPadding() * boardHeight) / 2)
.coerceAtLeast(0.0f)
.toInt()
companion object {
const val DEFAULT_SCHEME = "antimine"
const val DEEP_LINK_NEW_GAME_HOST = "//new-game/"
const val DEEP_LINK_LOAD_GAME_HOST = "//load-game/"
const val DEEP_LINK_BEGINNER = "beginner"
const val DEEP_LINK_INTERMEDIATE = "intermediate"
const val DEEP_LINK_EXPERT = "expert"
const val DEEP_LINK_STANDARD = "standard"
}
}

View file

@ -66,10 +66,13 @@ class ShareBuilder(
val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawRect(0.0f, 0.0f, imageWidth.toFloat(), imageHeight.toFloat(), Paint().apply {
color = Color.WHITE
style = Paint.Style.FILL
})
canvas.drawRect(
0.0f, 0.0f, imageWidth.toFloat(), imageHeight.toFloat(),
Paint().apply {
color = Color.WHITE
style = Paint.Style.FILL
}
)
for (x in 0 until minefield.width) {
for (y in 0 until minefield.height) {

View file

@ -14,17 +14,17 @@ class StatsViewModel : ViewModel() {
return if (statsCount > 0) {
val result = stats.fold(
StatsModel(statsCount, 0L, 0L, 0, 0, 0)
) { acc, value ->
StatsModel(
acc.totalGames,
acc.duration + value.duration,
0,
acc.mines + value.mines,
acc.victory + value.victory,
acc.openArea + value.openArea
)
}
StatsModel(statsCount, 0L, 0L, 0, 0, 0)
) { acc, value ->
StatsModel(
acc.totalGames,
acc.duration + value.duration,
0,
acc.mines + value.mines,
acc.victory + value.victory,
acc.openArea + value.openArea
)
}
result.copy(averageDuration = result.duration / result.totalGames)
} else {
StatsModel(0, 0, 0, 0, 0, 0)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,5V2.21c0,-0.45 -0.54,-0.67 -0.85,-0.35l-3.8,3.79c-0.2,0.2 -0.2,0.51 0,0.71l3.79,3.79c0.32,0.31 0.86,0.09 0.86,-0.36V7c3.73,0 6.68,3.42 5.86,7.29 -0.47,2.27 -2.31,4.1 -4.57,4.57 -3.57,0.75 -6.75,-1.7 -7.23,-5.01 -0.07,-0.48 -0.49,-0.85 -0.98,-0.85 -0.6,0 -1.08,0.53 -1,1.13 0.62,4.39 4.8,7.64 9.53,6.72 3.12,-0.61 5.63,-3.12 6.24,-6.24C20.84,9.48 16.94,5 12,5z"/>
</vector>

View file

@ -66,13 +66,16 @@
app:layout_constraintTop_toBottomOf="@id/difficulty"
tools:text="9 mines" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/open"
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/replay"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/retry"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/text_color"
app:srcCompat="@drawable/retry" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -36,7 +36,8 @@ class AreaScreenshot {
Paint().apply {
color = if (ambientMode) Color.BLACK else Color.WHITE
style = Paint.Style.FILL
})
}
)
canvas.save()
canvas.translate(testPadding * 0.5f, testPadding.toFloat() * 0.5f)

View file

@ -1,5 +1,6 @@
package dev.lucasnlm.antimine.common.level
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.SaveStatus
import dev.lucasnlm.antimine.common.level.database.models.Stats
@ -16,6 +17,7 @@ class LevelFacade {
private val randomGenerator: Random
private val startTime = System.currentTimeMillis()
private var saveId = 0
private var firstOpen: FirstOpen = FirstOpen.Unknown
var hasMines = false
private set
@ -29,21 +31,24 @@ class LevelFacade {
var mines: Sequence<Area> = sequenceOf()
private set
constructor(minefield: Minefield, seed: Long) {
constructor(minefield: Minefield, seed: Long, saveId: Int? = null) {
this.minefield = minefield
this.randomGenerator = Random(seed)
this.seed = seed
this.saveId = 0
this.saveId = saveId ?: 0
createEmptyField()
}
constructor(save: Save) {
this.minefield = save.minefield
this.randomGenerator = Random(save.seed)
this.saveId = save.uid
this.seed = save.seed
this.firstOpen = save.firstOpen
this.field = save.field.asSequence()
this.mines = this.field.filter { it.hasMine }.asSequence()
this.hasMines = this.mines.count() != 0
this.saveId = save.uid
}
private fun createEmptyField() {
@ -55,6 +60,8 @@ class LevelFacade {
val xPosition = (index % width)
Area(index, xPosition, yPosition)
}.asSequence()
this.hasMines = false
this.mines = sequenceOf()
}
fun getArea(id: Int) = field.first { it.id == id }
@ -108,6 +115,7 @@ class LevelFacade {
}
}
firstOpen = FirstOpen.Position(safeIndex)
field.filterNot { it.safeZone }
.toSet()
.shuffled(randomGenerator)
@ -350,6 +358,7 @@ class LevelFacade {
duration,
minefield,
difficulty,
firstOpen,
saveStatus,
field.toList()
)

View file

@ -5,6 +5,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.lucasnlm.antimine.common.level.database.converters.DifficultyConverter
import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.FirstOpenConverter
import dev.lucasnlm.antimine.common.level.database.converters.MinefieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
@ -16,13 +17,15 @@ import dev.lucasnlm.antimine.common.level.database.models.Stats
entities = [
Save::class,
Stats::class
], version = 3, exportSchema = false
],
version = 4, exportSchema = false
)
@TypeConverters(
FieldConverter::class,
SaveStatusConverter::class,
MinefieldConverter::class,
DifficultyConverter::class
DifficultyConverter::class,
FirstOpenConverter::class
)
abstract class AppDataBase : RoomDatabase() {
abstract fun saveDao(): SaveDao

View file

@ -4,7 +4,6 @@ import androidx.room.TypeConverter
import dev.lucasnlm.antimine.common.level.models.Difficulty
class DifficultyConverter {
@TypeConverter
fun toDifficulty(difficulty: Int): Difficulty =
when (difficulty) {

View file

@ -0,0 +1,15 @@
package dev.lucasnlm.antimine.common.level.database.converters
import androidx.room.TypeConverter
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
class FirstOpenConverter {
@TypeConverter
fun toFirstOpen(value: Int): FirstOpen = when {
(value < 0) -> FirstOpen.Unknown
else -> FirstOpen.Position(value)
}
@TypeConverter
fun toInteger(firstOpen: FirstOpen): Int = firstOpen.toInt()
}

View file

@ -9,7 +9,8 @@ import dev.lucasnlm.antimine.common.level.models.Minefield
class MinefieldConverter {
private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val jsonAdapter: JsonAdapter<Minefield> = moshi.adapter(
Minefield::class.java)
Minefield::class.java
)
@TypeConverter
fun toMinefield(jsonInput: String): Minefield =

View file

@ -30,6 +30,6 @@ interface SaveDao {
@Delete
suspend fun delete(save: Save)
@Query("DELETE FROM save WHERE uid NOT IN (SELECT uid FROM save LIMIT :maxStorage)")
@Query("DELETE FROM save WHERE uid NOT IN (SELECT uid FROM save WHERE status != 1 LIMIT :maxStorage)")
suspend fun deleteOldSaves(maxStorage: Int)
}

View file

@ -0,0 +1,28 @@
package dev.lucasnlm.antimine.common.level.database.models
/**
* Used to define the position of the first open square.
*/
sealed class FirstOpen {
/**
* Used before the first step or before this value be recorded.
*/
object Unknown : FirstOpen()
/**
* Describes the [value] of the first step.
*/
class Position(
val value: Int
) : FirstOpen()
override fun toString(): String = when (this) {
is Position -> value.toString()
else -> "Unknown"
}
fun toInt(): Int = when (this) {
is Position -> value
else -> -1
}
}

View file

@ -8,6 +8,7 @@ import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.FirstOpenConverter
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
@Entity
@ -30,6 +31,10 @@ data class Save(
@ColumnInfo(name = "difficulty")
val difficulty: Difficulty,
@TypeConverters(FirstOpenConverter::class)
@ColumnInfo(name = "firstOpen")
val firstOpen: FirstOpen,
@TypeConverters(SaveStatusConverter::class)
@ColumnInfo(name = "status")
val status: SaveStatus,

View file

@ -30,12 +30,15 @@ open class Clock {
fun start(onTick: (seconds: Long) -> Unit) {
stop()
timer = provideTimer().apply {
scheduleAtFixedRate(object : TimerTask() {
override fun run() {
elapsedTimeSeconds++
onTick(elapsedTimeSeconds)
}
}, 1000L, 1000L)
scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
elapsedTimeSeconds++
onTick(elapsedTimeSeconds)
}
},
1000L, 1000L
)
}
}
}

View file

@ -103,11 +103,12 @@ class AreaView : View {
area.hasMine -> IMPORTANT_FOR_ACCESSIBILITY_YES
area.mistake -> IMPORTANT_FOR_ACCESSIBILITY_YES
area.mark.isNotNone() -> IMPORTANT_FOR_ACCESSIBILITY_YES
!area.isCovered -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
IMPORTANT_FOR_ACCESSIBILITY_NO
}
!area.isCovered ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
IMPORTANT_FOR_ACCESSIBILITY_NO
}
else -> IMPORTANT_FOR_ACCESSIBILITY_YES
}
)

View file

@ -5,6 +5,7 @@ import android.os.Handler
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dev.lucasnlm.antimine.common.level.LevelFacade
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
@ -48,6 +49,7 @@ class GameViewModel(
val mineCount = MutableLiveData<Int>()
val difficulty = MutableLiveData<Difficulty>()
val levelSetup = MutableLiveData<Minefield>()
val saveId = MutableLiveData<Long>()
fun startNewGame(newDifficulty: Difficulty = currentDifficulty): Minefield {
clock.reset()
@ -96,14 +98,49 @@ class GameViewModel(
else -> eventObserver.postValue(Event.ResumeGame)
}
saveId.postValue(save.uid.toLong())
analyticsManager.sentEvent(Analytics.ResumePreviousGame())
return setup
}
private fun retryGame(save: Save): Minefield {
clock.reset()
elapsedTimeSeconds.postValue(0L)
currentDifficulty = save.difficulty
val setup = save.minefield
levelFacade = LevelFacade(setup, save.seed, save.uid).apply {
if (save.firstOpen is FirstOpen.Position) {
plantMinesExcept(save.firstOpen.value, true)
singleClick(save.firstOpen.value)
}
}
mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty)
levelSetup.postValue(setup)
refreshAll()
eventObserver.postValue(Event.StartNewGame)
analyticsManager.sentEvent(
Analytics.RetryGame(
setup, currentDifficulty,
levelFacade.seed,
useAccessibilityMode(),
save.firstOpen.toInt()
)
)
saveId.postValue(save.uid.toLong())
return setup
}
suspend fun loadGame(uid: Int): Minefield = withContext(Dispatchers.IO) {
val lastGame = savesRepository.loadFromId(uid)
if (lastGame != null) {
saveId.postValue(uid.toLong())
currentDifficulty = lastGame.difficulty
resumeGameFromSave(lastGame)
} else {
@ -115,10 +152,27 @@ class GameViewModel(
}
}
suspend fun retryGame(uid: Int): Minefield = withContext(Dispatchers.IO) {
val save = savesRepository.loadFromId(uid)
if (save != null) {
saveId.postValue(uid.toLong())
currentDifficulty = save.difficulty
retryGame(save)
} else {
// Fail to load
startNewGame()
}.also {
initialized = true
oldGame = true
}
}
suspend fun loadLastGame(): Minefield = withContext(Dispatchers.IO) {
val lastGame = savesRepository.fetchCurrentSave()
if (lastGame != null) {
saveId.postValue(lastGame.uid.toLong())
currentDifficulty = lastGame.difficulty
resumeGameFromSave(lastGame)
} else {
@ -144,6 +198,7 @@ class GameViewModel(
val id = savesRepository.saveGame(
levelFacade.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty)
)
saveId.postValue(id)
levelFacade.setCurrentSaveId(id?.toInt() ?: 0)
}
}

View file

@ -253,7 +253,8 @@ class FixedGridLayoutManager(
for (offset in 0 until it.size()) {
// Look for off-screen removals that are less-than this
if (removedPositions.valueAt(offset) ==
REMOVE_INVISIBLE && removedPositions.keyAt(offset) < nextPosition) {
REMOVE_INVISIBLE && removedPositions.keyAt(offset) < nextPosition
) {
// Offset position to match
offsetPosition--
}

View file

@ -10,38 +10,59 @@ sealed class Analytics(
) {
class Open : Analytics("Open game")
class NewGame(minefield: Minefield, difficulty: Difficulty, seed: Long, useAccessibilityMode: Boolean) :
Analytics("New Game", mapOf(
class NewGame(
minefield: Minefield,
difficulty: Difficulty,
seed: Long,
useAccessibilityMode: Boolean
) : Analytics(
"New Game",
mapOf(
"Seed" to seed.toString(),
"Difficulty Preset" to difficulty.text,
"Width" to minefield.width.toString(),
"Height" to minefield.height.toString(),
"Mines" to minefield.mines.toString(),
"Accessibility" to useAccessibilityMode.toString()
)
)
)
class RetryGame(
minefield: Minefield,
difficulty: Difficulty,
seed: Long,
useAccessibilityMode: Boolean,
firstOpen: Int
) : Analytics(
"Retry Game",
mapOf(
"Seed" to seed.toString(),
"Difficulty Preset" to difficulty.text,
"Width" to minefield.width.toString(),
"Height" to minefield.height.toString(),
"Mines" to minefield.mines.toString(),
"Accessibility" to useAccessibilityMode.toString(),
"First Open" to firstOpen.toString()
)
)
class ResumePreviousGame : Analytics("Resume previous game")
class LongPressArea(index: Int) : Analytics("Long press area",
mapOf("Index" to index.toString())
)
class LongPressArea(index: Int) : Analytics("Long press area", mapOf("Index" to index.toString()))
class LongPressMultipleArea(index: Int) : Analytics("Long press to open multiple",
mapOf("Index" to index.toString())
)
class LongPressMultipleArea(index: Int) :
Analytics("Long press to open multiple", mapOf("Index" to index.toString()))
class PressArea(index: Int) : Analytics("Press area",
mapOf("Index" to index.toString())
)
class PressArea(index: Int) : Analytics("Press area", mapOf("Index" to index.toString()))
class GameOver(time: Long, score: Score) : Analytics("Game Over",
class GameOver(time: Long, score: Score) : Analytics(
"Game Over",
mapOf(
"Time" to time.toString(),
"Right Mines" to score.rightMines.toString(),
"Total Mines" to score.totalMines.toString(),
"Total Area" to score.totalArea.toString()
)
)
)
class Victory(time: Long, score: Score, difficulty: Difficulty) : Analytics(
@ -71,17 +92,9 @@ sealed class Analytics(
class OpenSaveHistory : Analytics("Open Save History")
class ShowRatingRequest(usages: Int) : Analytics("Shown Rating Request",
mapOf(
"Usages" to usages.toString()
))
class ShowRatingRequest(usages: Int) : Analytics("Shown Rating Request", mapOf("Usages" to usages.toString()))
class TapRatingRequest(from: String) : Analytics("Rating Request",
mapOf(
"From" to from
))
class TapRatingRequest(from: String) : Analytics("Rating Request", mapOf("From" to from))
class TapGameReset(resign: Boolean) : Analytics("Game reset",
mapOf("Resign" to resign.toString())
)
class TapGameReset(resign: Boolean) : Analytics("Game reset", mapOf("Resign" to resign.toString()))
}

View file

@ -6,6 +6,10 @@
<color name="primary">#212121</color>
<color name="primary_dark">#212121</color>
<color name="victory">#FFFFFF</color>
<color name="ongoing">#616161</color>
<color name="lose">#616161</color>
<color name="mines_around_1">#d5d2cc</color>
<color name="mines_around_2">#d5d2cc</color>
<color name="mines_around_3">#d5d2cc</color>

View file

@ -6,6 +6,10 @@
<color name="primary">#FFFFFF</color>
<color name="primary_dark">#9E9E9E</color>
<color name="victory">#4CAF50</color>
<color name="ongoing">#455a64</color>
<color name="lose">#F44336</color>
<color name="mines_around_1">#527F8D</color>
<color name="mines_around_2">#2B8D43</color>
<color name="mines_around_3">#E65100</color>

View file

@ -305,7 +305,8 @@ class LevelFacadeTest {
1, 1, 1, 1, 1,
1, 1, 0, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1),
1, 1, 1, 1, 1
),
field.map { if (it.isCovered) 1 else 0 }.toList()
)
openNeighbors(12)
@ -315,7 +316,8 @@ class LevelFacadeTest {
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1),
1, 1, 1, 1, 1
),
field.map { if (it.isCovered) 1 else 0 }.toList()
)
}

View file

@ -5,7 +5,6 @@
<uses-feature android:name="android.hardware.type.watch" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name="dev.lucasnlm.antimine.wear.MainApplication"

View file

@ -12,7 +12,6 @@ import dev.lucasnlm.antimine.common.level.models.Status
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import kotlinx.android.synthetic.main.activity_level.*
import javax.inject.Inject
import android.text.format.DateFormat
@ -33,9 +32,6 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC
@Inject
lateinit var viewModelFactory: GameViewModelFactory
@Inject
lateinit var preferencesRepository: IPreferencesRepository
private lateinit var viewModel: GameViewModel
private lateinit var ambientController: AmbientModeSupport.AmbientController
@ -138,20 +134,32 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC
}
private fun bindViewModel() = viewModel.apply {
eventObserver.observe(this@WatchGameActivity, Observer {
onGameEvent(it)
})
elapsedTimeSeconds.observe(this@WatchGameActivity, Observer {
// Nothing
})
mineCount.observe(this@WatchGameActivity, Observer {
if (it > 0) {
messageText.text = applicationContext.getString(R.string.mines_remaining, it)
eventObserver.observe(
this@WatchGameActivity,
Observer {
onGameEvent(it)
}
})
difficulty.observe(this@WatchGameActivity, Observer {
// Nothing
})
)
elapsedTimeSeconds.observe(
this@WatchGameActivity,
Observer {
// Nothing
}
)
mineCount.observe(
this@WatchGameActivity,
Observer {
if (it > 0) {
messageText.text = applicationContext.getString(R.string.mines_remaining, it)
}
}
)
difficulty.observe(
this@WatchGameActivity,
Observer {
// Nothing
}
)
}
private fun onGameEvent(event: Event) {
@ -194,17 +202,21 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC
}
private fun waitAndShowNewGameButton(wait: Long = DateUtils.SECOND_IN_MILLIS) {
HandlerCompat.postDelayed(Handler(), {
if (this.status is Status.Over && !isFinishing) {
newGame.visibility = View.VISIBLE
newGame.setOnClickListener {
it.visibility = View.GONE
GlobalScope.launch {
viewModel.startNewGame()
HandlerCompat.postDelayed(
Handler(),
{
if (this.status is Status.Over && !isFinishing) {
newGame.visibility = View.VISIBLE
newGame.setOnClickListener {
it.visibility = View.GONE
GlobalScope.launch {
viewModel.startNewGame()
}
}
}
}
}, null, wait)
},
null, wait
)
}
override fun getAmbientCallback(): AmbientCallback = ambientMode

View file

@ -78,27 +78,39 @@ class WatchLevelFragment : DaggerFragment() {
}
viewModel.run {
field.observe(viewLifecycleOwner, Observer {
areaAdapter.bindField(it)
})
levelSetup.observe(viewLifecycleOwner, Observer {
recyclerGrid.layoutManager =
GridLayoutManager(activity, it.width, RecyclerView.VERTICAL, false)
})
fieldRefresh.observe(viewLifecycleOwner, Observer {
areaAdapter.notifyItemChanged(it)
})
eventObserver.observe(viewLifecycleOwner, Observer {
if (it == Event.StartNewGame) {
recyclerGrid.scrollToPosition(areaAdapter.itemCount / 2)
field.observe(
viewLifecycleOwner,
Observer {
areaAdapter.bindField(it)
}
)
levelSetup.observe(
viewLifecycleOwner,
Observer {
recyclerGrid.layoutManager =
GridLayoutManager(activity, it.width, RecyclerView.VERTICAL, false)
}
)
fieldRefresh.observe(
viewLifecycleOwner,
Observer {
areaAdapter.notifyItemChanged(it)
}
)
eventObserver.observe(
viewLifecycleOwner,
Observer {
if (it == Event.StartNewGame) {
recyclerGrid.scrollToPosition(areaAdapter.itemCount / 2)
}
when (it) {
Event.ResumeGameOver, Event.GameOver,
Event.Victory, Event.ResumeVictory -> areaAdapter.setClickEnabled(false)
else -> areaAdapter.setClickEnabled(true)
when (it) {
Event.ResumeGameOver, Event.GameOver,
Event.Victory, Event.ResumeVictory -> areaAdapter.setClickEnabled(false)
else -> areaAdapter.setClickEnabled(true)
}
}
})
)
}
}