diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b1dff0dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Kotlin ### +.kotlin + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.run/desktop.run.xml b/.run/desktop.run.xml new file mode 100644 index 00000000..f91b2c22 --- /dev/null +++ b/.run/desktop.run.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/README.md b/README.md index 645d2500..62581b7a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,55 @@ -# Frobnicator (this is a template) +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/M0kyOMLZ) +# Aplikacja z mini-grami logicznymi -## Authors -- Andrzej Głuszak (@agluszak on GitHub) -- Linus Torvalds (@torvalds on GitHub) +## Autorzy +- Jan Kuźma -## Description -Frobnicator is going to be a platformer game similar to Super Mario Bros made using Korge game engine. +## Opis +Planuję stworzyć aplikacje desktopową zawierającą kilka mini gier logicznych, jeszcze się zastanawiam nad konkretnymi przykładami, ale będzie to coś jak sudoku/krzyżówki/mastermind itp, potencjalnie jakaś przygodowa gra tekstowa. -## Features -- map generator -- shooting -- enemy AI -- game state saving and loading -- scores +## Funkcjonalności +- gry logiczne singleplayer z kilkoma poziomami, +- ranking graczy/historia wyników, +- pomoc dla gracza (podpowiedzi/tutoriale), +- zapis stanu gry. ## Plan -In the first part we're going to implement the basics: movement, physics and shooting. The enemies will simply bounce from one edge of the platform to the other. There will be only a single map. +W pierwszej części planuję storzyć obsługę ekranów startowych, menu głównego, podstawową nawigacji, jakiś przykład łamigłówki. -In the second part we're going to add random map generator, saving/loading, scores and a more sophisticated AI. +W drugiej części planuję dodać nowe łamigłówki, rankingi/historie wyników, wskazówki dla graczy oraz potencjalnie (jeśli wyżej wymienione punkty okażą się zbyt mało czasochłonne) tryb multiplayer (być może rozgrywany na jednym telefonie, coś aka gra turowa- np kółko i krzyżyk). Dodatkowo, po konsultacji- zostanie dodany zapis stanu gry. -## Libraries -- Korge (https://korge.org/) -- kotlinx-serialization (https://github.com/Kotlin/kotlinx.serialization) +## Biblioteki +Będzie to apliakcja desktopowa (rezygnuję z android studio), z użyciem Compose. + +# Podsumowanie części pierwszej +Struktura projektu- w głównym folderze aplikacji znajdują się następujące komponenty: +- Main.kt, który pozwala na uruchomienie aplikacji, +- DatabaseConfig.kt, który zapewnia konfigurację połączenia z bazą danych +- populateDb.kt, który umożliwia załadowania początkowych planszy sudkou do bazy danych. Aby populować bazę danych, +należy odpowiednio zmienić kod w Main.kt (uruchomić jedynie z funkcją populate()) +- /backend/sudoku/Node.kt - deklaracja klasy pojedynczej komórki na planszy sudoku +- /backend/sudoku/SudokuBoard.kt - deklaracja klasy planszy sudoku, zawiera m.in serializacje oraz funckje sprawdzające poprawność planszy +- /backend/entities/SudokuBoards - zawiera deklaracje tabeli przechowującej sudoku +- /ui/screens/GameMenu.kt - główny widok statowego menu +- /ui/screens/games/MastermindScreen.kt - placeholder screen dla gry Mastermind +- /ui/screens/games/TickTakToe.kt - placeholder screen dla gry Kółko i Krzyżyk +- /ui/screens/games/SudokuScreen.kt - główny ekran dla sudoku, duża część implementacji +- /ui/sudokuComponents/NumberPad.kt - design dla klawiatury do wprowadzania numerków na plansze +- /ui/sudokuComponents/Popup.kt - design dla popup-ów, jeszcze nie używany +- /ui/sudokuComponents/SudokuBoardConfig.kt - konfiguracja kolorów dla planszy sudoku +- /ui/sudokuComponents/SudokuBoardUI.kt - design dla planszy sudoku + +Aplikacja zawiera podstawową nawigacje, oraz logikę gry sudoku, wraz z przechowywaniem stanu gry w bazie danych. +Stan gry można zapisywać, chwilowo istnieje jedna plansza. Jest zaimplementowana również walidacja stanu planszy, czy +sprawdzanie wygranej gracza. + +W projeckie użyłem takich bibliotek/funkcjonalności jal: +- Compose Desktop- było to wyzwanie, ponieważ nie omawialiśmy tego jeszcze na zajęciach, jednak praca z tą biblioteką okazała się bardzo przyjemna, +- Exposed – biblioteka służąca do obsługi połączeń z bazą danych w Kotlinie, +- Korutyny- użyte trochę na siłę, do komunikacji z bazą danych. Aby symulować dłuższą operację, ręcznie dodaję +sleep(1000) w funkcjach komunikujących się z bazą danych. + +# Jak uruchomić aplikację? +Można wywołać polecenie ./gradlew run. Ważna notatka- w funkcji main w pliku Main.kt jest wywoływana funckja populate(). +Czyści ona lokalnie bazę danych, oraz insertuje do niej 2 plansze sudoku. Jeżeli chcemy, aby stan planszy zapisywał się +między wywołaniami aplikacji- po pierwszym uruchomieniu należy usunąć (wykomentować) tą linijkę. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..7e261b97 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,47 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") + id("org.jetbrains.kotlin.plugin.compose") + kotlin("plugin.serialization") version "1.6.0" + id("org.jlleitschuh.gradle.ktlint") version "11.3.2" +} + +group = "com.app" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(compose.desktop.currentOs) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") + implementation("org.jetbrains.exposed:exposed-core:0.41.1") + implementation("org.jetbrains.exposed:exposed-dao:0.41.1") + implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1") + implementation("org.xerial:sqlite-jdbc:3.42.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("org.slf4j:slf4j-api:2.0.16.") + implementation("org.slf4j:slf4j-simple:2.0.16") +} + +compose.desktop { + application { + mainClass = "com.app.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "logicGames" + packageVersion = "1.0.0" + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..53094af2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.code.style=official +kotlin.version=2.0.0 +compose.version=1.6.11 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..48c0a02c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..717acefd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String) + } +} + +rootProject.name = "logicGames" diff --git a/src/main/kotlin/com/app/DatabaseConfig.kt b/src/main/kotlin/com/app/DatabaseConfig.kt new file mode 100644 index 00000000..ede8245b --- /dev/null +++ b/src/main/kotlin/com/app/DatabaseConfig.kt @@ -0,0 +1,22 @@ +package database + +import com.app.backend.database.entities.SudokuBoards +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction + +object DatabaseConfig { + suspend fun init() = withContext(Dispatchers.IO) { + // Connecting to SQLite + Database.connect( + url = "jdbc:sqlite:backend.db", + driver = "org.sqlite.JDBC" + ) + + transaction { + SchemaUtils.create(SudokuBoards) + } + } +} diff --git a/src/main/kotlin/com/app/Main.kt b/src/main/kotlin/com/app/Main.kt new file mode 100644 index 00000000..69309cd3 --- /dev/null +++ b/src/main/kotlin/com/app/Main.kt @@ -0,0 +1,24 @@ +package com.app + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.app.ui.screens.GameMenu +import kotlinx.coroutines.runBlocking + +@Composable +@Preview +fun App() { + GameMenu() +} + +fun main() = application { + populate() + runBlocking { + database.DatabaseConfig.init() + } + Window(onCloseRequest = ::exitApplication, title = "PuzzleVerse") { + App() + } +} diff --git a/src/main/kotlin/com/app/PopulateDb.kt b/src/main/kotlin/com/app/PopulateDb.kt new file mode 100644 index 00000000..e67f3464 --- /dev/null +++ b/src/main/kotlin/com/app/PopulateDb.kt @@ -0,0 +1,41 @@ +package com.app + +import com.app.backend.database.entities.SudokuBoards +import com.app.backend.database.services.SudokuService.insertSudoku +import com.app.backend.sudoku.Node +import com.app.backend.sudoku.SudokuBoard +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.deleteAll +import org.jetbrains.exposed.sql.transactions.transaction + +fun populate() = runBlocking { + database.DatabaseConfig.init() + + // Example sudoku, in the future could be replaced with own logic of generating sudoku + val boardNodesValues = mutableListOf>( + mutableListOf(5, 0, 0, 0, 7, 0, 0, 0, 4), + mutableListOf(6, 0, 9, 0, 3, 1, 0, 5, 0), + mutableListOf(8, 0, 0, 0, 0, 9, 0, 1, 0), + mutableListOf(4, 9, 0, 1, 0, 0, 8, 0, 2), + mutableListOf(2, 0, 8, 0, 0, 6, 7, 0, 5), + mutableListOf(0, 0, 0, 2, 8, 4, 1, 0, 0), + mutableListOf(0, 7, 4, 0, 0, 8, 5, 6, 0), + mutableListOf(1, 0, 0, 7, 6, 3, 4, 2, 0), + mutableListOf(9, 6, 0, 0, 0, 0, 0, 0, 0) + ) + + val board = SudokuBoard() + for (i in 0..8) { + for (j in 0..8) { + board.content[i][j] = Node(i, j, boardNodesValues[i][j], generated = boardNodesValues[i][j] != 0) + } + } + + transaction { + SudokuBoards.deleteAll() + } + + // id 1: current sudoku, id 2: base, non-modifiable sudoku + insertSudoku(board.serialize(), 1) + insertSudoku(board.serialize(), 2) +} diff --git a/src/main/kotlin/com/app/backend/database/entities/SudokuBoards.kt b/src/main/kotlin/com/app/backend/database/entities/SudokuBoards.kt new file mode 100644 index 00000000..c8bedfec --- /dev/null +++ b/src/main/kotlin/com/app/backend/database/entities/SudokuBoards.kt @@ -0,0 +1,11 @@ +package com.app.backend.database.entities + +import org.jetbrains.exposed.sql.Table + +// The board is stored as a string, serializing and deserializing is provided by SudokuBoard class +// id 1 is the game which is currently modified, id 2 is the base game, which is stored to provide 'new game' functionality. +object SudokuBoards : Table() { + val id = integer("id") + val board = text("board") + override val primaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/com/app/backend/database/services/SudokuService.kt b/src/main/kotlin/com/app/backend/database/services/SudokuService.kt new file mode 100644 index 00000000..349e9590 --- /dev/null +++ b/src/main/kotlin/com/app/backend/database/services/SudokuService.kt @@ -0,0 +1,45 @@ +package com.app.backend.database.services + +import com.app.backend.database.entities.SudokuBoards +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update + +// Basic functions that provide comunication with databse +object SudokuService { + suspend fun insertSudoku(board: String, id: Int) = withContext(Dispatchers.IO) { + transaction { + SudokuBoards.insert { + it[SudokuBoards.board] = board + it[SudokuBoards.id] = id + } + } + } + + suspend fun getSudokuById(id: Int): String? = withContext(Dispatchers.IO) { + transaction { + SudokuBoards.select { SudokuBoards.id eq id } + .map { it[SudokuBoards.board] } + .singleOrNull() + } + } + + suspend fun getAllSudokus(): List = withContext(Dispatchers.IO) { + transaction { + SudokuBoards.selectAll() + .map { it[SudokuBoards.board] } + } + } + + suspend fun updateSudoku(board: String, id: Int) = withContext(Dispatchers.IO) { + transaction { + SudokuBoards.update({ SudokuBoards.id eq id }) { + it[SudokuBoards.board] = board + } + } + } +} diff --git a/src/main/kotlin/com/app/backend/sudoku/Node.kt b/src/main/kotlin/com/app/backend/sudoku/Node.kt new file mode 100644 index 00000000..666defa4 --- /dev/null +++ b/src/main/kotlin/com/app/backend/sudoku/Node.kt @@ -0,0 +1,20 @@ +package com.app.backend.sudoku +import kotlinx.serialization.Serializable + +// Representation of sudoku node. x,y: coordinates on the board, number: value of the node, if equal to 0- no number +// generated: if a node was generated (during game creation) it cannot be modified. +// isValid: if a player inputs a number, it can invalidate the board/cell. If a number collides with another number due +// to sudoku rules, it is not valid, and displayed with red background. + +@Serializable +data class Node( + val x: Int, + val y: Int, + var number: Int = 0, + var generated: Boolean = false, + var isValid: Boolean = true +) { + override fun toString(): String { + return number.toString() + } +} diff --git a/src/main/kotlin/com/app/backend/sudoku/SudokuBoard.kt b/src/main/kotlin/com/app/backend/sudoku/SudokuBoard.kt new file mode 100644 index 00000000..ee579d31 --- /dev/null +++ b/src/main/kotlin/com/app/backend/sudoku/SudokuBoard.kt @@ -0,0 +1,74 @@ +package com.app.backend.sudoku + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +data class SudokuBoard( + val content: MutableList> = MutableList(9) { x -> + MutableList(9) { y -> + Node(x, y) + } + } +) { + + // Checking if a number is colliding with any number due to sudoku rules. + private fun isValidNumber(x: Int, y: Int, number: Int): Boolean { + if (content[x].any { it.number == number && it.y != y }) { + return false + } + + if (content.any { it[y].number == number && it != content[x] }) { + return false + } + + val startRow = (x / 3) * 3 + val startCol = (y / 3) * 3 + for (row in startRow until startRow + 3) { + for (col in startCol until startCol + 3) { + if (content[row][col].number == number && (row != x || col != y)) { + return false + } + } + } + return true + } + + // Checking if the whole board is valid- if the user won. + fun isBoardValid(): Boolean { + for (row in content) { + for (node in row) { + if (!isValidNumber(node.x, node.y, node.number) || node.number == 0) { + return false + } + } + } + return true + } + + // Updating the whole board, checking if nodes collide with something or not. + fun validate() { + for (row in content) { + for (node in row) { + node.isValid = if (node.number != 0) isValidNumber(node.x, node.y, node.number) else true + } + } + } + + // Serializing and deserializing between JSON and string. + fun serialize(): String { + return Json.encodeToString(content) + } + + companion object { + fun deserialize(json: String): SudokuBoard { + val nodes = Json.decodeFromString>>(json) + val board = SudokuBoard() + for (row in nodes.indices) { + for (col in nodes[row].indices) { + board.content[row][col] = nodes[row][col] + } + } + return board + } + } +} diff --git a/src/main/kotlin/com/app/ui/screens/GameMenu.kt b/src/main/kotlin/com/app/ui/screens/GameMenu.kt new file mode 100644 index 00000000..0b60c794 --- /dev/null +++ b/src/main/kotlin/com/app/ui/screens/GameMenu.kt @@ -0,0 +1,141 @@ +package com.app.ui.screens + +import SudokuScreen +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.app.ui.screens.games.MastermindScreen +import com.app.ui.screens.games.TicTacToeScreen +enum class Game { + MENU, SUDOKU, MASTERMIND, TIC_TAC_TOE +} + +@Composable +fun GameMenu() { + var currentScreen by remember { mutableStateOf(Game.MENU) } + + when (currentScreen) { + Game.MENU -> MenuScreen { selectedGame -> + currentScreen = selectedGame + } + Game.SUDOKU -> SudokuScreen { currentScreen = Game.MENU } + Game.MASTERMIND -> MastermindScreen { currentScreen = Game.MENU } + Game.TIC_TAC_TOE -> TicTacToeScreen { currentScreen = Game.MENU } + } +} + +@Composable +fun MenuScreen(onGameSelect: (Game) -> Unit) { + val games = listOf("Sudoku", "Mastermind", "Tic Tac Toe") + var currentIndex by remember { mutableStateOf(0) } + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Main Menu", + fontSize = 50.sp, + color = Color.Black, + fontFamily = FontFamily.Serif + ) + + Spacer(modifier = Modifier.height(100.dp)) + Box( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .height(300.dp) + .background(Color(0xFFD0B8A8), RoundedCornerShape(16.dp)) + .padding(16.dp) + ) { + Column( + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = games[currentIndex], + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFFC5705D), + modifier = Modifier.padding(bottom = 16.dp) + ) + + // Navigation between games + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button( + onClick = { currentIndex = if (currentIndex > 0) currentIndex - 1 else games.size - 1 }, + modifier = Modifier.padding(8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Previous") + } + + Button( + onClick = { currentIndex = (currentIndex + 1) % games.size }, + modifier = Modifier.padding(8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Next") + } + } + // Selecting game + Button( + onClick = { + when (currentIndex) { + 0 -> onGameSelect(Game.SUDOKU) + 1 -> onGameSelect(Game.MASTERMIND) + 2 -> onGameSelect(Game.TIC_TAC_TOE) + } + }, + modifier = Modifier + .padding(top = 16.dp) + .width(150.dp) + .height(75.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Start", color = Color.White, fontSize = 20.sp) + } + } + } + } +} diff --git a/src/main/kotlin/com/app/ui/screens/games/MastermindScreen.kt b/src/main/kotlin/com/app/ui/screens/games/MastermindScreen.kt new file mode 100644 index 00000000..5860c6ee --- /dev/null +++ b/src/main/kotlin/com/app/ui/screens/games/MastermindScreen.kt @@ -0,0 +1,30 @@ +package com.app.ui.screens.games + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +// Mastermind Screen, currently not developed +@Composable +fun MastermindScreen(onBack: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Mastermind - Placeholder") + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onBack) { + Text("Back") + } + } +} diff --git a/src/main/kotlin/com/app/ui/screens/games/SudokuScreen.kt b/src/main/kotlin/com/app/ui/screens/games/SudokuScreen.kt new file mode 100644 index 00000000..a4944cb4 --- /dev/null +++ b/src/main/kotlin/com/app/ui/screens/games/SudokuScreen.kt @@ -0,0 +1,200 @@ +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.app.backend.database.services.SudokuService.getSudokuById +import com.app.backend.database.services.SudokuService.updateSudoku +import com.app.backend.sudoku.SudokuBoard +import com.app.ui.sudokuComponents.SudokuBoardUI +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.Thread.sleep + +@Composable +fun SudokuScreen(onBack: () -> Unit) { + // State of the board, currently selected cell and number + var selectedCell by remember { mutableStateOf?>(null) } + var selectedNumber by remember { mutableStateOf(null) } + var board by remember { mutableStateOf(null) } + // var showPopup by remember { mutableStateOf(false) } TODO in the future + // Is the board completed + var completed by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + // Loading the initial board - for now errors are displayed in the console, should be developed further + LaunchedEffect(Unit) { + try { + board = getInitialSudoku() + } catch (e: Exception) { + println("Error during initialization: ${e.message}") + } + } + + Column( + modifier = Modifier + .padding(8.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Sudoku", fontSize = 28.sp, modifier = Modifier.padding(8.dp)) + + Row( + modifier = Modifier + .padding(4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + SudokuBoardUI( + sudokuBoard = board, + selectedNode = selectedCell, + completed = completed, + onNodeClick = { x, y -> + selectedCell = Pair(x, y) + } + ) + + Spacer(modifier = Modifier.width(16.dp)) + + NumberPad( + onNumberClick = { number -> + selectedNumber = number + selectedCell?.let { (x, y) -> + if (selectedNumber != null) { + scope.launch { + val updatedBoard = updateCell(board, x, y, selectedNumber!!) + board = updatedBoard.first + completed = updatedBoard.second + } + } + } + } + ) + } + + Row( + modifier = Modifier + .padding(4.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button( + onClick = onBack, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Main Menu") + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Saving current state of the board to the database + Button( + onClick = { + scope.launch { + val temp = board + board = null + board = saveSudoku(temp) + } + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Save progress") + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Displaying the basic board to the user. + Button( + onClick = { + scope.launch { + try { + board = null + board = newGame() + } catch (e: Exception) { + println("Error during creating new game: ${e.message}") + } + } + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("New game") + } + } + } +} + +// Updating board state +fun updateCell(board: SudokuBoard?, x: Int, y: Int, number: Int): Pair { + if (!board!!.content[x][y].generated) { + val newBoard = board.content.toMutableList().apply { + this[x] = this[x].toMutableList().apply { + this[y] = this[y].copy(number = number) + } + } + val result = SudokuBoard(newBoard) + result.validate() + return Pair(result, result.isBoardValid()) + } + return Pair(board, false) +} + +// Coroutines use is unnecessary - just to provide an example of communication with the database. +// In my project it is really fast, but potentially- interactions with database shouldn't block the main thread. +// I manually add sleep() to simulate a longer process. +suspend fun getInitialSudoku(): SudokuBoard { + return withContext(Dispatchers.IO) { + sleep(1000) // To show that coroutines work :) + val result = SudokuBoard.deserialize(getSudokuById(1)!!) + result.validate() + result + } +} + +// Error handling should be furtehr developped +suspend fun saveSudoku(board: SudokuBoard?): SudokuBoard? { + if (board != null) { + try { + withContext(Dispatchers.IO) { + sleep(1000) + updateSudoku(board.serialize(), 1) + } + } catch (e: Exception) { + println("Error saving sudoku: ${e.message}") + } + } + return board +} + +suspend fun newGame(): SudokuBoard { + return withContext(Dispatchers.IO) { + sleep(1000) // To show that coroutines work :) + val sudoku = getSudokuById(2)!! + updateSudoku(sudoku, 1) + SudokuBoard.deserialize(sudoku) + } +} diff --git a/src/main/kotlin/com/app/ui/screens/games/TicTacToeScreen.kt b/src/main/kotlin/com/app/ui/screens/games/TicTacToeScreen.kt new file mode 100644 index 00000000..284f00a3 --- /dev/null +++ b/src/main/kotlin/com/app/ui/screens/games/TicTacToeScreen.kt @@ -0,0 +1,30 @@ +package com.app.ui.screens.games + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +// Tick Tack Toe Screen, currently not developed +@Composable +fun TicTacToeScreen(onBack: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Tic Tac Toe - Placeholder") + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onBack) { + Text("Back") + } + } +} diff --git a/src/main/kotlin/com/app/ui/sudokuComponents/NumberPad.kt b/src/main/kotlin/com/app/ui/sudokuComponents/NumberPad.kt new file mode 100644 index 00000000..b387d9b4 --- /dev/null +++ b/src/main/kotlin/com/app/ui/sudokuComponents/NumberPad.kt @@ -0,0 +1,46 @@ +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +// Number pad that enables user inserting numbers into the board +@Composable +fun NumberPad(onNumberClick: (Int) -> Unit) { + Column( + modifier = Modifier + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + (1..9).chunked(3).forEach { rowNumbers -> + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowNumbers.forEach { number -> + Button( + onClick = { onNumberClick(number) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text(number.toString()) + } + } + } + } + // Additional feature: clearing the cell + Button( + onClick = { onNumberClick(0) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFFC5705D), + contentColor = Color.White + ) + ) { + Text("Clear Cell") + } + } +} diff --git a/src/main/kotlin/com/app/ui/sudokuComponents/Popup.kt b/src/main/kotlin/com/app/ui/sudokuComponents/Popup.kt new file mode 100644 index 00000000..4cecedbe --- /dev/null +++ b/src/main/kotlin/com/app/ui/sudokuComponents/Popup.kt @@ -0,0 +1,53 @@ +package com.app.ui.sudokuComponents + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +// Popup that should inform user, if the sudoku was saved successfully. +// Not implemented yet: error handling should be developed int the second part of the project. +@Composable +fun ResultPopup(showPopup: Boolean, success: Boolean, message: String?, onDismiss: () -> Unit) { + if (showPopup) { + Dialog(onDismissRequest = onDismiss) { + Box( + modifier = Modifier + .size(300.dp) + .background(color = Color.White, shape = RoundedCornerShape(16.dp)) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = if (success) "Success" else "Error", + style = MaterialTheme.typography.h6, + color = if (success) Color.Green else Color.Red, + modifier = Modifier.padding(bottom = 16.dp) + ) + Text( + text = message ?: if (success) "Sudoku saved successfully!" else "An error occurred.", + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 16.dp) + ) + Button(onClick = onDismiss) { + Text("OK") + } + } + } + } + } +} diff --git a/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardConfig.kt b/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardConfig.kt new file mode 100644 index 00000000..80d6181a --- /dev/null +++ b/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardConfig.kt @@ -0,0 +1,13 @@ +package com.app.ui.sudokuComponents + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +// Easy UI parameters modifications: mainly about design of the board +val cellWidth = 40.dp +val thinDividerWidth = 1.dp +val dividerWidth = 2.dp +val dividerColor = Color.Black +val selectedCellColor = Color(0xFFC5705D) +val adjacentColor = Color(0xFFD0B8A8) +val fullLen = dividerWidth * 4 + thinDividerWidth * 6 + cellWidth * 9 diff --git a/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardUI.kt b/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardUI.kt new file mode 100644 index 00000000..31d56dfe --- /dev/null +++ b/src/main/kotlin/com/app/ui/sudokuComponents/SudokuBoardUI.kt @@ -0,0 +1,150 @@ +package com.app.ui.sudokuComponents + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.TabRowDefaults.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.app.backend.sudoku.SudokuBoard + +@Composable +fun SudokuBoardUI( + sudokuBoard: SudokuBoard?, + selectedNode: Pair?, + onNodeClick: (Int, Int) -> Unit, + completed: Boolean +) { + Column( + modifier = Modifier.padding(8.dp) + ) { + // Checking if the board is completed + if (completed) { + Box( + modifier = Modifier + .width(fullLen) + .height(fullLen), + contentAlignment = Alignment.Center + ) { + Text("CONGRATULATIONS", fontSize = 40.sp) + } + } else { + when (sudokuBoard) { + null -> { + Box( // Simulation of long communication, loading screen + modifier = Modifier + .width(fullLen) + .height(fullLen), + contentAlignment = Alignment.Center + ) { + Text("Loading...", fontSize = 20.sp, modifier = Modifier.padding(8.dp)) + } + } + else -> { // Displaying the whole board + for (row in 0 until 9) { + Row() { + Divider( + color = dividerColor, + modifier = Modifier + .height(if (row % 3 == 0) dividerWidth else thinDividerWidth) + .width(fullLen) + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(0.dp) + ) { + Divider( + color = dividerColor, + modifier = Modifier + .height(cellWidth) + .width(dividerWidth) + ) + for (col in 0 until 9) { + val nodeValue = sudokuBoard.content[row][col] + val isSelected = selectedNode == Pair(row, col) + var isAdjacentToSelected = false + var isAdjacentSquare = false + if (selectedNode != null) { + isAdjacentToSelected = (selectedNode.first == row) || (selectedNode.second == col) + isAdjacentSquare = + (row / 3) * 3 + (col / 3) == (selectedNode.first / 3) * 3 + (selectedNode.second / 3) + } + + SudokuNode( + value = nodeValue.number, + isSelected = isSelected, + onClick = { onNodeClick(row, col) }, + isAdjacentToSelected = isAdjacentToSelected, + isAdjacentSquare = isAdjacentSquare, + isValid = nodeValue.isValid + ) + Divider( + color = dividerColor, + modifier = Modifier + .height(cellWidth) + .width(if (col % 3 == 2) dividerWidth else thinDividerWidth) + ) + } + } + } + Row() { + Divider( + color = Color.Black, + thickness = dividerWidth, + modifier = Modifier + .height(dividerWidth) + .width(fullLen) + ) + } + } + } + } + } +} + +// Single Sudoku node +@Composable +fun SudokuNode( + value: Int, + isSelected: Boolean, + isAdjacentToSelected: Boolean, + isAdjacentSquare: Boolean, + onClick: () -> Unit, + isValid: Boolean +) { + Box( + modifier = Modifier + .size(40.dp) + .clickable(onClick = onClick) + .background( + color = if (!isValid) { + Color.Red + } else if (isSelected) { + selectedCellColor + } else if (isAdjacentToSelected || isAdjacentSquare) { + adjacentColor + } else { + Color.Transparent + } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = if (value == 0) "" else value.toString(), + fontSize = 18.sp, + color = Color.Black + ) + } +}