diff --git a/android/gradle.properties b/android/gradle.properties
index bc2d95e8567abcfd41c26ebeb95fced48f43e773..818e87b23b224ced309ae5c147e5ed827826e237 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.0.1
-app.versionCode=1
+app.versionName=0.0.2
+app.versionCode=2
diff --git a/assets/icons/button_back.png b/assets/icons/button_back.png
deleted file mode 100644
index 2a802ff2d2bc0c90488a1c319288be9f28cdea5e..0000000000000000000000000000000000000000
Binary files a/assets/icons/button_back.png and /dev/null differ
diff --git a/assets/icons/button_restart.png b/assets/icons/button_restart.png
new file mode 100644
index 0000000000000000000000000000000000000000..389eabc630dcb6d973aae1b616e1783dbc414ca6
Binary files /dev/null and b/assets/icons/button_restart.png differ
diff --git a/assets/icons/button_start.png b/assets/icons/button_start.png
deleted file mode 100644
index 0a3b74a3b98f45de590a7f290b810daba7120912..0000000000000000000000000000000000000000
Binary files a/assets/icons/button_start.png and /dev/null differ
diff --git a/assets/icons/difficulty_easy.png b/assets/icons/difficulty_easy.png
deleted file mode 100644
index 66f1476dadfda62b03378a040f10737d9edbab1c..0000000000000000000000000000000000000000
Binary files a/assets/icons/difficulty_easy.png and /dev/null differ
diff --git a/assets/icons/difficulty_hard.png b/assets/icons/difficulty_hard.png
deleted file mode 100644
index 2c1aef8bb260d6e5241a0856c11cfae129c9d24e..0000000000000000000000000000000000000000
Binary files a/assets/icons/difficulty_hard.png and /dev/null differ
diff --git a/assets/icons/difficulty_medium.png b/assets/icons/difficulty_medium.png
deleted file mode 100644
index 146d9b9ca714b9c82e9a5188e88e5cc8736b893e..0000000000000000000000000000000000000000
Binary files a/assets/icons/difficulty_medium.png and /dev/null differ
diff --git a/assets/icons/empty.png b/assets/icons/empty.png
new file mode 100644
index 0000000000000000000000000000000000000000..28f81cf18e57d73b9cce53e1ae7a0525f3bdedbc
Binary files /dev/null and b/assets/icons/empty.png differ
diff --git a/assets/empty.png b/assets/skins/default_tile.png
similarity index 100%
rename from assets/empty.png
rename to assets/skins/default_tile.png
diff --git a/assets/skins/default_tile_black.png b/assets/skins/default_tile_black.png
index c9147334912e10a656a748dacd9e5ee8a51210bb..5fed42df2e38f0385dfb1bc1fe4f2c1c020d8bec 100644
Binary files a/assets/skins/default_tile_black.png and b/assets/skins/default_tile_black.png differ
diff --git a/assets/skins/default_tile_empty.png b/assets/skins/default_tile_empty.png
new file mode 100644
index 0000000000000000000000000000000000000000..586de0573e7f77d97c16942f603b8ee6d9c7d7bc
Binary files /dev/null and b/assets/skins/default_tile_empty.png differ
diff --git a/assets/skins/default_tile_white.png b/assets/skins/default_tile_white.png
index 7ac8407cc621b5931503d80be48443d44fead163..afd8939dc1e839364210e534ed908e6e0bc1a6e1 100644
Binary files a/assets/skins/default_tile_white.png and b/assets/skins/default_tile_white.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/2.txt b/fastlane/metadata/android/en-US/changelogs/2.txt
index fe56ebf91e506ee59b7d03cf8b12af27f3218d98..d636f6b3db88af84786df7618f6cc7f821f81a22 100644
--- a/fastlane/metadata/android/en-US/changelogs/2.txt
+++ b/fastlane/metadata/android/en-US/changelogs/2.txt
@@ -1 +1 @@
-Improve CI/CD, add jabber notification on create tag
+Add minimal gameplay
diff --git a/fastlane/metadata/android/en-US/changelogs/3.txt b/fastlane/metadata/android/en-US/changelogs/3.txt
deleted file mode 100644
index c4767c31cd60be21ad424c3e0b8357a7a045e229..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/en-US/changelogs/3.txt
+++ /dev/null
@@ -1 +0,0 @@
-Add minimal playable game.
diff --git a/fastlane/metadata/android/en-US/changelogs/4.txt b/fastlane/metadata/android/en-US/changelogs/4.txt
deleted file mode 100644
index 60ec69f6163967c4cd86a7c5123de298689182f8..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/en-US/changelogs/4.txt
+++ /dev/null
@@ -1 +0,0 @@
-Finalize minimal gameplay.
diff --git a/fastlane/metadata/android/en-US/changelogs/5.txt b/fastlane/metadata/android/en-US/changelogs/5.txt
deleted file mode 100644
index 8b03873cfea4fcffcd65ec9c0133ec0fe39820a3..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/en-US/changelogs/5.txt
+++ /dev/null
@@ -1 +0,0 @@
-Remove flag on cell when explored.
diff --git a/fastlane/metadata/android/en-US/changelogs/6.txt b/fastlane/metadata/android/en-US/changelogs/6.txt
deleted file mode 100644
index 9ff6c06121da32c2c920f8bc13b6304cda653493..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/en-US/changelogs/6.txt
+++ /dev/null
@@ -1 +0,0 @@
-Ensure first cell is not mined (postone put mines on board)
diff --git a/fastlane/metadata/android/fr-FR/changelogs/2.txt b/fastlane/metadata/android/fr-FR/changelogs/2.txt
index cdf380ac4f91ab3c0d9d07ac7af06ee46adfa7a2..7e81cee46211e6c898312e24098e852386290b34 100644
--- a/fastlane/metadata/android/fr-FR/changelogs/2.txt
+++ b/fastlane/metadata/android/fr-FR/changelogs/2.txt
@@ -1 +1 @@
-Amélioration de la chaîne CI/CD, envoi d'un message à la création de version
+Ajout du jeu minimal
diff --git a/fastlane/metadata/android/fr-FR/changelogs/3.txt b/fastlane/metadata/android/fr-FR/changelogs/3.txt
deleted file mode 100644
index 75d529883f8fa154d4ea9bd7307daab48f0680d0..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/fr-FR/changelogs/3.txt
+++ /dev/null
@@ -1 +0,0 @@
-Ajout du jeu minimal, jouable.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/4.txt b/fastlane/metadata/android/fr-FR/changelogs/4.txt
deleted file mode 100644
index 6f741b5ea2b0522dbf8d43262f726a9d86e4702d..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/fr-FR/changelogs/4.txt
+++ /dev/null
@@ -1 +0,0 @@
-Finalisation du jeu minimal.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/5.txt b/fastlane/metadata/android/fr-FR/changelogs/5.txt
deleted file mode 100644
index 0b46e4f89d48042f40111a1cb1678e65a7ce22f6..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/fr-FR/changelogs/5.txt
+++ /dev/null
@@ -1 +0,0 @@
-Enlève le marquage sur les cellules parcourues.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/6.txt b/fastlane/metadata/android/fr-FR/changelogs/6.txt
deleted file mode 100644
index 0c8a46bf50f92898aae7011440789c5eb94437d5..0000000000000000000000000000000000000000
--- a/fastlane/metadata/android/fr-FR/changelogs/6.txt
+++ /dev/null
@@ -1 +0,0 @@
-Impossibilité de tomber sur une mine au premier coup (reporte la génération de la grille)
diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt
index 170a1a633d68331f0ae329b7d3af80123aac1ce5..3369f597ba7389af90d58962de34112f9eb8b8f4 100644
--- a/fastlane/metadata/android/fr-FR/full_description.txt
+++ b/fastlane/metadata/android/fr-FR/full_description.txt
@@ -1 +1 @@
-Jeu du démineur simple
+Jeu du reversi simple
diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt
index 170a1a633d68331f0ae329b7d3af80123aac1ce5..3369f597ba7389af90d58962de34112f9eb8b8f4 100644
--- a/fastlane/metadata/android/fr-FR/short_description.txt
+++ b/fastlane/metadata/android/fr-FR/short_description.txt
@@ -1 +1 @@
-Jeu du démineur simple
+Jeu du reversi simple
diff --git a/fastlane/metadata/android/fr-FR/title.txt b/fastlane/metadata/android/fr-FR/title.txt
index 170a1a633d68331f0ae329b7d3af80123aac1ce5..3369f597ba7389af90d58962de34112f9eb8b8f4 100644
--- a/fastlane/metadata/android/fr-FR/title.txt
+++ b/fastlane/metadata/android/fr-FR/title.txt
@@ -1 +1 @@
-Jeu du démineur simple
+Jeu du reversi simple
diff --git a/icons/build_application_icons.sh b/icons/build_application_icons.sh
index 9a5488a87fc7e4376fde76429e03c066830ccfcb..7909dd523da1283627fabba793bdc7b458aa0d51 100755
--- a/icons/build_application_icons.sh
+++ b/icons/build_application_icons.sh
@@ -46,6 +46,7 @@ function build_asset_image() {
 function build_icons_for_skin() {
   SKIN_CODE="$1"
 
+  build_icon_for_skin ${SKIN_CODE} tile_empty
   build_icon_for_skin ${SKIN_CODE} tile_black
   build_icon_for_skin ${SKIN_CODE} tile_white
 }
@@ -58,13 +59,9 @@ function build_icon_for_skin() {
 }
 
 # Game icons
-build_asset_image ${CURRENT_DIR}/button_back.svg ${BASE_DIR}/assets/icons/button_back.png
-build_asset_image ${CURRENT_DIR}/button_start.svg ${BASE_DIR}/assets/icons/button_start.png
-build_asset_image ${CURRENT_DIR}/difficulty_easy.svg ${BASE_DIR}/assets/icons/difficulty_easy.png
-build_asset_image ${CURRENT_DIR}/difficulty_medium.svg ${BASE_DIR}/assets/icons/difficulty_medium.png
-build_asset_image ${CURRENT_DIR}/difficulty_hard.svg ${BASE_DIR}/assets/icons/difficulty_hard.png
+build_asset_image ${CURRENT_DIR}/button_restart.svg ${BASE_DIR}/assets/icons/button_restart.png
+build_asset_image ${CURRENT_DIR}/empty.svg ${BASE_DIR}/assets/icons/empty.png
 build_asset_image ${CURRENT_DIR}/game_win.svg ${BASE_DIR}/assets/icons/game_win.png
-build_asset_image ${CURRENT_DIR}/empty.svg ${BASE_DIR}/assets/empty.png
 
 # Skins
 build_icons_for_skin "default"
diff --git a/icons/button_back.svg b/icons/button_back.svg
deleted file mode 100644
index 2622a578dba53ce582afabfc587c2a85a1fb6eaa..0000000000000000000000000000000000000000
--- a/icons/button_back.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg>
diff --git a/icons/button_restart.svg b/icons/button_restart.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d18f3b4122b5df597f3be46abfd0770affb3ca6
--- /dev/null
+++ b/icons/button_restart.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><switch transform="matrix(.69484 0 0 .69484 12.09 12.096)" fill="#fefeff" stroke="#105ca1" stroke-width="5.7567"><foreignObject width="1" height="1" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/><g fill="#fefeff" stroke="#105ca1" stroke-width="5.7567"><g fill="#fefeff" stroke="#105ca1" stroke-width="5.7567"><path d="m43.3 91.8-18.3 5.5c-2.5 0.7-5.1-0.7-5.9-3.1-0.7-2.5 0.7-5.1 3.1-5.9l6.8-2c-12.4-7.3-20.8-20.8-20.8-36.3 0-17.3 10.6-32.2 25.6-38.6 3.1-1.3 6.6 1 6.6 4.3 0 1.9-1.1 3.6-2.8 4.3-11.8 5-20 16.5-20 30 0 12.2 6.8 22.9 16.8 28.4l-2.4-8.1c-0.7-2.5 0.7-5.1 3.1-5.9 2.5-0.7 5.1 0.7 5.9 3.1l5.5 18.3c0.7 2.6-0.7 5.2-3.2 6z"/><path d="m77.7 11.7-6.8 2c12.5 7.3 20.9 20.8 20.9 36.3 0 17.3-10.6 32.2-25.6 38.6-3.1 1.3-6.6-1-6.6-4.3 0-1.9 1.1-3.6 2.8-4.3 11.8-5 20-16.5 20-30 0-12.2-6.8-22.9-16.8-28.4l2.4 8.1c0.7 2.5-0.7 5.1-3.1 5.9-2.5 0.7-5.1-0.7-5.9-3.1l-5.5-18.3c-0.7-2.5 0.7-5.1 3.2-5.9l18.3-5.6c2.5-0.7 5.1 0.7 5.9 3.1 0.7 2.5-0.7 5.2-3.2 5.9z"/></g></g></switch></svg>
diff --git a/icons/button_start.svg b/icons/button_start.svg
deleted file mode 100644
index e9d49d2172b9a0305db82779971e3c1e12f34a70..0000000000000000000000000000000000000000
--- a/icons/button_start.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg>
diff --git a/icons/difficulty_easy.svg b/icons/difficulty_easy.svg
deleted file mode 100644
index da0e21f6c94972d3e3f19d21cd44d4f712e3174d..0000000000000000000000000000000000000000
--- a/icons/difficulty_easy.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#41ff6a" stroke="#000" stroke-width="2"/><path d="m23.408 53.415 20.8 13.7c0.2 0.1 0.3 0.2 0.5 0.3 1.3 0.5 2.8-0.1 3.3-1.3 0.5-1.3-0.1-2.8-1.3-3.3l-23.2-9.6c-0.2 0-0.3 0.2-0.1 0.2zm23.7 11.6c0.1 0.8-0.5 1.4-1.3 1.5s-1.4-0.5-1.5-1.3 0.5-1.4 1.3-1.5c0.8 0 1.5 0.5 1.5 1.3z"/><path d="m41.708 36.515c0.1 0.4 0.6 0.7 1 0.7 0.9-0.1 1.9-0.1 2.8-0.1 1.5 0 2.9 0.1 4.3 0.3 0.6 0.1 1.2-0.4 1.2-1v-6.9c0-0.6-0.5-1-1-1-3.2 0.1-6.4 0.7-9.3 1.6-0.5 0.2-0.8 0.7-0.6 1.3z"/><path d="m52.508 37.815c2.1 0.5 4.2 1.3 6.2 2.2 0.5 0.3 1.2 0 1.4-0.6l2.7-8.2c0.2-0.5-0.1-1.1-0.6-1.3-3-0.9-6.1-1.5-9.3-1.6-0.6 0-1 0.4-1 1v7.5c-0.1 0.5 0.2 0.9 0.6 1z"/><path d="m81.508 46.115-8.4 6.1c-0.4 0.3-0.5 0.8-0.3 1.3 0.9 1.6 1.6 3.2 2.2 4.9 0.2 0.5 0.7 0.8 1.2 0.6l9.8-3.2c0.5-0.2 0.8-0.7 0.6-1.3-1-2.9-2.3-5.7-3.8-8.2-0.2-0.4-0.9-0.6-1.3-0.2z"/><path d="m32.908 40.015c2.3-1.1 4.7-2 7.2-2.5 0.6-0.1 1-0.7 0.8-1.3l-1.5-4.7c-0.2-0.5-0.8-0.8-1.3-0.6-2.9 1.2-5.7 2.7-8.2 4.6-0.4 0.3-0.5 0.9-0.2 1.4l2 2.8c0.2 0.4 0.8 0.5 1.2 0.3z"/><path d="m63.408 31.615-2.8 8.6c-0.1 0.4 0 0.9 0.4 1.1 1.7 1 3.4 2.2 4.9 3.6 0.4 0.4 1.1 0.3 1.5-0.1l5.7-7.9c0.3-0.4 0.2-1.1-0.2-1.4-2.5-1.9-5.3-3.4-8.2-4.6-0.5-0.2-1.1 0.1-1.3 0.7z"/><path d="m67.908 46.815c1.3 1.4 2.5 2.8 3.5 4.4 0.3 0.5 0.9 0.6 1.4 0.3l8.3-6c0.4-0.3 0.5-0.9 0.2-1.4-1.8-2.5-3.9-4.8-6.2-6.8-0.4-0.4-1.1-0.3-1.5 0.2l-5.9 8.1c-0.2 0.3-0.2 0.8 0.2 1.2z"/><path d="m24.408 46.315c1.8-2 3.9-3.7 6.2-5.1 0.5-0.3 0.6-1 0.3-1.4l-1.8-2.4c-0.3-0.5-1-0.6-1.5-0.2-2.3 2-4.4 4.3-6.2 6.8-0.3 0.4-0.2 1.1 0.2 1.4l1.4 1c0.5 0.3 1 0.3 1.4-0.1z"/><path d="m13.808 54.815 6 2c0.5 0.2 1-0.1 1.2-0.6 1.1-2.6 2.2-4.3 3.9-6.4 0.4-0.4 0.3-1.1-0.2-1.4l-5.9-4.2c-0.5-0.3-1.1-0.2-1.4 0.3-1.6 2.5-3.2 6.1-4.1 9-0.3 0.6 0 1.2 0.5 1.3z"/><path d="m75.808 60.915c0.4 1.7 0.7 3.5 0.9 5.3 0 0.5 0.5 0.9 1 0.9h10.2c0.6 0 1-0.5 1-1-0.1-3.1-0.5-6-1.3-8.9-0.1-0.6-0.7-0.9-1.3-0.7l-9.8 3.2c-0.6 0.2-0.9 0.7-0.7 1.2z"/><path d="m15.808 67.115c0.1-3.2 0.7-6.3 1.7-9.2 0.2-0.5-0.1-1.1-0.6-1.3l-0.4-0.1c-0.5-0.2-1.1 0.1-1.3 0.7-0.8 3.2-1.3 6.5-1.3 9.9z"/></svg>
diff --git a/icons/difficulty_hard.svg b/icons/difficulty_hard.svg
deleted file mode 100644
index 254346afe6f2b8c3cf79ee079881802f863caee9..0000000000000000000000000000000000000000
--- a/icons/difficulty_hard.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#d31158" stroke="#000" stroke-width="2"/><path d="m69.154 59.254-24.4 5.1c-0.2 0-0.4 0.1-0.5 0.1-1.3 0.6-1.9 2-1.4 3.3 0.6 1.3 2 1.9 3.3 1.4l23-9.7c0.2 0 0.1-0.2 0-0.2zm-25 8.7c-0.6-0.5-0.6-1.4-0.1-2s1.4-0.6 2-0.1 0.6 1.4 0.1 2c-0.5 0.5-1.4 0.6-2 0.1z"/><path d="m41.054 38.354c0.1 0.4 0.6 0.7 1 0.7 0.9-0.1 1.9-0.1 2.8-0.1 1.5 0 2.9 0.1 4.3 0.3 0.6 0.1 1.2-0.4 1.2-1v-6.9c0-0.6-0.5-1-1-1-3.2 0.1-6.4 0.7-9.3 1.6-0.5 0.2-0.8 0.7-0.6 1.3z"/><path d="m51.854 39.754c2.1 0.5 4.2 1.3 6.2 2.2 0.5 0.3 1.2 0 1.4-0.6l2.7-8.2c0.2-0.5-0.1-1.1-0.6-1.3-3-0.9-6.1-1.5-9.3-1.6-0.6 0-1 0.4-1 1v7.5c-0.2 0.5 0.1 0.9 0.6 1z"/><path d="m82.754 46.554-12.2 8.9c-0.4 0.3-0.5 0.8-0.3 1.3 0.9 1.6 1.2 2.6 1.8 4.3 0.2 0.5 0.7 0.8 1.2 0.6l14.4-4.7c0.5-0.2 0.8-0.7 0.6-1.3-1-2.9-2.6-6.4-4.1-8.9-0.3-0.5-1-0.6-1.4-0.2z"/><path d="m32.154 41.854c2.3-1.1 4.7-2 7.2-2.5 0.6-0.1 1-0.7 0.8-1.3l-1.5-4.7c-0.2-0.5-0.8-0.8-1.3-0.6-2.9 1.2-5.7 2.7-8.2 4.6-0.4 0.3-0.5 0.9-0.2 1.4l2 2.8c0.3 0.4 0.8 0.5 1.2 0.3z"/><path d="m62.754 33.454-2.8 8.6c-0.1 0.4 0 0.9 0.4 1.1 1.7 1 3.4 2.2 4.9 3.6 0.4 0.4 1.1 0.3 1.5-0.1l5.7-7.9c0.3-0.4 0.2-1.1-0.2-1.4-2.5-1.9-5.3-3.4-8.2-4.6-0.5-0.1-1.1 0.2-1.3 0.7z"/><path d="m67.154 48.654c1.3 1.4 2.5 2.8 3.5 4.4 0.3 0.5 0.9 0.6 1.4 0.3l8.3-6c0.4-0.3 0.5-0.9 0.2-1.4-1.8-2.5-3.9-4.8-6.2-6.8-0.4-0.4-1.1-0.3-1.5 0.2l-5.9 8.1c-0.2 0.3-0.1 0.9 0.2 1.2z"/><path d="m23.654 48.154c1.8-2 3.9-3.7 6.2-5.1 0.5-0.3 0.6-1 0.3-1.4l-1.7-2.4c-0.3-0.5-1-0.6-1.5-0.2-2.3 2-4.4 4.3-6.2 6.8-0.3 0.4-0.2 1.1 0.2 1.4l1.4 1c0.4 0.4 1 0.3 1.3-0.1z"/><path d="m15.954 57.654 0.5 0.2c0.5 0.2 1-0.1 1.2-0.6 1.1-2.6 2.5-5 4.2-7.1 0.4-0.4 0.3-1.1-0.2-1.4l-1.2-0.8c-0.3-0.4-1-0.2-1.3 0.3-1.6 2.5-2.9 5.3-3.8 8.2-0.2 0.5 0.1 1.1 0.6 1.2z"/><path d="m75.054 62.754c0.4 1.7 0.7 3.5 0.9 5.3 0 0.5 0.5 0.9 1 0.9h10.2c0.6 0 1-0.5 1-1-0.1-3.1-0.5-6-1.3-8.9-0.1-0.6-0.7-0.9-1.3-0.7l-9.8 3.2c-0.5 0.2-0.8 0.7-0.7 1.2z"/><path d="m15.054 68.954c0.1-3.2 0.7-6.3 1.7-9.2 0.2-0.5-0.1-1.1-0.6-1.3l-0.3-0.1c-0.5-0.2-1.1 0.1-1.3 0.7-0.8 3.2-1.3 6.5-1.3 9.9z"/></svg>
diff --git a/icons/difficulty_medium.svg b/icons/difficulty_medium.svg
deleted file mode 100644
index a6a8c5565ce3937fdb73613f7f53e14f1c888630..0000000000000000000000000000000000000000
--- a/icons/difficulty_medium.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#eeb517" stroke="#000" stroke-width="2"/><path d="m53.928 43.009-11 21.7c-0.1 0.2-0.1 0.3-0.2 0.5-0.4 1.4 0.4 2.7 1.7 3.2 1.4 0.4 2.7-0.4 3.2-1.7l6.6-23.7c0-0.1-0.2-0.2-0.3 0zm-8.5 24.3c-0.8 0.1-1.5-0.4-1.6-1.1-0.1-0.8 0.4-1.5 1.1-1.6 0.8-0.1 1.5 0.4 1.6 1.1 0.1 0.8-0.4 1.5-1.1 1.6z"/><path d="m41.128 37.409c0.1 0.4 0.6 0.7 1 0.7 0.9-0.1 1.9-0.1 2.8-0.1 1.5 0 2.9 0.1 4.3 0.3 0.6 0.1 1.2-0.4 1.2-1v-6.9c0-0.6-0.5-1-1-1-3.2 0.1-6.4 0.7-9.3 1.6-0.5 0.2-0.8 0.7-0.6 1.3z"/><path d="m51.928 41.209c2.1 0.5 3.5 1 5.5 2 0.5 0.3 1.2 0 1.4-0.6l4.2-12.8c0.2-0.5-0.1-1.1-0.6-1.3-3-0.9-6.9-1.5-10.1-1.7-0.6 0-1 0.4-1 1v12.4c-0.1 0.5 0.2 0.9 0.6 1z"/><path d="m80.928 47.009-8.4 6.1c-0.4 0.3-0.5 0.8-0.3 1.3 0.9 1.6 1.6 3.2 2.2 4.9 0.2 0.5 0.7 0.8 1.2 0.6l9.8-3.2c0.5-0.2 0.8-0.7 0.6-1.3-1-2.9-2.3-5.7-3.8-8.2-0.2-0.4-0.9-0.5-1.3-0.2z"/><path d="m32.328 40.909c2.3-1.1 4.7-2 7.2-2.5 0.6-0.1 1-0.7 0.8-1.3l-1.5-4.7c-0.2-0.5-0.8-0.8-1.3-0.6-2.9 1.2-5.7 2.7-8.2 4.6-0.4 0.3-0.5 0.9-0.2 1.4l2 2.8c0.2 0.4 0.8 0.5 1.2 0.3z"/><path d="m62.828 32.509-2.8 8.6c-0.1 0.4 0 0.9 0.4 1.1 1.7 1 3.4 2.2 4.9 3.6 0.4 0.4 1.1 0.3 1.5-0.1l5.7-7.9c0.3-0.4 0.2-1.1-0.2-1.4-2.5-1.9-5.3-3.4-8.2-4.6-0.5-0.1-1.1 0.2-1.3 0.7z"/><path d="m67.328 47.709c1.3 1.4 2.5 2.8 3.5 4.4 0.3 0.5 0.9 0.6 1.4 0.3l8.3-6c0.4-0.3 0.5-0.9 0.2-1.4-1.8-2.5-3.9-4.8-6.2-6.8-0.4-0.4-1.1-0.3-1.5 0.2l-5.8 8.1c-0.3 0.3-0.3 0.9 0.1 1.2z"/><path d="m23.828 47.209c1.8-2 3.9-3.7 6.2-5.1 0.5-0.3 0.6-1 0.3-1.4l-1.8-2.4c-0.3-0.5-1-0.6-1.5-0.2-2.3 2-4.4 4.3-6.2 6.8-0.3 0.4-0.2 1.1 0.2 1.4l1.4 1c0.5 0.4 1 0.3 1.4-0.1z"/><path d="m16.128 56.709 0.5 0.2c0.5 0.2 1-0.1 1.2-0.6 1.1-2.6 2.5-5 4.2-7.1 0.4-0.4 0.3-1.1-0.2-1.4l-1.2-0.8c-0.5-0.3-1.1-0.2-1.4 0.3-1.6 2.5-2.9 5.3-3.8 8.2-0.1 0.5 0.2 1.1 0.7 1.2z"/><path d="m75.228 61.809c0.4 1.7 0.7 3.5 0.9 5.3 0 0.5 0.5 0.9 1 0.9h10.2c0.6 0 1-0.5 1-1-0.1-3.1-0.5-6-1.3-8.9-0.1-0.6-0.7-0.9-1.3-0.7l-9.8 3.2c-0.6 0.2-0.8 0.7-0.7 1.2z"/><path d="m15.228 68.009c0.1-3.2 0.7-6.3 1.7-9.2 0.2-0.5-0.1-1.1-0.6-1.3l-0.3-0.1c-0.5-0.2-1.1 0.1-1.3 0.7-0.8 3.2-1.3 6.5-1.3 9.9z"/></svg>
diff --git a/icons/empty.svg b/icons/empty.svg
index 6b7cca72d5b5fe5d116270f4c6d4aa081402fbee..d988c4937ec1d00ce581b412ff4a859a0fd1862d 100644
--- a/icons/empty.svg
+++ b/icons/empty.svg
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/></svg>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/>
diff --git a/icons/skins/default/tile_black.svg b/icons/skins/default/tile_black.svg
index 76875fdd51faa445efb93d3e4f71067695c895d9..9bc87447b02e8879399d554e0badab996c28c541 100644
--- a/icons/skins/default/tile_black.svg
+++ b/icons/skins/default/tile_black.svg
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><circle cx="50" cy="50" r="42.259" stroke="#919191" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.37"/></svg>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><circle cx="50" cy="50" r="42.259" stroke="#313131" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/></svg>
diff --git a/icons/skins/default/tile_empty.svg b/icons/skins/default/tile_empty.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6b7cca72d5b5fe5d116270f4c6d4aa081402fbee
--- /dev/null
+++ b/icons/skins/default/tile_empty.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/></svg>
diff --git a/icons/skins/default/tile_white.svg b/icons/skins/default/tile_white.svg
index a41d136e9cfa3c17ee63e1064e63890420512299..e26fc614bb65a4814727f6280495ad602807c934 100644
--- a/icons/skins/default/tile_white.svg
+++ b/icons/skins/default/tile_white.svg
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><circle cx="50" cy="50" r="42.259" fill="#fff" stroke="#919191" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.37"/></svg>
+<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" ry="2" fill="none"/><circle cx="50" cy="50" r="42.259" fill="#fff" stroke="#ececec" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/></svg>
diff --git a/lib/game_board.dart b/lib/game_board.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1591fbae261f9f857822bd263f4b9b7a376717fa
--- /dev/null
+++ b/lib/game_board.dart
@@ -0,0 +1,255 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+enum PieceType {
+  empty,
+  black,
+  white,
+}
+
+/// This method flips a black piece to a white one, and vice versa. I'm still
+/// unsure about having it as a global function, but don't know where else to
+/// put it.
+PieceType getOpponent(PieceType player) =>
+    (player == PieceType.black) ? PieceType.white : PieceType.black;
+
+/// A position on the reversi board. Just an [x] and [y] coordinate pair.
+class Position {
+  final int x;
+  final int y;
+
+  const Position(this.x, this.y);
+}
+
+/// An immutable representation of a reversi game's board.
+class GameBoard {
+  static final int height = 8;
+  static final int width = 8;
+  final List<List<PieceType>> rows;
+
+  // Because calculating out all the available moves for a player can be
+  // expensive, they're cached here.
+  final _availableMoveCache = <PieceType, List<Position>>{};
+
+  /// Default constructor, which creates a board with pieces in starting
+  /// position.
+  GameBoard() : rows = _emptyBoard;
+
+  /// Copy constructor.
+  GameBoard.fromGameBoard(GameBoard other)
+      : rows = List.generate(height, (i) => List.from(other.rows[i]));
+
+  /// Retrieves the type of piece at a location on the game board.
+  PieceType getPieceAtLocation(int x, int y) {
+    assert(x >= 0 && x < width);
+    assert(y >= 0 && y < height);
+    return rows[y][x];
+  }
+
+  /// Gets the total number of pieces of a particular type.
+  int getPieceCount(PieceType pieceType) {
+    return rows.fold(
+      0,
+      (s, e) => s + e.where((e) => e == pieceType).length,
+    );
+  }
+
+  /// Calculates the list of available moves on this board for a player. These
+  /// moves are calculated for the first call and cached for any subsequent
+  /// ones.
+  List<Position> getMovesForPlayer(PieceType player) {
+    if (player == PieceType.empty) {
+      return [];
+    }
+
+    if (_availableMoveCache.containsKey(player)) {
+      return _availableMoveCache[player]!;
+    }
+
+    final legalMoves = <Position>[];
+    for (var x = 0; x < width; x++) {
+      for (var y = 0; y < width; y++) {
+        if (isLegalMove(x, y, player)) {
+          legalMoves.add(Position(x, y));
+        }
+      }
+    }
+
+    _availableMoveCache[player] = legalMoves;
+    return legalMoves;
+  }
+
+  /// Returns a new GameBoard instance representing the state this one would
+  /// have after [player] puts a piece at [x],[y]. This method does not check if
+  /// the move was legal, and will blindly trust its input.
+  GameBoard updateForMove(int x, int y, PieceType player) {
+    assert(player != PieceType.empty);
+    final newBoard = GameBoard.fromGameBoard(this);
+
+    if (!isLegalMove(x, y, player)) {
+      return newBoard;
+    }
+
+    newBoard.rows[y][x] = player;
+
+    for (var dx = -1; dx <= 1; dx++) {
+      for (var dy = -1; dy <= 1; dy++) {
+        if (dx == 0 && dy == 0) continue;
+        newBoard._traversePath(x, y, dx, dy, player, true);
+      }
+    }
+
+    return newBoard;
+  }
+
+  /// Returns true if it would be a legal move for [player] to put a piece down
+  /// at [x],[y].
+  bool isLegalMove(int x, int y, PieceType player) {
+    assert(player != PieceType.empty);
+    assert(x >= 0 && x < width);
+    assert(y >= 0 && y < height);
+
+    // It's occupied, yo. No can do.
+    if (rows[y][x] != PieceType.empty) return false;
+
+    // Try each of the eight cardinal directions, looking for a row of opposing
+    // pieces to flip.
+    for (var dx = -1; dx <= 1; dx++) {
+      for (var dy = -1; dy <= 1; dy++) {
+        if (dx == 0 && dy == 0) continue;
+        if (_traversePath(x, y, dx, dy, player, false)) return true;
+      }
+    }
+
+    // No flippable opponent pieces were found in any directions. This is not a
+    // legal move.
+    return false;
+  }
+
+  // This method walks the board in one of eight cardinal directions (determined
+  // by the [dx] and [dy] parameters) beginning at [x],[y], and attempts to
+  // determine if a move at [x],[y] by [player] would result in pieces getting
+  // flipped. If so, the method returns true, otherwise false. If [flip] is set
+  // to true, the pieces are flipped in place to their new colors before the
+  // method returns.
+  bool _traversePath(
+      int x, int y, int dx, int dy, PieceType player, bool flip) {
+    var foundOpponent = false;
+    var curX = x + dx;
+    var curY = y + dy;
+
+    while (curX >= 0 && curX < width && curY >= 0 && curY < height) {
+      if (rows[curY][curX] == PieceType.empty) {
+        // This path led to an empty spot rather than a legal move.
+        return false;
+      } else if (rows[curY][curX] == getOpponent(player)) {
+        // Update flag and keep going, hoping to hit one of player's pieces.
+        foundOpponent = true;
+      } else if (foundOpponent) {
+        // Found opposing pieces and then one of player's afterward. This is
+        // a legal move.
+        if (flip) {
+          // Backtrack, flipping pieces to player's color.
+          while (curX != x || curY != y) {
+            curX -= dx;
+            curY -= dy;
+            rows[curY][curX] = player;
+          }
+        }
+        return true;
+      } else {
+        // Found one of player's pieces, but no opposing pieces.
+        return false;
+      }
+
+      curX += dx;
+      curY += dy;
+    }
+
+    return false;
+  }
+}
+
+const _emptyBoard = [
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.black,
+    PieceType.white,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.white,
+    PieceType.black,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+  [
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+    PieceType.empty,
+  ],
+];
diff --git a/lib/game_board_scorer.dart b/lib/game_board_scorer.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ac314aa27145fc1e1a058e478025bdfab36bae62
--- /dev/null
+++ b/lib/game_board_scorer.dart
@@ -0,0 +1,64 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'game_board.dart';
+
+class GameBoardScorer {
+  // Values for each position on the board.
+  static const _positionValues = [
+    [10000, -1000, 100, 100, 100, 100, -1000, 10000],
+    [-1000, -1000, 1, 1, 1, 1, -1000, -1000],
+    [100, 1, 50, 50, 50, 50, 1, 100],
+    [100, 1, 50, 1, 1, 50, 1, 100],
+    [100, 1, 50, 1, 1, 50, 1, 100],
+    [100, 1, 50, 50, 50, 50, 1, 100],
+    [-1000, -1000, 1, 1, 1, 1, -1000, -1000],
+    [10000, -1000, 100, 100, 100, 100, -1000, 10000],
+  ];
+
+  /// Maximum and minimum values for scores, which are used in the minimax
+  /// algorithm in [MoveFinder].
+  static const maxScore = 1000 * 1000 * 1000;
+  static const minScore = -1 * maxScore;
+
+  final GameBoard board;
+
+  GameBoardScorer(this.board);
+
+  /// Returns the score of the board, as determined by what pieces are in place,
+  /// and how valuable their locations are. This is a very simple scoring
+  /// heuristic, but it's surprisingly effective.
+  int getScore(PieceType player) {
+    assert(player != PieceType.empty);
+    var opponent = getOpponent(player);
+    var score = 0;
+
+    if (board.getMovesForPlayer(PieceType.black).isEmpty &&
+        board.getMovesForPlayer(PieceType.white).isEmpty) {
+      // Game is over.
+      var playerCount = board.getPieceCount(player);
+      var opponentCount = board.getPieceCount(getOpponent(player));
+
+      if (playerCount > opponentCount) {
+        return maxScore;
+      } else if (playerCount < opponentCount) {
+        return minScore;
+      } else {
+        return 0;
+      }
+    }
+
+    for (var y = 0; y < GameBoard.height; y++) {
+      for (var x = 0; x < GameBoard.width; x++) {
+        if (board.getPieceAtLocation(x, y) == player) {
+          score += _positionValues[y][x];
+        } else if (board.getPieceAtLocation(x, y) == opponent) {
+          score -= _positionValues[y][x];
+        }
+      }
+    }
+
+    return score;
+  }
+}
diff --git a/lib/game_model.dart b/lib/game_model.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bb95262e3c46353a1d12ad96fb6e91fcc4255c3b
--- /dev/null
+++ b/lib/game_model.dart
@@ -0,0 +1,58 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'game_board.dart';
+
+/// A model representing the state of a game of reversi. It's a board and the
+/// player who's next to go, essentially.
+class GameModel {
+  late final GameBoard board;
+  final PieceType player;
+  final String _skin = 'default';
+
+  GameModel({
+    required this.board,
+    this.player = PieceType.black,
+  });
+
+  String get skin => _skin;
+
+  int get blackScore => board.getPieceCount(PieceType.black);
+
+  int get whiteScore => board.getPieceCount(PieceType.white);
+
+  bool get gameIsOver => (board.getMovesForPlayer(player).isEmpty);
+
+  String get gameResultString {
+    if (blackScore > whiteScore) {
+      return 'Black wins.';
+    } else if (whiteScore > blackScore) {
+      return 'White wins.';
+    } else {
+      return 'Tie.';
+    }
+  }
+
+  /// Attempts to create a new instance of GameModel using the coordinates
+  /// provided as the current player's move. If successful, a new GameModel is
+  /// returned. If unsuccessful, null is returned.
+  GameModel updateForMove(int x, int y) {
+    if (!board.isLegalMove(x, y, player)) {
+      return null!;
+    }
+
+    final newBoard = board.updateForMove(x, y, player);
+    PieceType nextPlayer;
+
+    if (newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) {
+      nextPlayer = getOpponent(player);
+    } else if (newBoard.getMovesForPlayer(player).isNotEmpty) {
+      nextPlayer = player;
+    } else {
+      nextPlayer = PieceType.empty;
+    }
+
+    return GameModel(board: newBoard, player: nextPlayer);
+  }
+}
diff --git a/lib/main.dart b/lib/main.dart
index dd1afff2ab481d34fc88bd2e0751f294b17f42ed..75b66dec53ff27807ac716bfa2717d328bffcbad 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,34 +1,324 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:provider/provider.dart';
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
 
-import 'provider/data.dart';
-import 'screens/home.dart';
+import 'dart:async';
 
+import 'package:async/async.dart';
+import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation;
+import 'package:flutter/widgets.dart';
+
+import 'game_board.dart';
+import 'game_model.dart';
+import 'move_finder.dart';
+import 'styling.dart';
+import 'thinking_indicator.dart';
+
+/// Main function for the app. Turns off the system overlays and locks portrait
+/// orientation for a more game-like UI, and then runs the [Widget] tree.
 void main() {
   WidgetsFlutterBinding.ensureInitialized();
-  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
-      .then((value) => runApp(MyApp()));
+
+  SystemChrome.setEnabledSystemUIOverlays([]);
+  SystemChrome.setPreferredOrientations([
+    DeviceOrientation.portraitUp,
+    DeviceOrientation.portraitDown,
+  ]);
+
+  runApp(FlutterFlipApp());
 }
 
-class MyApp extends StatelessWidget {
+/// The App class. Unlike many Flutter apps, this one does not use Material
+/// widgets, so there's no [MaterialApp] or [Theme] objects.
+class FlutterFlipApp extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return WidgetsApp(
+      color: Color(0xffffffff), // Mandatory background color.
+      onGenerateRoute: (settings) {
+        return PageRouteBuilder<dynamic>(
+          settings: settings,
+          pageBuilder: (context, animation, secondaryAnimation) => GameScreen(),
+        );
+      },
+    );
+  }
+}
+
+/// The [GameScreen] Widget represents the entire game
+/// display, from scores to board state and everything in between.
+class GameScreen extends StatefulWidget {
+  @override
+  State createState() => _GameScreenState();
+}
+
+/// State class for [GameScreen].
+///
+/// The game is modeled as a [Stream] of immutable instances of [GameModel].
+/// Each move by the player or CPU results in a new [GameModel], which is
+/// sent downstream. [GameScreen] uses a [StreamBuilder] wired up to that stream
+/// of models to build out its [Widget] tree.
+class _GameScreenState extends State<GameScreen> {
+  final StreamController<GameModel> _userMovesController =
+      StreamController<GameModel>();
+  final StreamController<GameModel> _restartController =
+      StreamController<GameModel>();
+  Stream<GameModel>? _modelStream;
+
+  _GameScreenState() {
+    // Below is the combination of streams that controls the flow of the game.
+    // There are two streams of models produced by player interaction (either by
+    // restarting the game, which produces a brand new game model and sends it
+    // downstream, or tapping on one of the board locations to play a piece, and
+    // which creates a new board model with the result of the move and sends it
+    // downstream. The StreamGroup combines these into a single stream, then
+    // does a little trick with asyncExpand.
+    //
+    // The function used in asyncExpand checks to see if it's the CPU's turn
+    // (white), and if so creates a [MoveFinder] to look for the best move. It
+    // awaits the calculation, and then creates a new [GameModel] with the
+    // result of that move and sends it downstream by yielding it. If it's still
+    // the CPU's turn after making that move (which can happen in reversi), this
+    // is repeated.
+    //
+    // The final stream of models that exits the asyncExpand call is a
+    // combination of "new game" models, models with the results of player
+    // moves, and models with the results of CPU moves. These are fed into the
+    // StreamBuilder in [build], and used to create the widgets that comprise
+    // the game's display.
+    _modelStream = StreamGroup.merge([
+      _userMovesController.stream,
+      _restartController.stream,
+    ]).asyncExpand((model) async* {
+      yield model;
+
+      var newModel = model;
+
+      while (newModel.player == PieceType.white) {
+        final finder = MoveFinder(newModel.board);
+        final move = await finder.findNextMove(newModel.player, 5);
+        if (move != null) {
+          newModel = newModel.updateForMove(move.x, move.y);
+          yield newModel;
+        }
+      }
+    });
+  }
+
+  // Thou shalt tidy up thy stream controllers.
+  @override
+  void dispose() {
+    _userMovesController.close();
+    _restartController.close();
+    super.dispose();
+  }
+
+  /// The build method mostly just sets up the StreamBuilder and leaves the
+  /// details to _buildWidgets.
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (BuildContext context) => Data(),
-      child: Consumer<Data>(builder: (context, data, child) {
-        return MaterialApp(
-          debugShowCheckedModeBanner: false,
-          theme: ThemeData(
-            primaryColor: Colors.blue,
-            visualDensity: VisualDensity.adaptivePlatformDensity,
+    return StreamBuilder<GameModel>(
+      stream: _modelStream,
+      builder: (context, snapshot) {
+        return _buildWidgets(
+          context,
+          snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()),
+        );
+      },
+    );
+  }
+
+  // Called when the user taps on the game's board display. If it's the player's
+  // turn, this method will attempt to make the move, creating a new GameModel
+  // in the process.
+  void _attemptUserMove(GameModel model, int x, int y) {
+    if (model.player == PieceType.black &&
+        model.board.isLegalMove(x, y, model.player)) {
+      _userMovesController.add(model.updateForMove(x, y));
+    }
+  }
+
+  Widget _buildScoreBox(PieceType player, GameModel model) {
+    var assetImageCode = player == PieceType.black ? 'black' : 'white';
+    String assetImageName = 'assets/skins/' + model.skin + '_tile_' + assetImageCode + '.png';
+
+    var scoreText = player == PieceType.black
+        ? '${model.blackScore}'
+        : '${model.whiteScore}';
+
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        vertical: 3.0,
+        horizontal: 30.0,
+      ),
+      decoration: (model.player == player)
+          ? Styling.activePlayerIndicator
+          : Styling.inactivePlayerIndicator,
+      child: Row(
+        children: <Widget>[
+          SizedBox(
+            width: 30.0,
+            height: 30.0,
+            child: Image(
+              image: AssetImage(assetImageName),
+              fit: BoxFit.fill,
+            ),
           ),
-          home: Home(),
-          routes: {
-            Home.id: (context) => Home(),
-          },
+          SizedBox(
+            width: 10.0,
+          ),
+          Text(
+            scoreText,
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              fontSize: 50.0,
+              color: Color(0xff000000),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildScoreBoxes(GameModel model) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        Spacer(flex: 1),
+        _buildScoreBox(PieceType.black, model),
+        Spacer(flex: 4),
+        _buildScoreBox(PieceType.white, model),
+        Spacer(flex: 1),
+      ],
+    );
+  }
+
+  Table _buildGameBoardDisplay(BuildContext context, GameModel model) {
+    final rows = <TableRow>[];
+
+    for (var y = 0; y < GameBoard.height; y++) {
+      final cells = <Column>[];
+
+      for (var x = 0; x < GameBoard.width; x++) {
+        PieceType pieceType = model.board.getPieceAtLocation(x, y);
+        String? assetImageCode = Styling.assetImageCodes[pieceType];
+        String assetImageName = 'assets/skins/' + model.skin + '_tile_' + (assetImageCode != null ? assetImageCode : 'empty') + '.png';
+
+        Column cell = Column(
+          children: [
+            Container(
+              decoration: Styling.boardCellDecoration,
+              child: SizedBox(
+                child: GestureDetector(
+                  child: AnimatedSwitcher(
+                    duration: const Duration(milliseconds: 500),
+                    transitionBuilder: (Widget child, Animation<double> animation) {
+                      return ScaleTransition(child: child, scale: animation);
+                    },
+                    child: Image(
+                      image: AssetImage(assetImageName),
+                      fit: BoxFit.fill,
+                      key: ValueKey<int>(pieceType == PieceType.empty ? 0 : (pieceType == PieceType.black ? 1 : 2)),
+                    ),
+                  ),
+                  onTap: () {
+                    _attemptUserMove(model, x, y);
+                  },
+                ),
+              ),
+            )
+          ]
         );
-      }),
+
+        cells.add(cell);
+      }
+
+      rows.add(TableRow(children: cells));
+    }
+
+    return Table(
+      defaultColumnWidth: IntrinsicColumnWidth(),
+      children: rows
+    );
+  }
+
+  Widget _buildThinkingIndicator(GameModel model) {
+    return ThinkingIndicator(
+      color: Styling.thinkingColor,
+      height: Styling.thinkingSize,
+      visible: model.player == PieceType.white,
+    );
+  }
+
+  Widget _buildRestartGameWidget() {
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        vertical: 5.0,
+        horizontal: 15.0,
+      ),
+      child: Image(
+        image: AssetImage('assets/icons/button_restart.png'),
+        fit: BoxFit.fill,
+      ),
+    );
+  }
+
+  Widget _buildEndGameWidget(GameModel model) {
+    Image decorationImage = Image(
+      image: AssetImage(
+        model.gameResultString == 'black'
+          ? 'assets/icons/game_win.png'
+          : 'assets/icons/empty.png'
+      ),
+      fit: BoxFit.fill,
+    );
+
+    return Container(
+      margin: EdgeInsets.all(2),
+      padding: EdgeInsets.all(2),
+
+      child: Table(
+        defaultColumnWidth: IntrinsicColumnWidth(),
+        children: [
+          TableRow(
+            children: [
+              Column(children: [ decorationImage ]),
+              Column(children: [
+                GestureDetector(
+                  onTap: () {
+                    _restartController.add(
+                      GameModel(board: GameBoard()),
+                    );
+                  },
+                  child: _buildRestartGameWidget(),
+                )
+              ]),
+              Column(children: [ decorationImage ]),
+            ],
+          ),
+        ]
+      )
+    );
+  }
+
+  // Builds out the Widget tree using the most recent GameModel from the stream.
+  Widget _buildWidgets(BuildContext context, GameModel model) {
+    return Container(
+      padding: EdgeInsets.only(top: 30.0, left: 15.0, right: 15.0),
+      decoration: Styling.mainWidgetDecoration,
+      child: SafeArea(
+        child: Column(
+          children: [
+            _buildScoreBoxes(model),
+            SizedBox(height: 10),
+            _buildGameBoardDisplay(context, model),
+            SizedBox(height: 10),
+            _buildThinkingIndicator(model),
+            SizedBox(height: 30),
+            if (model.gameIsOver) _buildEndGameWidget(model),
+          ],
+        ),
+      ),
     );
   }
 }
diff --git a/lib/move_finder.dart b/lib/move_finder.dart
new file mode 100644
index 0000000000000000000000000000000000000000..32a37f96bbdf7aeb4188fb01390f6786f1f504ad
--- /dev/null
+++ b/lib/move_finder.dart
@@ -0,0 +1,116 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+
+import 'game_board.dart';
+import 'game_board_scorer.dart';
+
+class MoveSearchArgs {
+  MoveSearchArgs(
+      {required this.board, required this.player, required this.numPlies});
+
+  final GameBoard board;
+  final PieceType player;
+  final int numPlies;
+}
+
+/// A move and its score. Used by the minimax algorithm.
+class ScoredMove {
+  final int score;
+  final Position move;
+
+  const ScoredMove(this.score, this.move);
+}
+
+// The [compute] function requires a top-level method as its first argument.
+// This is that method for [MoveFinder].
+Position? _findNextMove(MoveSearchArgs args) {
+  final bestMove = _performSearchPly(
+      args.board, args.player, args.player, args.numPlies - 1);
+  return bestMove?.move;
+}
+
+// This is a recursive implementation of minimax, an algorithm so old it has
+// its own Wikipedia page: https://wikipedia.org/wiki/Minimax.
+ScoredMove? _performSearchPly(
+  GameBoard board,
+  PieceType scoringPlayer,
+  PieceType player,
+  int pliesRemaining,
+) {
+  final availableMoves = board.getMovesForPlayer(player);
+
+  if (availableMoves.isEmpty) {
+    return null;
+  }
+
+  var score = (scoringPlayer == player)
+      ? GameBoardScorer.minScore
+      : GameBoardScorer.maxScore;
+  ScoredMove? bestMove;
+
+  for (var i = 0; i < availableMoves.length; i++) {
+    final newBoard =
+        board.updateForMove(availableMoves[i].x, availableMoves[i].y, player);
+    if (pliesRemaining > 0 &&
+        newBoard.getMovesForPlayer(getOpponent(player)).isNotEmpty) {
+      // Opponent has next turn.
+      score = _performSearchPly(
+            newBoard,
+            scoringPlayer,
+            getOpponent(player),
+            pliesRemaining - 1,
+          )?.score ??
+          0;
+    } else if (pliesRemaining > 0 &&
+        newBoard.getMovesForPlayer(player).isNotEmpty) {
+      // Opponent has no moves; player gets another turn.
+      score = _performSearchPly(
+            newBoard,
+            scoringPlayer,
+            player,
+            pliesRemaining - 1,
+          )?.score ??
+          0;
+    } else {
+      // Game is over or the search has reached maximum depth.
+      score = GameBoardScorer(newBoard).getScore(scoringPlayer);
+    }
+
+    if (bestMove == null ||
+        (score > bestMove.score && scoringPlayer == player) ||
+        (score < bestMove.score && scoringPlayer != player)) {
+      bestMove =
+          ScoredMove(score, Position(availableMoves[i].x, availableMoves[i].y));
+    }
+  }
+
+  return bestMove;
+}
+
+/// The [MoveFinder] class exists to provide its [findNextMove] method, which
+/// uses the minimax algorithm to look for the best available move on
+/// [initialBoard] a [GameBoardScorer] to provide the heuristic.
+class MoveFinder {
+  final GameBoard initialBoard;
+
+  MoveFinder(this.initialBoard);
+
+  /// Searches the tree of possible moves on [initialBoard] to a depth of
+  /// [numPlies], looking for the best possible move for [player]. Because the
+  /// actual work is done in an isolate, a [Future] is used as the return value.
+  Future<Position?> findNextMove(PieceType player, int numPlies) {
+    return compute(
+      _findNextMove,
+      MoveSearchArgs(
+        board: initialBoard,
+        player: player,
+        numPlies: numPlies,
+      ),
+    );
+  }
+}
diff --git a/lib/provider/data.dart b/lib/provider/data.dart
deleted file mode 100644
index ee24df3f0b90f712741afee85f4cc35ed563f70b..0000000000000000000000000000000000000000
--- a/lib/provider/data.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-import 'package:flutter/foundation.dart';
-
-class Data extends ChangeNotifier {
-
-  // Configuration available values
-  List _availableDifficultyLevels = ['easy', 'medium', 'hard'];
-
-  List get availableDifficultyLevels => _availableDifficultyLevels;
-
-  // Application default configuration
-  String _level = 'medium';
-  String _size = '8x8';
-  String _skin = 'default';
-
-  // Game data
-  bool _gameRunning = false;
-  int _sizeVertical = null;
-  int _sizeHorizontal = null;
-  List _cells = [];
-
-  String get level => _level;
-  void updateLevel(String level) {
-    _level = level;
-    notifyListeners();
-  }
-
-  String get size => _size;
-  int get sizeVertical => _sizeVertical;
-  int get sizeHorizontal => _sizeHorizontal;
-  void updateSize(String size) {
-    _size = size;
-    _sizeHorizontal = int.parse(_size.split('x')[0]);
-    _sizeVertical = int.parse(_size.split('x')[1]);
-    notifyListeners();
-  }
-
-  String get skin => _skin;
-  void updateSkin(String skin) {
-    _skin = skin;
-    notifyListeners();
-  }
-
-  getParameterValue(String parameterCode) {
-    switch(parameterCode) {
-      case 'difficulty': { return _level; }
-      break;
-    }
-  }
-
-  List getParameterAvailableValues(String parameterCode) {
-    switch(parameterCode) {
-      case 'difficulty': { return _availableDifficultyLevels; }
-      break;
-    }
-  }
-
-  setParameterValue(String parameterCode, String parameterValue) {
-    switch(parameterCode) {
-      case 'difficulty': { updateLevel(parameterValue); }
-      break;
-    }
-  }
-
-  List get cells => _cells;
-  void updateCells(List cells) {
-    _cells = cells;
-    notifyListeners();
-  }
-
-  bool get gameRunning => _gameRunning;
-  void updateGameRunning(bool gameRunning) {
-    _gameRunning = gameRunning;
-    notifyListeners();
-  }
-}
diff --git a/lib/screens/home.dart b/lib/screens/home.dart
deleted file mode 100644
index ba85d3740ca9ae317594ceb95e22d1d37f7034d0..0000000000000000000000000000000000000000
--- a/lib/screens/home.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import '../provider/data.dart';
-
-class Home extends StatelessWidget {
-  static const String id = 'home';
-
-  @override
-  Widget build(BuildContext context) {
-    Data myProvider = Provider.of<Data>(context);
-
-    return Scaffold(
-      appBar: AppBar(
-        title: new Text('Reversi'),
-
-        actions: [
-        ],
-        
-        leading: IconButton(
-          icon: Image.asset('assets/icons/application.png'),
-          onPressed: () { },
-        ),
-      ),
-      body: SafeArea(
-        child: Center(
-          child: Text('🎮')
-        ),
-      )
-    );
-  }
-}
diff --git a/lib/styling.dart b/lib/styling.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1ece8b8bc8111cbdc6b6ca15c32b94bf5cad9199
--- /dev/null
+++ b/lib/styling.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/widgets.dart';
+
+import 'game_board.dart';
+
+abstract class Styling {
+  // **** GRADIENTS AND COLORS ****
+
+  static const Map<PieceType, String> assetImageCodes = {
+    PieceType.black: 'black',
+    PieceType.white: 'white',
+    PieceType.empty: 'empty',
+  };
+
+  static const BorderSide boardCellBorderSide = BorderSide(
+    color: Color(0xff108010),
+    width: 1.0,
+  );
+
+  static const BoxDecoration boardCellDecoration = BoxDecoration(
+    color: Color(0xff50bb50),
+    border: Border(
+      bottom: boardCellBorderSide,
+      top: boardCellBorderSide,
+      left: boardCellBorderSide,
+      right: boardCellBorderSide,
+    ),
+  );
+
+  static const BoxDecoration mainWidgetDecoration = BoxDecoration(
+    color: Color(0xffffffff)
+  );
+
+  static const thinkingColor = Color(0xff2196f3);
+
+  // **** ANIMATIONS ****
+
+  static const Duration thinkingFadeDuration = Duration(milliseconds: 500);
+
+  static const pieceFlipDuration = Duration(milliseconds: 300);
+
+  // **** SIZES ****
+
+  static const thinkingSize = 10.0;
+
+  // **** BOXES ****
+
+  static const BorderSide activePlayerIndicatorBorder = BorderSide(
+    color: Color(0xff2196f3),
+    width: 10.0,
+  );
+
+  static const BorderSide inactivePlayerIndicatorBorder = BorderSide(
+    color: Color(0x00000000),
+    width: 10.0,
+  );
+
+  static const activePlayerIndicator = BoxDecoration(
+    borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
+    border: Border(
+      bottom: activePlayerIndicatorBorder,
+      top: activePlayerIndicatorBorder,
+      left: activePlayerIndicatorBorder,
+      right: activePlayerIndicatorBorder,
+    ),
+  );
+
+  static const inactivePlayerIndicator = BoxDecoration(
+    borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
+    border: Border(
+      bottom: inactivePlayerIndicatorBorder,
+      top: inactivePlayerIndicatorBorder,
+      left: inactivePlayerIndicatorBorder,
+      right: inactivePlayerIndicatorBorder,
+    ),
+  );
+}
diff --git a/lib/thinking_indicator.dart b/lib/thinking_indicator.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ad3478099d4b49a83c57a4bd09b69e75a0ad6c4b
--- /dev/null
+++ b/lib/thinking_indicator.dart
@@ -0,0 +1,143 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+import 'styling.dart';
+
+/// This is a self-animated progress spinner, only instead of spinning it
+/// moves five little circles in a horizontal arrangement.
+class ThinkingIndicator extends ImplicitlyAnimatedWidget {
+  final Color color;
+  final double height;
+  final bool visible;
+
+  ThinkingIndicator({
+    this.color = const Color(0xffffffff),
+    this.height = 10.0,
+    this.visible = true,
+    Key? key,
+  }) : super(
+          duration: Styling.thinkingFadeDuration,
+          key: key,
+        );
+
+  @override
+  ImplicitlyAnimatedWidgetState createState() => _ThinkingIndicatorState();
+}
+
+class _ThinkingIndicatorState
+    extends AnimatedWidgetBaseState<ThinkingIndicator> {
+  Tween<double>? _opacityTween;
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: SizedBox(
+        height: widget.height,
+        child: Opacity(
+          opacity: _opacityTween!.evaluate(animation!),
+          child: _opacityTween!.evaluate(animation!) != 0
+              ? _AnimatedCircles(
+                  color: widget.color,
+                  height: widget.height,
+                )
+              : null,
+        ),
+      ),
+    );
+  }
+
+  @override
+  void forEachTween(visitor) {
+    _opacityTween = visitor(
+      _opacityTween,
+      widget.visible ? 1.0 : 0.0,
+      (dynamic value) => Tween<double>(begin: value as double),
+    ) as Tween<double>?;
+  }
+}
+
+class _AnimatedCircles extends StatefulWidget {
+  final Color color;
+  final double height;
+
+  const _AnimatedCircles({
+    required this.color,
+    required this.height,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  _AnimatedCirclesState createState() => _AnimatedCirclesState();
+}
+
+class _AnimatedCirclesState extends State<_AnimatedCircles>
+    with SingleTickerProviderStateMixin {
+  late Animation<double> _thinkingAnimation;
+  late AnimationController _thinkingController;
+
+  @override
+  void initState() {
+    super.initState();
+    _thinkingController = AnimationController(
+        duration: const Duration(milliseconds: 500), vsync: this)
+      ..addStatusListener((status) {
+        // This bit ensures that the animation reverses course rather than
+        // stopping.
+        if (status == AnimationStatus.completed) _thinkingController.reverse();
+        if (status == AnimationStatus.dismissed) _thinkingController.forward();
+      });
+    _thinkingAnimation = Tween(begin: 0.0, end: widget.height).animate(
+        CurvedAnimation(parent: _thinkingController, curve: Curves.easeOut));
+    _thinkingController.forward();
+  }
+
+  @override
+  void dispose() {
+    _thinkingController.dispose();
+    super.dispose();
+  }
+
+  Widget _buildCircle() {
+    return Container(
+      width: widget.height,
+      height: widget.height,
+      decoration: BoxDecoration(
+        border: Border.all(
+          color: widget.color,
+          width: 2.0,
+        ),
+        borderRadius: BorderRadius.all(const Radius.circular(5.0)),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedBuilder(
+      animation: _thinkingAnimation,
+      builder: (context, child) {
+        return Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            _buildCircle(),
+            SizedBox(width: _thinkingAnimation.value),
+            _buildCircle(),
+            SizedBox(width: _thinkingAnimation.value),
+            _buildCircle(),
+            SizedBox(width: _thinkingAnimation.value),
+            _buildCircle(),
+            SizedBox(width: _thinkingAnimation.value),
+            _buildCircle(),
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/pubspec.lock b/pubspec.lock
index b6eca0d9a310f699c2be40fd6273acfa381332e9..9bed09620d00b8ade2770a54031e59727a291b7b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -7,7 +7,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.7.0"
+    version: "2.8.1"
   boolean_selector:
     dependency: transitive
     description:
@@ -50,6 +50,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.2.0"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.2"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.2"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -60,6 +74,18 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.3"
   matcher:
     dependency: transitive
     description:
@@ -88,6 +114,48 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.0"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.2"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  process:
+    dependency: transitive
+    description:
+      name: process
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.2.3"
   provider:
     dependency: "direct main"
     description:
@@ -95,6 +163,48 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "5.0.0"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.6"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
+  shared_preferences_macos:
+    dependency: transitive
+    description:
+      name: shared_preferences_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -141,7 +251,7 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.1"
+    version: "0.4.2"
   typed_data:
     dependency: transitive
     description:
@@ -156,6 +266,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.5"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
 sdks:
-  dart: ">=2.12.0 <3.0.0"
-  flutter: ">=1.16.0"
+  dart: ">=2.13.0 <3.0.0"
+  flutter: ">=2.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 1bc5838b8d43223d6c349189125beed09328d221..51faa3b57525d31f800da7addc981764fc44ad55 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,15 +1,16 @@
-name: reversi
+name: simpler_reversi
 description: A reversi game application.
 publish_to: 'none'
 version: 1.0.0+1
 
 environment:
-  sdk: ">=2.7.0 <3.0.0"
+  sdk: ">=2.12.0 <3.0.0"
 
 dependencies:
   flutter:
     sdk: flutter
   provider: ^5.0.0
+  shared_preferences: ^2.0.6
 
 dev_dependencies:
   flutter_test: