diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a7509..7bfef59 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 238486c..e2b12cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,25 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion 29 @@ -23,34 +42,53 @@ android { } } - dataBinding { - enabled = true + buildFeatures { + viewBinding = true + //dataBinding = true + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" } + } dependencies { def lifecycle_version = "2.2.0" - def lottieVersion = "2.8.0" + def lottieVersion = "3.4.0" implementation fileTree(dir: 'libs', include: ['*.jar']) - // ViewModel - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" - // LiveData + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.constraintlayout:constraintlayout:1.1.3" + implementation "androidx.core:core-ktx:1.2.0" + implementation "androidx.fragment:fragment-ktx:1.2.4" + debugImplementation 'androidx.fragment:fragment-testing:1.3.0-alpha03' + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation "androidx.navigation:navigation-fragment-ktx:2.2.2" + implementation "androidx.navigation:navigation-ui-ktx:2.2.2" + implementation "com.jakewharton.timber:timber:4.7.1" + implementation "com.wajahatkarim3.EasyFlipView:EasyFlipView:3.0.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" - // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" - - implementation 'androidx.fragment:fragment-ktx:1.1.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation "com.airbnb.android:lottie:$lottieVersion" - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01" - testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "com.airbnb.android:lottie:$lottieVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + testImplementation "junit:junit:4.13" + testImplementation "org.mockito:mockito-all:1.10.19" + androidTestImplementation "androidx.test:core:1.2.0" + androidTestImplementation "androidx.test:rules:1.2.0" + androidTestImplementation "androidx.test:runner:1.2.0" + androidTestImplementation "androidx.test.espresso:espresso-contrib:3.2.0" + androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" + androidTestImplementation "androidx.test.ext:junit:1.1.1" + androidTestImplementation 'com.google.truth:truth:1.0.1' } diff --git a/app/build/tmp/kotlin-classes/debug/com/example/memorygame/BoardActivity.class b/app/build/tmp/kotlin-classes/debug/com/example/memorygame/BoardActivity.class deleted file mode 100644 index c135887..0000000 Binary files a/app/build/tmp/kotlin-classes/debug/com/example/memorygame/BoardActivity.class and /dev/null differ diff --git a/app/build/tmp/kotlin-classes/debug/com/example/memorygame/LobbyActivity.class b/app/build/tmp/kotlin-classes/debug/com/example/memorygame/LobbyActivity.class deleted file mode 100644 index 8578520..0000000 Binary files a/app/build/tmp/kotlin-classes/debug/com/example/memorygame/LobbyActivity.class and /dev/null differ diff --git a/app/src/androidTest/java/com/example/memorygame/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/memorygame/ExampleInstrumentedTest.kt index 71e3ef2..7939b04 100644 --- a/app/src/androidTest/java/com/example/memorygame/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/example/memorygame/ExampleInstrumentedTest.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package com.example.memorygame import androidx.test.platform.app.InstrumentationRegistry diff --git a/app/src/androidTest/java/com/example/memorygame/lobby/LobbyScreenFragmentTest.kt b/app/src/androidTest/java/com/example/memorygame/lobby/LobbyScreenFragmentTest.kt new file mode 100644 index 0000000..bf7205e --- /dev/null +++ b/app/src/androidTest/java/com/example/memorygame/lobby/LobbyScreenFragmentTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.lobby + +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.example.memorygame.R +import com.example.memorygame.R.id.button_new_game +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class LobbyScreenFragmentTest { + + @Before + fun setUp() { + launchFragmentInContainer() + } + + @Test + fun testLobbyScreenIsDisplayed() { + onView(withId(button_new_game)).check(matches(isDisplayed())) + } + + @Test + fun testDifficultyLevelBottomSheetIsDisplayed() { + onView(withId(button_new_game)).perform(click()) + onView(withId(R.id.text_sheet_difficulty_selection_title)).check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f084fea..77cc6a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,24 +1,43 @@ - + + - - + android:theme="@style/Theme.MemoryGame" + tools:ignore="AllowBackup"> + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..fba55af Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/memorygame/BoardActivity.kt b/app/src/main/java/com/example/memorygame/BoardActivity.kt deleted file mode 100644 index 496e78d..0000000 --- a/app/src/main/java/com/example/memorygame/BoardActivity.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.memorygame - -import android.media.MediaPlayer -import android.os.Bundle -import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager -import com.example.memorygame.LobbyActivity.Companion.EXTRA_DIFICULTY -import com.example.memorygame.databinding.ActivityBoardBinding - - -class BoardActivity : AppCompatActivity(), BoardAdapter.OnCardItemListener { - - private lateinit var boardAdapter: BoardAdapter - private lateinit var binding: ActivityBoardBinding - private lateinit var viewModel: BoardViewModel - private lateinit var difficulty: BoardDifficulty - private lateinit var mediaPlayer: MediaPlayer - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - difficulty = intent.getSerializableExtra(EXTRA_DIFICULTY) as BoardDifficulty - binding = DataBindingUtil.setContentView(this, R.layout.activity_board) - viewModel = ViewModelProvider(this, BoardViewModelFactory(difficulty, BoardUseCaseImp())) - .get(BoardViewModel::class.java) - binding.viewModel = viewModel - binding.lifecycleOwner = this - - initViews() - initObservers() - } - - private fun initViews() { - binding.boardTableRv.apply { - layoutManager = GridLayoutManager(this@BoardActivity, difficulty.columns) - boardAdapter = BoardAdapter( this@BoardActivity) - adapter = boardAdapter - } - binding.boardBackBtn.setOnClickListener { onBackPressed() } - } - - private fun initObservers() { - viewModel.score.observe(this, Observer { onScoreChanged(it) }) - viewModel.cardList.observe(this, Observer { updateAdapter(it) }) - } - - private fun updateAdapter(cardList: MutableList) { - boardAdapter.submitCardList(cardList) - boardAdapter.notifyDataSetChanged() - } - - private fun onScoreChanged(score: Int) { - val isWinner: Boolean = viewModel.areYouWinner(score) - if (isWinner) - playWinnerAnimation() - else - playMatch() - } - - private fun playMatch() { - mediaPlayer = MediaPlayer.create(this, R.raw.game_match) - mediaPlayer.start() - } - - private fun playMatchError() { - mediaPlayer = MediaPlayer.create(this, R.raw.game_error) - mediaPlayer.start() - } - - private fun playWinnerAnimation() { - binding.boardTableRv.visibility = GONE - binding.boardLottieAnimation.visibility = VISIBLE - binding.boardLottieAnimation.playAnimation() - } - - override fun onCardClicked(card: Card, pos: Int) = viewModel.onCardClicked(card, pos) -} - diff --git a/app/src/main/java/com/example/memorygame/BoardAdapter.kt b/app/src/main/java/com/example/memorygame/BoardAdapter.kt deleted file mode 100644 index a3a3b26..0000000 --- a/app/src/main/java/com/example/memorygame/BoardAdapter.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.memorygame - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.example.memorygame.State.CLOSE - - -class BoardAdapter(private val listener: OnCardItemListener) : - RecyclerView.Adapter() { - - private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean { - return oldItem.character == newItem.character && oldItem.state == newItem.state - } - - override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean { - return oldItem.character == newItem.character && oldItem.state == newItem.state - } - - } - private val differ = AsyncListDiffer(this, DIFF_CALLBACK) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.item_board_layout, parent, false) - return CardHolder(view) - } - - override fun getItemCount(): Int { - return differ.currentList.size - } - - override fun onBindViewHolder(holder: CardHolder, position: Int) { - holder.bind(differ.currentList[position], listener) - } - - fun submitCardList(cards: List) { - differ.submitList(cards) - } - - class CardHolder(private val view: View) : RecyclerView.ViewHolder(view) { - private val image: ImageView = view.findViewById(R.id.item_board_card) as ImageView - - fun bind(card: Card, listener: OnCardItemListener) { - - if (card.state == CLOSE) - image.setBackgroundResource(R.drawable.ic_card_cover) - else - image.setBackgroundResource(card.character.resource) - - view.setOnClickListener { - listener.onCardClicked(card, adapterPosition) - } - } - } - - interface OnCardItemListener { - fun onCardClicked(card: Card, position: Int) - } -} diff --git a/app/src/main/java/com/example/memorygame/BoardAdapterListener.kt b/app/src/main/java/com/example/memorygame/BoardAdapterListener.kt deleted file mode 100644 index b4baf9b..0000000 --- a/app/src/main/java/com/example/memorygame/BoardAdapterListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.memorygame - -interface BoardAdapterListener { - fun onSelectedCardListener(position: Int, card: Card) -} diff --git a/app/src/main/java/com/example/memorygame/BoardUseCase.kt b/app/src/main/java/com/example/memorygame/BoardUseCase.kt deleted file mode 100644 index 784955a..0000000 --- a/app/src/main/java/com/example/memorygame/BoardUseCase.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.memorygame - -import com.example.memorygame.Character.BAT -import com.example.memorygame.Character.CAT -import com.example.memorygame.Character.COW -import com.example.memorygame.Character.DOG -import com.example.memorygame.Character.DRAGON -import com.example.memorygame.Character.HEN -import com.example.memorygame.Character.HORSE -import com.example.memorygame.Character.MAN -import com.example.memorygame.Character.PIG -import com.example.memorygame.Character.SPIDER - -interface BoardUseCase { - fun getCardsToPlay(numberOfCharacters: Int): List - fun verifyMatch(firstCard: Card?, secondCard: Card?): Boolean - fun areYouWinner(score: Int, difficulty: BoardDifficulty): Boolean -} - -class BoardUseCaseImp : BoardUseCase { - private val cardSet = setOf( - Card(BAT), - Card(COW), - Card(CAT), - Card(DOG), - Card(DRAGON), - Card(HEN), - Card(HORSE), - Card(MAN), - Card(PIG), - Card(SPIDER) - ) - - private fun getCharacters(numberOfPairs: Int): List { - return cardSet.shuffled().subList(0, numberOfPairs) - } - - override fun getCardsToPlay(numberOfCharacters: Int): List { - val characters = getCharacters(numberOfCharacters) - var cardList = mutableListOf() - cardList.addAll(characters.shuffled()) - cardList.addAll(characters.shuffled()) - return cardList - } - - override fun verifyMatch(firstCard: Card?, secondCard: Card?): Boolean = - firstCard != null && secondCard != null && (firstCard?.character == secondCard?.character) - - override fun areYouWinner(score: Int, difficulty: BoardDifficulty): Boolean { - val maxScore = getMaxScore(difficulty) - return score == maxScore - } - - private fun getMaxScore(difficulty: BoardDifficulty) = - (difficulty.columns * difficulty.rows) / 2 -} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/BoardViewModel.kt b/app/src/main/java/com/example/memorygame/BoardViewModel.kt deleted file mode 100644 index 6f09d10..0000000 --- a/app/src/main/java/com/example/memorygame/BoardViewModel.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.memorygame - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.example.memorygame.State.CLOSE -import com.example.memorygame.State.MATCHED -import com.example.memorygame.State.OPEN - - -class BoardViewModel( - private val difficulty: BoardDifficulty, - private val boardUseCase: BoardUseCase -) : ViewModel() { - - private val INVALID_POSITION = -1 - private val maxScore = (difficulty.columns * difficulty.rows) / 2 - - private var isWaitingForMatch = false - private var firstCardPos = INVALID_POSITION - private var secondCardPos = INVALID_POSITION - - private var _isBoardActive = MutableLiveData(true) - val isBoardActive: LiveData get() = _isBoardActive - - private var _cardList = MutableLiveData(boardUseCase.getCardsToPlay(maxScore).toMutableList()) - val cardList: LiveData> get() = _cardList - - private val _score = MutableLiveData(0) - val score: LiveData - get() = _score - - fun getMaxScore() = maxScore - - fun onCardClicked(clickedCard: Card, position: Int) { - if (clickedCard.state == CLOSE) { - openCard(clickedCard, position) - if (!isWaitingForMatch) { - firstCardPos = position - } else { - secondCardPos = position - lockScreenBy(10000) - verifyMatch() - } - isWaitingForMatch = !isWaitingForMatch - } - } - - private fun verifyMatch() { - val firstCard = cardList.value?.get(firstCardPos) - val secondCard = cardList.value?.get(secondCardPos) - val matchResult = boardUseCase.verifyMatch(firstCard, secondCard) - if (matchResult) { - val currentScore = score.value ?: 0 - matchCards(firstCard, secondCard) - _score.value = currentScore + 1 - } else { - closeCards(firstCard, secondCard) - } - firstCardPos = INVALID_POSITION - secondCardPos = INVALID_POSITION - } - - private fun lockScreenBy(timeMillis: Long) { - _isBoardActive.value = false - Thread.sleep(1000) - _isBoardActive.value = true - } - - private fun matchCards(firstCard: Card?, secondCard: Card?) { - firstCard?.let { matchCard(firstCard, firstCardPos) } - secondCard?.let { matchCard(secondCard, secondCardPos) } - } - - private fun closeCards(firstCard: Card?, secondCard: Card?) { - firstCard?.let { closeCard(firstCard, firstCardPos) } - secondCard?.let { closeCard(secondCard, secondCardPos) } - } - - private fun changeCardState(card: Card, position: Int, newState: State) { - val copyCards = cardList.value ?: mutableListOf() - copyCards[position] = card.copy(state = newState) - _cardList.value = copyCards - } - - private fun openCard(card: Card, position: Int) { - changeCardState(card, position, OPEN) - } - - private fun closeCard(card: Card, position: Int) { - changeCardState(card, position, CLOSE) - } - - private fun matchCard(card: Card, position: Int) { - changeCardState(card, position, MATCHED) - } - - fun areYouWinner(score: Int): Boolean { - return boardUseCase.areYouWinner(score, difficulty) - } -} diff --git a/app/src/main/java/com/example/memorygame/BoardViewModelFactory.kt b/app/src/main/java/com/example/memorygame/BoardViewModelFactory.kt deleted file mode 100644 index 51a4ede..0000000 --- a/app/src/main/java/com/example/memorygame/BoardViewModelFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.memorygame - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -class BoardViewModelFactory( - private val difficulty: BoardDifficulty, - private var boardUseCase: BoardUseCase -) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(BoardViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return BoardViewModel(difficulty, boardUseCase) as T - } - throw IllegalArgumentException("Unable to construct viewmodel") - } -} - diff --git a/app/src/main/java/com/example/memorygame/Card.kt b/app/src/main/java/com/example/memorygame/Card.kt deleted file mode 100644 index 156a860..0000000 --- a/app/src/main/java/com/example/memorygame/Card.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.memorygame - -import com.example.memorygame.State.CLOSE - -data class Card(val character: Character, var state: State = CLOSE) - -enum class Character(val resource: Int) { - BAT(R.drawable.ic_card_bat), - CAT(R.drawable.ic_card_cat), - DRAGON(R.drawable.ic_card_dragon), - DOG(R.drawable.ic_card_gosh_dog), - COW(R.drawable.ic_card_cow), - HEN(R.drawable.ic_card_hen), - HORSE(R.drawable.ic_card_horse), - MAN(R.drawable.ic_card_garbage_man), - PIG(R.drawable.ic_card_pig), - SPIDER(R.drawable.ic_card_spider) -} - -enum class State { - CLOSE, - OPEN, - MATCHED -} diff --git a/app/src/main/java/com/example/memorygame/GameApp.kt b/app/src/main/java/com/example/memorygame/GameApp.kt index 25c61df..2c4797f 100644 --- a/app/src/main/java/com/example/memorygame/GameApp.kt +++ b/app/src/main/java/com/example/memorygame/GameApp.kt @@ -1,7 +1,30 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package com.example.memorygame import android.app.Application +import timber.log.Timber class GameApp : Application() { - + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/LobbyActivity.kt b/app/src/main/java/com/example/memorygame/LobbyActivity.kt deleted file mode 100644 index 2cc911a..0000000 --- a/app/src/main/java/com/example/memorygame/LobbyActivity.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.memorygame - -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import com.example.memorygame.BoardDifficulty.NONE -import com.example.memorygame.databinding.ActivityLobbyBinding - -class LobbyActivity : AppCompatActivity() { - - private lateinit var binding: ActivityLobbyBinding - private lateinit var viewModel: LobbyViewModel - - companion object { - val EXTRA_DIFICULTY = "DIFFICULTY" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(LobbyViewModel::class.java) - binding = DataBindingUtil.setContentView(this, R.layout.activity_lobby) - binding.viewModel = viewModel - binding.lifecycleOwner = this - viewModel.level.observe(this, Observer { if (it != NONE) playGame(it) }) - } - - private fun playGame(difficulty: BoardDifficulty) { - startActivity( - Intent(this, BoardActivity::class.java) - .putExtra(EXTRA_DIFICULTY, difficulty) - ) - } - - fun back(view: View) {} -} diff --git a/app/src/main/java/com/example/memorygame/LobbyViewModel.kt b/app/src/main/java/com/example/memorygame/LobbyViewModel.kt deleted file mode 100644 index ad1cf53..0000000 --- a/app/src/main/java/com/example/memorygame/LobbyViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.memorygame - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.example.memorygame.BoardDifficulty.EASY -import com.example.memorygame.BoardDifficulty.HARD -import com.example.memorygame.BoardDifficulty.NONE -import com.example.memorygame.BoardDifficulty.NORMAL -import com.example.memorygame.BoardDifficulty.VERY_EASY - -class LobbyViewModel : ViewModel() { - private var _level: MutableLiveData = MutableLiveData(NONE) - val level: MutableLiveData - get() = _level - - fun onVeryEasyClicked() { - _level.value = VERY_EASY - } - - fun onEasyClicked() { - _level.value = EASY - } - - fun onNormalClicked() { - _level.value = NORMAL - } - - fun onHardClicked() { - _level.value = HARD - } -} - -enum class BoardDifficulty( val columns: Int, val rows: Int) { - NONE(0, 0), - VERY_EASY(5, 2), - EASY(3, 4), - NORMAL(4, 4), - HARD(4, 5) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/MainActivity.kt b/app/src/main/java/com/example/memorygame/MainActivity.kt new file mode 100644 index 0000000..e3b9488 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/MainActivity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.example.memorygame.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + private lateinit var mainBinding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(mainBinding.root) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/board/BoardGridAdapter.kt b/app/src/main/java/com/example/memorygame/board/BoardGridAdapter.kt new file mode 100644 index 0000000..8f3f761 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/board/BoardGridAdapter.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.board + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView +import com.example.memorygame.core.GameAttributes.SingleCard +import com.example.memorygame.databinding.ItemBoardCardBinding +import com.wajahatkarim3.easyflipview.EasyFlipView +import timber.log.Timber + +class BoardGridAdapter : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + ViewHolder(ItemBoardCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + + override fun getItemCount(): Int = differ.currentList.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bindCard(differ.currentList[position], onCardFlippedListener) + } + + private lateinit var onCardFlippedListener: (String) -> Unit + + private val differ = AsyncListDiffer(this, object : ItemCallback() { + override fun areItemsTheSame(oldItem: SingleCard, newItem: SingleCard): Boolean = + oldItem.uid == newItem.uid + + override fun areContentsTheSame(oldItem: SingleCard, newItem: SingleCard): Boolean = + oldItem == newItem + + }) + + fun swapList(cardsList: List) { + differ.submitList(cardsList) + } + + fun setOnCardFlippedListener(flipListener: (String) -> Unit) { + onCardFlippedListener = flipListener + } + + class ViewHolder(private val cardBinding: ItemBoardCardBinding) : + RecyclerView.ViewHolder(cardBinding.root) { + + fun bindCard( + cardItem: SingleCard, + onCardFlippedListener: (String) -> Unit + ) { + cardBinding.imageCardFront.setOnClickListener { + cardBinding.flipper.flipTheView() + } + + cardBinding.imageCardBack.apply { + setImageDrawable( + ContextCompat.getDrawable( + cardBinding.imageCardFront.context, + cardItem.card.imageResource + ) + ) + } + + Timber.d("flipper.currentFlipState: ${cardBinding.flipper.currentFlipState}") + + cardBinding.flipper.setOnFlipListener { _, newCurrentSide -> + Timber.d("state: $newCurrentSide, card: ${cardItem.card.name}, flipped before? ${cardItem.flipped}") + if (EasyFlipView.FlipState.BACK_SIDE == newCurrentSide) { + onCardFlippedListener(cardItem.uid) + } + } + } + + fun flipBack(cardItem: SingleCard) { + Timber.d("flipper.currentFlipState.before: ${cardBinding.flipper.currentFlipState}") + if (!cardItem.flipped && cardBinding.flipper.isBackSide) { + cardBinding.flipper.flipTheView() + Timber.d("flipper.currentFlipState.after: ${cardBinding.flipper.currentFlipState}") + } + } + } +} diff --git a/app/src/main/java/com/example/memorygame/board/BoardScreenFragment.kt b/app/src/main/java/com/example/memorygame/board/BoardScreenFragment.kt new file mode 100644 index 0000000..deacbbf --- /dev/null +++ b/app/src/main/java/com/example/memorygame/board/BoardScreenFragment.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.board + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.RawRes +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import androidx.recyclerview.widget.GridLayoutManager +import com.example.memorygame.R +import com.example.memorygame.core.GameAttributes +import com.example.memorygame.core.GameStateContract +import com.example.memorygame.core.GameStateContract.GameResult.FAILED +import com.example.memorygame.core.GameStateContract.GameResult.SUCCESS +import com.example.memorygame.core.SingleAudioPlayer +import com.example.memorygame.databinding.FragmentBoardBinding +import timber.log.Timber + +class BoardScreenFragment : Fragment() { + + private var _viewBinding: FragmentBoardBinding? = null + private val viewBinding get() = _viewBinding!! + private val viewModel by navGraphViewModels(R.id.nav_graph_game) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _viewBinding = FragmentBoardBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _viewBinding = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBoardTable() + + viewModel.gameState + .observe(viewLifecycleOwner, Observer { state -> + viewBinding.recyclerBoardTable.apply { + (layoutManager as GridLayoutManager).spanCount = + state.difficultyLevel.columnCount + (adapter as BoardGridAdapter).swapList(state.cardsList) + } + checkGameProgress(state) + }) + } + + private fun checkGameProgress(gameState: GameStateContract.StateItem) { + when (gameState.gameResult) { + SUCCESS -> { + Timber.w("Game finished as SUCCESS (${gameState.score}/${gameState.totalScore}). going to game success screen.") + findNavController().navigate(BoardScreenFragmentDirections.actionDestBoardToGameSuccessScreenFragment()) + } + FAILED -> { + Timber.w("Game finished as FAILED (${gameState.failsCount}/${gameState.difficultyLevel.errorsLimit}). going to game lost screen.") + findNavController().navigate(BoardScreenFragmentDirections.actionDestBoardToGameFailedScreenFragment()) + } + else -> { + checkCurrentStats(gameState) + if (gameState.cardsToFaceDown.isNotBlank()) { + checkCardsToBeFaceDown(gameState) + } else { + Timber.d("gameState.firstMove: ${gameState.firstMove}") + Timber.d("gameState.cardsPaired: ${gameState.cardsPaired}") + if (!gameState.firstMove) { + playSound(if (gameState.cardsPaired) R.raw.game_match else R.raw.game_flip_card) + } + Timber.i( + requireContext().getString( + R.string.text_warning_game_state, gameState.gameResult, + gameState.score, gameState.totalScore + ) + ) + } + } + } + } + + private fun checkCurrentStats(gameState: GameStateContract.StateItem) { + viewBinding.textBoardLabelLevelValue.text = when (gameState.difficultyLevel) { + GameAttributes.DifficultyLevel.VERY_EASY -> getString(R.string.text_button_difficulty_very_easy) + GameAttributes.DifficultyLevel.EASY -> getString(R.string.text_button_difficulty_easy) + GameAttributes.DifficultyLevel.NORMAL -> getString(R.string.text_button_difficulty_normal) + GameAttributes.DifficultyLevel.HARD -> getString(R.string.text_button_difficulty_hard) + } + viewBinding.textBoardLabelScore.text = + requireContext().getString(R.string.text_board_label_score, gameState.score) + viewBinding.textBoardLabelFails.apply { + visibility = if (gameState.difficultyLevel.errorsLimit > 0) View.VISIBLE else View.GONE + text = + requireContext().getString( + R.string.text_board_label_fails, + gameState.failsCount, + gameState.difficultyLevel.errorsLimit + ) + } + } + + private fun checkCardsToBeFaceDown(gameState: GameStateContract.StateItem) { + gameState.cardsToFaceDown.split(",").forEach { backedCardId -> + val foundCard = gameState.cardsList.find { it.uid == backedCardId } + android.os.Handler().postAtTime({ + Timber.d("Flip down card: $backedCardId") + (viewBinding.recyclerBoardTable.findViewHolderForAdapterPosition( + gameState.cardsList.indexOf(foundCard) + ) as BoardGridAdapter.ViewHolder).flipBack(foundCard!!) + }, 400) + } + playSound(R.raw.game_error) + } + + private fun playSound(@RawRes soundResourceId: Int) { + SingleAudioPlayer().playSound(requireContext(), soundResourceId) + } + + private fun setupBoardTable() { + viewBinding.recyclerBoardTable.apply { + setHasFixedSize(true) + layoutManager = + GridLayoutManager(requireContext(), 2, GridLayoutManager.VERTICAL, false) + adapter = BoardGridAdapter().apply { + setOnCardFlippedListener { cardUniqueId -> handleCardFlipped(cardUniqueId) } + } + } + } + + private fun handleCardFlipped(cardUniqueId: String) { + viewModel.checkFlippedCard(cardUniqueId) + } + +} diff --git a/app/src/main/java/com/example/memorygame/core/GameController.kt b/app/src/main/java/com/example/memorygame/core/GameController.kt new file mode 100644 index 0000000..6b9e965 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/core/GameController.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.core + +import com.example.memorygame.core.GameAttributes.CardItems +import com.example.memorygame.core.GameAttributes.SingleCard +import timber.log.Timber + +class GameController { + + fun generateNewDeck(rowCount: Int): List { + var position = 0 + return CardItems.randomDeck(rowCount).map { + SingleCard(card = it).apply { + position = position.inc() + generateUniqueId("card_$position") + } + } + } + + fun allCardsAreFaceDown(cardsList: List): Boolean = + cardsList.filter { it.flipped }.isNullOrEmpty() + + fun checkPairingCards( + actualList: MutableList, + secondCardId: String, + pairingCardId: String + ): Triple, Int, Int> { + if (actualList.isNullOrEmpty()) { + return Triple(actualList, -1, -1) + } + val pairingCard = actualList.find { it.uid == pairingCardId } + val selectedCard = actualList.find { it.uid == secondCardId } + Timber.d("checkPairingCards(${pairingCard!!.card.name},${selectedCard!!.card.name})") + val matchesCardNames = pairingCard.card.matches(selectedCard.card.name) + Timber.d("matchesCardNames? $matchesCardNames") + val pairingFailCount = if (!matchesCardNames) 1 else 0 + Timber.d("pairingFailCount: $pairingFailCount") + val gainedScore = if (matchesCardNames) selectedCard.card.cardScore else 0 + Timber.d("gainedScore: $gainedScore") + + val updatedList = updateActualListWithMatches( + actualList, + matchesCardNames, + pairingCardId, + secondCardId + ) + + return Triple(updatedList, gainedScore, pairingFailCount) + } + + private fun updateActualListWithMatches( + actualList: MutableList, + matchesCardNames: Boolean, + pairingCardId: String, + secondCardId: String + ): List = + actualList.map { + if (matchesCardNames) { + if (it.uid == pairingCardId && !it.flipped) { + it.flip() + } else if (it.uid == secondCardId && !it.flipped) { + it.flip() + } + } else { + if (it.uid == pairingCardId && it.flipped) { + it.flip() + } else if (it.uid == secondCardId && it.flipped) { + it.flip() + } + } + it + }.toList() +} diff --git a/app/src/main/java/com/example/memorygame/core/GameStateContract.kt b/app/src/main/java/com/example/memorygame/core/GameStateContract.kt new file mode 100644 index 0000000..6c40704 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/core/GameStateContract.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.core + +import androidx.lifecycle.MutableLiveData + +interface GameStateContract { + + enum class GameResult { + SUCCESS, FAILED, PLAYING + } + + data class StateItem( + val difficultyLevel: GameAttributes.DifficultyLevel, + private var _score: Int = 0, + private var _failsCount: Int = 0, + private var _cardWaitingPair: String = "", + private var _faceDownCardIds: String = "", + private var _cardsPaired: Boolean = false, + val cardsList: List + ) { + val firstMove: Boolean get() = _score == 0 && _failsCount == 0 && _cardWaitingPair.isBlank() + val score get() = _score + val failsCount get() = _failsCount + val pairingCardId get() = _cardWaitingPair + val cardsToFaceDown get() = _faceDownCardIds + val totalScore get() = cardsList.distinctBy { it.card.name }.sumBy { it.card.cardScore } + + private val gameSuccess: Boolean + get() = _score >= totalScore + + private val gameFailed + get() = difficultyLevel.errorsLimit in 1.._failsCount + + val cardsPaired get() = _cardsPaired + + val gameResult + get() = if (gameSuccess) GameResult.SUCCESS else if (gameFailed) GameResult.FAILED else GameResult.PLAYING + + fun computeScore(obtainedScore: Int) { + _score = _score.plus(obtainedScore) + } + + fun changePairingCard(cardUid: String) { + _cardWaitingPair = cardUid + } + + fun computeFails(failCount: Int) { + _failsCount = _failsCount.plus(failCount) + } + + fun changeCardIdsToFaceDown(faceDownCardIds: String) { + _faceDownCardIds = faceDownCardIds + } + + fun togglePaired(paired: Boolean) { + _cardsPaired = paired + } + } + + class ViewModel : androidx.lifecycle.ViewModel() { + + private val _gameState = MutableLiveData() + private val gameController = GameController() + + val gameState get() = _gameState + + fun createNewGame(difficultyLevel: GameAttributes.DifficultyLevel) { + _gameState.value = StateItem( + difficultyLevel, + 0, + 0, + "", + "", + false, + gameController.generateNewDeck(difficultyLevel.rowCount) + ) + } + + fun checkFlippedCard(cardUniqueId: String) { + val actualState = _gameState.value!! + var newStateItem: StateItem? = null + + if (gameController.allCardsAreFaceDown(actualState.cardsList) || actualState.pairingCardId.isBlank()) { + newStateItem = + actualState.copy( + _cardWaitingPair = cardUniqueId, + cardsList = actualState.cardsList.toMutableList().map { + if (cardUniqueId == it.uid && !it.flipped) { + it.flip() + } + it + }.toList() + ).apply { + changeCardIdsToFaceDown("") + togglePaired(false) + } + } else if (actualState.pairingCardId.isNotBlank()) { + val (newCardsList, gainedScore, failsCount) = gameController.checkPairingCards( + actualState.cardsList.toMutableList(), + cardUniqueId, + actualState.pairingCardId + ) + val faceDownCardIds = + if (failsCount > 0) "${actualState.pairingCardId},$cardUniqueId" else "" + val areCardsPaired = gainedScore > 0 + + newStateItem = + actualState.copy( + _cardWaitingPair = cardUniqueId, + cardsList = newCardsList + ).apply { + changePairingCard("") + changeCardIdsToFaceDown(faceDownCardIds) + computeScore(gainedScore) + computeFails(failsCount) + togglePaired(areCardsPaired) + } + } + + _gameState.value = newStateItem + } + + } +} diff --git a/app/src/main/java/com/example/memorygame/core/SingleAudioPlayer.kt b/app/src/main/java/com/example/memorygame/core/SingleAudioPlayer.kt new file mode 100644 index 0000000..ebc189a --- /dev/null +++ b/app/src/main/java/com/example/memorygame/core/SingleAudioPlayer.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.core + +import android.content.Context +import android.media.MediaPlayer +import androidx.annotation.RawRes + +class SingleAudioPlayer { + + private val mediaPlayer = MediaPlayer().apply { + setOnPreparedListener { start() } + setOnCompletionListener { + reset() + release() + } + } + + fun playSound(context: Context, @RawRes soundResourceId: Int) { + val assetFileDescriptor = + context.resources.openRawResourceFd(soundResourceId) ?: return + mediaPlayer.run { + reset() + setDataSource( + assetFileDescriptor.fileDescriptor, + assetFileDescriptor.startOffset, + assetFileDescriptor.declaredLength + ) + prepareAsync() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/core/game_model.kt b/app/src/main/java/com/example/memorygame/core/game_model.kt new file mode 100644 index 0000000..d4ed2ed --- /dev/null +++ b/app/src/main/java/com/example/memorygame/core/game_model.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.core + +import com.example.memorygame.R + + +interface GameAttributes { + + enum class DifficultyLevel(val columnCount: Int, val rowCount: Int, val errorsLimit: Int) { + VERY_EASY(2, 2, -1), + EASY(4, 4, 10), + NORMAL(4, 6, 5), + HARD(4, 8, 3); + } + + data class SingleCard( + private var _uid: String = "", + val card: CardItems, + private var _cardFlipped: Boolean = false + ) { + + val flipped + get() = _cardFlipped + + fun flip() { + _cardFlipped = !_cardFlipped + } + + val uid + get() = _uid + + fun generateUniqueId(uuid: String) { + _uid = uuid + } + } + + enum class CardItems( + private val _resource: Int, + private val _cardScore: Int + ) { + BAT(R.drawable.ic_card_bat, 4), + CAT(R.drawable.ic_card_cat, 2), + DRAGON(R.drawable.ic_card_dragon, 10), + DOG(R.drawable.ic_card_gosh_dog, 2), + COW(R.drawable.ic_card_cow, 8), + HEN(R.drawable.ic_card_hen, 6), + HORSE(R.drawable.ic_card_horse, 8), + MAN(R.drawable.ic_card_garbage_man, 12), + PIG(R.drawable.ic_card_pig, 4), + SPIDER(R.drawable.ic_card_spider, 6); + + val cardScore + get() = _cardScore + + val imageResource + get() = _resource + + + fun matches(anotherAnimalName: String): Boolean = this == valueOf(anotherAnimalName) + + companion object { + + @JvmStatic + fun getTotalPairedCardsScore(cardItems: List): Int = + cardItems.distinct().map { it.cardScore }.reduce { acc, score -> acc + score } + + @JvmStatic + fun randomDeck(rowCount: Int): List { + val limit = if (rowCount < 0) 0 else rowCount + return if (limit > 0) values().asList().shuffled().subList(0, limit).duplicated() + .shuffled() else emptyList() + } + } + + } +} + +fun Collection.duplicated(): List = this.plus(this) diff --git a/app/src/main/java/com/example/memorygame/lobby/LobbyScreenFragment.kt b/app/src/main/java/com/example/memorygame/lobby/LobbyScreenFragment.kt new file mode 100644 index 0000000..7650245 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/lobby/LobbyScreenFragment.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.lobby + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.memorygame.R +import com.example.memorygame.core.GameAttributes +import com.example.memorygame.core.GameStateContract +import com.example.memorygame.databinding.FragmentLobbyBinding +import com.example.memorygame.databinding.LayoutBottomDifficultySelectionBinding +import com.google.android.material.bottomsheet.BottomSheetDialog + +class LobbyScreenFragment : Fragment() { + + private var _viewBinding: FragmentLobbyBinding? = null + private val viewBinding get() = _viewBinding!! + + private val viewModel by navGraphViewModels(R.id.nav_graph_game) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _viewBinding = FragmentLobbyBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _viewBinding = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewBinding.buttonNewGame.setOnClickListener { + BottomSheetDialog( + requireContext(), + R.style.BottomSheetDialogTheme + ).apply { + val difficultySelectionViewBinding = + LayoutBottomDifficultySelectionBinding.inflate(this.layoutInflater) + difficultySelectionViewBinding.buttonLevelVeryEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.VERY_EASY) + } + difficultySelectionViewBinding.buttonLevelEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.EASY) + } + difficultySelectionViewBinding.buttonLevelNormal.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.NORMAL) + } + difficultySelectionViewBinding.buttonLevelHard.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.HARD) + } + this.setContentView(difficultySelectionViewBinding.root) + this.show() + } + } + } + + private fun onDifficultyLevelSelected( + dialog: BottomSheetDialog, + difficultyLevel: GameAttributes.DifficultyLevel + ) { + viewModel.createNewGame(difficultyLevel) + findNavController().navigate(LobbyScreenFragmentDirections.actionGlobalDestBoard()) + dialog.dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/results/GameFailedScreenFragment.kt b/app/src/main/java/com/example/memorygame/results/GameFailedScreenFragment.kt new file mode 100644 index 0000000..899f8f9 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/results/GameFailedScreenFragment.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.results + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.memorygame.R +import com.example.memorygame.core.GameAttributes +import com.example.memorygame.core.GameStateContract +import com.example.memorygame.databinding.FragmentGameFailedBinding +import com.example.memorygame.databinding.LayoutBottomDifficultySelectionBinding +import com.google.android.material.bottomsheet.BottomSheetDialog + +class GameFailedScreenFragment : Fragment() { + private var _viewBinding: FragmentGameFailedBinding? = null + private val viewBinding get() = _viewBinding!! + private val viewModel by navGraphViewModels(R.id.nav_graph_game) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _viewBinding = FragmentGameFailedBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _viewBinding = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewBinding.buttonHome.setOnClickListener { + findNavController().navigate(GameFailedScreenFragmentDirections.actionGlobalDestLobby()) + } + viewBinding.buttonRetry.setOnClickListener { + BottomSheetDialog( + requireContext(), + R.style.BottomSheetDialogTheme + ).apply { + val difficultySelectionViewBinding = + LayoutBottomDifficultySelectionBinding.inflate(this.layoutInflater) + difficultySelectionViewBinding.buttonLevelVeryEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.VERY_EASY) + } + difficultySelectionViewBinding.buttonLevelEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.EASY) + } + difficultySelectionViewBinding.buttonLevelNormal.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.NORMAL) + } + difficultySelectionViewBinding.buttonLevelHard.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.HARD) + } + this.setContentView(difficultySelectionViewBinding.root) + this.show() + } + } + } + + private fun onDifficultyLevelSelected( + dialog: BottomSheetDialog, + difficultyLevel: GameAttributes.DifficultyLevel + ) { + viewModel.createNewGame(difficultyLevel) + findNavController().navigate(GameFailedScreenFragmentDirections.actionGlobalDestBoard()) + dialog.dismiss() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/memorygame/results/GameSuccessScreenFragment.kt b/app/src/main/java/com/example/memorygame/results/GameSuccessScreenFragment.kt new file mode 100644 index 0000000..cbf2f19 --- /dev/null +++ b/app/src/main/java/com/example/memorygame/results/GameSuccessScreenFragment.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2020 aliceresponde. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.example.memorygame.results + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.memorygame.R +import com.example.memorygame.core.GameAttributes +import com.example.memorygame.core.GameStateContract +import com.example.memorygame.databinding.FragmentGameSuccessBinding +import com.example.memorygame.databinding.LayoutBottomDifficultySelectionBinding +import com.google.android.material.bottomsheet.BottomSheetDialog + +class GameSuccessScreenFragment : Fragment() { + private var _viewBinding: FragmentGameSuccessBinding? = null + private val viewBinding get() = _viewBinding!! + private val viewModel by navGraphViewModels(R.id.nav_graph_game) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _viewBinding = FragmentGameSuccessBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewBinding.buttonHome.setOnClickListener { + findNavController().navigate(GameSuccessScreenFragmentDirections.actionGlobalDestLobby()) + } + viewBinding.buttonRetry.setOnClickListener { + BottomSheetDialog( + requireContext(), + R.style.BottomSheetDialogTheme + ).apply { + val difficultySelectionViewBinding = + LayoutBottomDifficultySelectionBinding.inflate(this.layoutInflater) + difficultySelectionViewBinding.buttonLevelVeryEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.VERY_EASY) + } + difficultySelectionViewBinding.buttonLevelEasy.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.EASY) + } + difficultySelectionViewBinding.buttonLevelNormal.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.NORMAL) + } + difficultySelectionViewBinding.buttonLevelHard.setOnClickListener { + onDifficultyLevelSelected(this, GameAttributes.DifficultyLevel.HARD) + } + this.setContentView(difficultySelectionViewBinding.root) + this.show() + } + } + } + + private fun onDifficultyLevelSelected( + dialog: BottomSheetDialog, + difficultyLevel: GameAttributes.DifficultyLevel + ) { + viewModel.createNewGame(difficultyLevel) + findNavController().navigate(GameSuccessScreenFragmentDirections.actionGlobalDestBoard()) + dialog.dismiss() + } +} \ 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 index 2b068d1..821df93 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -1,3 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_difficulty.xml b/app/src/main/res/drawable/bg_button_difficulty.xml new file mode 100644 index 0000000..b8c8a7b --- /dev/null +++ b/app/src/main/res/drawable/bg_button_difficulty.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_lobby.xml b/app/src/main/res/drawable/bg_button_lobby.xml new file mode 100644 index 0000000..8cd2b97 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_lobby.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_retry.xml b/app/src/main/res/drawable/bg_button_retry.xml new file mode 100644 index 0000000..20cfbe9 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_retry.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_success.xml b/app/src/main/res/drawable/bg_button_success.xml new file mode 100644 index 0000000..fa09018 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_success.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_lobby.xml b/app/src/main/res/drawable/bg_lobby.xml new file mode 100644 index 0000000..59d8cd1 --- /dev/null +++ b/app/src/main/res/drawable/bg_lobby.xml @@ -0,0 +1,21 @@ + + + + \ 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 index 07d5da9..18d29fc 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,4 +1,21 @@ + + + + - + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/activity_board.xml b/app/src/main/res/layout-v21/activity_board.xml deleted file mode 100644 index 2f64e08..0000000 --- a/app/src/main/res/layout-v21/activity_board.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_board.xml b/app/src/main/res/layout/activity_board.xml deleted file mode 100644 index 228d154..0000000 --- a/app/src/main/res/layout/activity_board.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_lobby.xml b/app/src/main/res/layout/activity_lobby.xml deleted file mode 100644 index b27a4b3..0000000 --- a/app/src/main/res/layout/activity_lobby.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - -