From ada58149f03d5b48f88a87357f2b1e1ba4f8efec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20R=C3=BCmpelein?= Date: Tue, 17 Aug 2021 20:36:49 +0200 Subject: [PATCH] Initial commit (works somehow, at least) --- .gitignore | 46 +++++ build.gradle.kts | 48 +++++ gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++++++++++++++++++ gradlew.bat | 89 +++++++++ settings.gradle.kts | 9 + src/main/kotlin/Main.kt | 150 ++++++++++++++ src/main/kotlin/components/EndCard.kt | 49 +++++ src/main/kotlin/components/GameHeader.kt | 30 +++ src/main/kotlin/components/PlayerBar.kt | 26 +++ src/main/kotlin/components/PlayerCard.kt | 46 +++++ src/main/kotlin/components/Question.kt | 84 ++++++++ src/main/kotlin/components/QuestionGrid.kt | 32 +++ src/main/kotlin/components/TopicRow.kt | 62 ++++++ .../components/common/DisabledButton.kt | 22 +++ .../questiondialog/QuestionDialog.kt | 42 ++++ .../questiondialog/QuestionDialogPlayer.kt | 74 +++++++ .../buttons/QuestionDialogButtons.kt | 83 ++++++++ .../buttons/QuestionDialogDeferredButtons.kt | 83 ++++++++ src/main/kotlin/data/ColorData.kt | 12 ++ src/main/kotlin/data/Game.kt | 10 + src/main/kotlin/data/Player.kt | 6 + src/main/kotlin/data/QuestionData.kt | 12 ++ src/main/kotlin/data/Topic.kt | 7 + 25 files changed, 1213 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/Main.kt create mode 100644 src/main/kotlin/components/EndCard.kt create mode 100644 src/main/kotlin/components/GameHeader.kt create mode 100644 src/main/kotlin/components/PlayerBar.kt create mode 100644 src/main/kotlin/components/PlayerCard.kt create mode 100644 src/main/kotlin/components/Question.kt create mode 100644 src/main/kotlin/components/QuestionGrid.kt create mode 100644 src/main/kotlin/components/TopicRow.kt create mode 100644 src/main/kotlin/components/common/DisabledButton.kt create mode 100644 src/main/kotlin/components/questiondialog/QuestionDialog.kt create mode 100644 src/main/kotlin/components/questiondialog/QuestionDialogPlayer.kt create mode 100644 src/main/kotlin/components/questiondialog/buttons/QuestionDialogButtons.kt create mode 100644 src/main/kotlin/components/questiondialog/buttons/QuestionDialogDeferredButtons.kt create mode 100644 src/main/kotlin/data/ColorData.kt create mode 100644 src/main/kotlin/data/Game.kt create mode 100644 src/main/kotlin/data/Player.kt create mode 100644 src/main/kotlin/data/QuestionData.kt create mode 100644 src/main/kotlin/data/Topic.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0ae70e --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +.gradle +**/build/ +!src/**/build/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache +*.class +*.log +*.ctxt +.mtj.tmp/ +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..a6aa4ca --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.compose.compose +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" +} + +group = "de.pheerai" +version = "0.0.1-SNAPSHOT" + +repositories { + jcenter() + mavenCentral() + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +dependencies { + implementation(compose.desktop.currentOs) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +tasks.withType() { + kotlinOptions.jvmTarget = "11" + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Deb, TargetFormat.Rpm) + packageName = "jeopardy" + val fullVersion = (project.version as String) + packageVersion = if (fullVersion.contains("-SNAPSHOT")) { + fullVersion.removeSuffix("-SNAPSHOT") + } else { + fullVersion + } + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..af7be50 --- /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-7.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /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 0000000..cc7ed63 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + +} +rootProject.name = "jeopardy" + diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..9f86969 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,150 @@ +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import components.EndCard +import components.GameHeader +import components.PlayerBar +import components.QuestionGrid +import data.* + +val game = Game( + title = "j-EP-ardy!", + doubleAfter = 5, + endGameAfter = 2, + players = listOf( + Player(name = "Oli"), + Player(name = "Gesina") + ), + topics = listOf( + Topic( + color = ColorData( + red = 0x44, + green = 0x44, + blue = 0xAA + ), + topic = "Wer MACKt's?", + questions = listOf( + QuestionData( + hint = "Mack", + answer = "Thomas", + points = 100u, + isDeferredDouble = true + ), + QuestionData( + hint = "Mack", + answer = "Michael", + points = 200u + ), + QuestionData( + hint = "Mack", + answer = "Ann-Kathrin", + points = 300u + ) + ) + ), + Topic( + color = ColorData( + red = 0x22, + green = 0x22, + blue = 0xCC + ), + topic = "Topic2", + questions = listOf( + QuestionData( + hint = "Wildcard", + answer = "any", + points = 1000u + ), + QuestionData( + hint = "foo", + answer = "bar", + points = 5u + ), + QuestionData( + hint = "foo", + answer = "bar", + points = 5u + ) + ) + ), + Topic( + topic = "Topic3", + questions = listOf( + QuestionData( + hint = "Wildcard", + answer = "any", + points = 1000u + ), + QuestionData( + hint = "foo", + answer = "bar", + points = 5u + ), + QuestionData( + hint = "foo", + answer = "bar", + points = 5u + ) + ) + ) + ) +) + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "J-EP-ardy" + ) { + MaterialTheme { + val playerPointMap: MutableMap = game.players.map { it to 0L }.toMutableStateMap() + + var questionsPlayed by remember { mutableStateOf(0) } + val doublePoints by remember { derivedStateOf { questionsPlayed >= game.doubleAfter } } + val gameEnded by remember { derivedStateOf { questionsPlayed >= game.endGameAfter } } + + if (!gameEnded) { + Column(modifier = Modifier.fillMaxHeight().fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxHeight(0.2f) + ) { + GameHeader( + title = game.title, + color = game.headerColor + ) + } + Spacer(modifier = Modifier.height(5.dp)) + Row( + modifier = Modifier.fillMaxHeight() + ) { + Column(Modifier.fillMaxWidth(0.8f)) { + QuestionGrid( + game, + doublePoints, + onResolveQuestion = { + questionsPlayed++ + println("Questions: $questionsPlayed, Double: $doublePoints") + }, + onPointsChange = { player, points -> + playerPointMap[player] = playerPointMap[player]?.plus(points) ?: 0 + } + ) + } + Spacer(modifier = Modifier.width(5.dp)) + Column(Modifier.fillMaxWidth()) { + PlayerBar(players = playerPointMap) + } + } + } + } else { + EndCard( + playerPointMap = playerPointMap, + close = ::exitApplication + ) + } + } + } +} diff --git a/src/main/kotlin/components/EndCard.kt b/src/main/kotlin/components/EndCard.kt new file mode 100644 index 0000000..6413806 --- /dev/null +++ b/src/main/kotlin/components/EndCard.kt @@ -0,0 +1,49 @@ +package components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import data.Player +import data.toColor + +@Suppress("FunctionName") +@Composable +fun EndCard( + playerPointMap: MutableMap, + close: () -> Unit +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + .fillMaxHeight() + ) { + val firstThreePlaces = playerPointMap.entries + .sortedBy { it.value } + .map { it.key } + .take(3) + val firstPlayer = firstThreePlaces.first() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "Congratulations, ${firstPlayer.name}!") + for ((index, player) in firstThreePlaces.withIndex()) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = "Place ${index + 1}") + Surface(color = player.color.toColor()) { + Text(text = player.name) + } + } + } + Button( + onClick = close + ) { + Text(text = "Close") + } + } + } +} diff --git a/src/main/kotlin/components/GameHeader.kt b/src/main/kotlin/components/GameHeader.kt new file mode 100644 index 0000000..8dca44e --- /dev/null +++ b/src/main/kotlin/components/GameHeader.kt @@ -0,0 +1,30 @@ +package components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +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.em +import data.ColorData +import data.toColor + +@Suppress("FunctionName") +@Composable +fun GameHeader(title: String, color: ColorData?) { + Surface( + color = color.toColor(), + modifier = Modifier.fillMaxSize(1f) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = title, color = Color.LightGray, fontSize = 8.em) + } + } +} diff --git a/src/main/kotlin/components/PlayerBar.kt b/src/main/kotlin/components/PlayerBar.kt new file mode 100644 index 0000000..4a2dfd7 --- /dev/null +++ b/src/main/kotlin/components/PlayerBar.kt @@ -0,0 +1,26 @@ +package components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import data.Player + +@Suppress("FunctionName") +@Composable +fun PlayerBar(players: MutableMap) { + val numberOfCards = players.size + Column( + modifier = Modifier.fillMaxHeight() + .fillMaxWidth() + ) { + for ( + (index, playerEntry) in players.entries.sortedBy { it.key.name }.sortedByDescending { it.value } + .withIndex() + ) { + val maxHeightFraction = 1f / (numberOfCards - index) + PlayerCard(playerEntry.key, maxHeightFraction, playerEntry.value) + } + } +} diff --git a/src/main/kotlin/components/PlayerCard.kt b/src/main/kotlin/components/PlayerCard.kt new file mode 100644 index 0000000..0b8c196 --- /dev/null +++ b/src/main/kotlin/components/PlayerCard.kt @@ -0,0 +1,46 @@ +package components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +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.em +import data.Player +import data.toColor + +@Suppress("FunctionName") +@Composable +fun PlayerCard( + player: Player, + maxHeightFraction: Float, + points: Long +) { + Surface( + color = player.color.toColor(), + modifier = Modifier.fillMaxHeight(maxHeightFraction) + .fillMaxWidth() + .padding(5.dp), + shape = MaterialTheme.shapes.small + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround + ) { + Text( + text = player.name, + fontSize = 2.5.em, + color = Color.LightGray + ) + Text( + text = points.toString(), + fontSize = 5.em, + color = Color.White + ) + } + } +} diff --git a/src/main/kotlin/components/Question.kt b/src/main/kotlin/components/Question.kt new file mode 100644 index 0000000..02510ae --- /dev/null +++ b/src/main/kotlin/components/Question.kt @@ -0,0 +1,84 @@ +package components + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.unit.dp +import components.questiondialog.QuestionDialogue +import data.Player +import data.QuestionData +import data.Topic + +private fun Modifier.setButtonSize(heightFraction: Float) = this.fillMaxWidth(1f) + .fillMaxHeight(heightFraction) + .padding(5.dp) + +@Composable +@Suppress("FunctionName") +fun Question( + questionData: QuestionData, + doublePoints: Boolean, + players: List, + topic: Topic, + heightFraction: Float, + color: Color, + onPointsChange: (Player, Long) -> Unit, + onResolve: () -> Unit +) { + var questionVisible by remember { mutableStateOf(false) } + var questionResolved by remember { mutableStateOf(false) } + var pointsButtonActive by remember { mutableStateOf(false) } + + if (questionResolved) { + Surface( + shape = MaterialTheme.shapes.small, + modifier = Modifier + .setButtonSize(heightFraction), + color = Color.LightGray + ) { } + } else { + Button( + shape = MaterialTheme.shapes.small, + colors = ButtonDefaults.buttonColors( + backgroundColor = color + ), + onClick = { questionVisible = true }, + modifier = Modifier + .setButtonSize(heightFraction) + .pointerMoveFilter( + onEnter = { + pointsButtonActive = true + false + }, + onExit = { + pointsButtonActive = false + false + } + ) + ) { + Text( + text = "${questionData.actualDisplayPoints(doublePoints)}", + color = if (pointsButtonActive) Color.Gray else Color.LightGray + ) + } + } + if (questionVisible) { + QuestionDialogue( + topic = topic, + questionData = questionData, + players = players, + doublePoints = doublePoints, + onResolve = { + questionResolved = true + questionVisible = false + onResolve() + }, + onPointsChange = onPointsChange + ) + } +} diff --git a/src/main/kotlin/components/QuestionGrid.kt b/src/main/kotlin/components/QuestionGrid.kt new file mode 100644 index 0000000..b4b50bf --- /dev/null +++ b/src/main/kotlin/components/QuestionGrid.kt @@ -0,0 +1,32 @@ +package components + +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.* +import data.Game +import data.Player + +@Suppress("FunctionName") +@Composable +fun QuestionGrid( + game: Game, + doublePoints: Boolean, + onPointsChange: (player: Player, points: Long) -> Unit, + onResolveQuestion: () -> Unit +) { + val users = game.players + val numberOfColumns = game.topics.size + + Row { + for ((index, topic) in game.topics.withIndex()) { + val maxColumnFraction = 1f / (numberOfColumns - index) + TopicRow( + topic = topic, + doublePoints = doublePoints, + users = users, + columnFraction = maxColumnFraction, + onPointsChange = onPointsChange, + onResolveQuestion = onResolveQuestion + ) + } + } +} diff --git a/src/main/kotlin/components/TopicRow.kt b/src/main/kotlin/components/TopicRow.kt new file mode 100644 index 0000000..07b626f --- /dev/null +++ b/src/main/kotlin/components/TopicRow.kt @@ -0,0 +1,62 @@ +package components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +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.em +import data.Player +import data.Topic +import data.toColor + +@Composable +@Suppress("FunctionName") +fun TopicRow( + topic: Topic, + doublePoints: Boolean, + users: List, + columnFraction: Float, + onPointsChange: (Player, Long) -> Unit, + onResolveQuestion: () -> Unit +) { + val topicColor = topic.color.toColor() + Column( + modifier = Modifier.fillMaxHeight() + .fillMaxWidth(columnFraction) + ) { + + val numberOfButtons = topic.questions.size + Box( + modifier = Modifier.fillMaxWidth() + .fillMaxHeight(0.1f), + contentAlignment = Alignment.Center + ) { + Text( + text = topic.topic, + fontSize = (2.5).em + ) + } + Column( + modifier = Modifier.fillMaxWidth() + .fillMaxHeight(1f) + ) { + for ((index, question) in topic.questions.withIndex()) { + val buttonHeightFraction = 1f / (numberOfButtons - index) + Question( + questionData = question, + topic = topic, + doublePoints = doublePoints, + players = users, + heightFraction = buttonHeightFraction, + color = topicColor, + onPointsChange = onPointsChange, + onResolve = onResolveQuestion + ) + } + } + } +} diff --git a/src/main/kotlin/components/common/DisabledButton.kt b/src/main/kotlin/components/common/DisabledButton.kt new file mode 100644 index 0000000..21a67c0 --- /dev/null +++ b/src/main/kotlin/components/common/DisabledButton.kt @@ -0,0 +1,22 @@ +package components.common + +import androidx.compose.foundation.layout.fillMaxWidth +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.TextUnit + +@Suppress("FunctionName") +@Composable +fun DisabledButton(maxWidthFraction: Float, fontSize: TextUnit) { + Button( + onClick = {}, + modifier = Modifier.fillMaxWidth(maxWidthFraction), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.DarkGray) + ) { + Text(text = "", fontSize = fontSize, color = Color.White) + } +} diff --git a/src/main/kotlin/components/questiondialog/QuestionDialog.kt b/src/main/kotlin/components/questiondialog/QuestionDialog.kt new file mode 100644 index 0000000..464bc86 --- /dev/null +++ b/src/main/kotlin/components/questiondialog/QuestionDialog.kt @@ -0,0 +1,42 @@ +package components.questiondialog + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.AlertDialog +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.UndecoratedWindowAlertDialogProvider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import components.QuestionDialogButtons +import data.Player +import data.QuestionData +import data.Topic + +@Suppress("FunctionName") +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun QuestionDialogue( + topic: Topic, + questionData: QuestionData, + players: List, + doublePoints: Boolean, + onResolve: () -> Unit, + onPointsChange: (Player, Long) -> Unit +) { + AlertDialog( + dialogProvider = UndecoratedWindowAlertDialogProvider, + modifier = Modifier.fillMaxWidth() + .fillMaxHeight(), + onDismissRequest = { }, + buttons = { + QuestionDialogButtons( + topic = topic, + questionData = questionData, + players = players, + onPointsChange = onPointsChange, + onResolve = onResolve, + doublePoints = doublePoints + ) + } + ) +} diff --git a/src/main/kotlin/components/questiondialog/QuestionDialogPlayer.kt b/src/main/kotlin/components/questiondialog/QuestionDialogPlayer.kt new file mode 100644 index 0000000..964380b --- /dev/null +++ b/src/main/kotlin/components/questiondialog/QuestionDialogPlayer.kt @@ -0,0 +1,74 @@ +package components.questiondialog + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +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.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import components.common.DisabledButton +import data.Player +import data.QuestionData + +@Suppress("FunctionName") +@Composable +fun QuestionDialogPlayer( + player: Player, + fontSize: TextUnit, + questionData: QuestionData, + onPointsChange: (Player, Long) -> Unit, + questionPoints: Long +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + var hadSuccess by remember { mutableStateOf(false) } + var hadFail by remember { mutableStateOf(false) } + Text( + text = player.name, + modifier = Modifier.fillMaxWidth(0.6f), + fontSize = fontSize + ) + Spacer(modifier = Modifier.width(5.dp)) + if (hadFail || (hadSuccess && !questionData.isDeferredDouble)) { + DisabledButton(maxWidthFraction = 1f, fontSize = fontSize) + } else { + if (hadSuccess && questionData.isDeferredDouble) { + DisabledButton(0.7f, fontSize) + } else { + Button( + modifier = Modifier.fillMaxWidth(0.7f), + onClick = { + hadFail = true + onPointsChange( + player, + -questionPoints + ) + }, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text("Wrong", color = Color.White, fontSize = fontSize) + } + } + Spacer(modifier = Modifier.width(5.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + hadSuccess = true + onPointsChange(player, questionPoints) + }, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Green) + ) { + Text("OK", color = Color.White, fontSize = fontSize) + } + } + } +} diff --git a/src/main/kotlin/components/questiondialog/buttons/QuestionDialogButtons.kt b/src/main/kotlin/components/questiondialog/buttons/QuestionDialogButtons.kt new file mode 100644 index 0000000..8026336 --- /dev/null +++ b/src/main/kotlin/components/questiondialog/buttons/QuestionDialogButtons.kt @@ -0,0 +1,83 @@ +package components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +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.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import components.questiondialog.QuestionDialogPlayer +import data.Player +import data.QuestionData +import data.Topic +import data.toColor + +@Suppress("FunctionName") +@Composable +fun QuestionDialogButtons( + topic: Topic, + questionData: QuestionData, + players: List, + onPointsChange: (Player, Long) -> Unit, + onResolve: () -> Unit, + doublePoints: Boolean +) { + val fontSize = 5.em + + Box( + modifier = Modifier.fillMaxHeight().fillMaxWidth() + .border( + border = BorderStroke(150.dp, topic.color.toColor()) + ) + .padding(150.dp), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround + ) { + Text( + text = """"${topic.topic}" for ${questionData.actualUsagePoints(doublePoints)} Pts.""", + fontSize = 2.em + ) + if (questionData.isDeferredDouble) { + Text( + text = """Deferred Double!""", + fontSize = 3.em + ) + } + Text( + text = questionData.hint, + fontSize = 5.em + ) + for (player in players) { + QuestionDialogPlayer( + player = player, + fontSize = fontSize, + questionData = questionData, + onPointsChange = onPointsChange, + questionPoints = questionData.actualUsagePoints(doublePoints).toLong() + ) + Spacer(Modifier.height(20.dp)) + } + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { + onResolve() + } + ) { + Text("Dismiss", color = Color.LightGray, fontSize = fontSize) + } + } + } + } +} diff --git a/src/main/kotlin/components/questiondialog/buttons/QuestionDialogDeferredButtons.kt b/src/main/kotlin/components/questiondialog/buttons/QuestionDialogDeferredButtons.kt new file mode 100644 index 0000000..0acc7e0 --- /dev/null +++ b/src/main/kotlin/components/questiondialog/buttons/QuestionDialogDeferredButtons.kt @@ -0,0 +1,83 @@ +package components.questiondialog.buttons + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +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.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import components.questiondialog.QuestionDialogPlayer +import data.Player +import data.QuestionData +import data.Topic +import data.toColor + +@Suppress("FunctionName") +@Composable +fun QuestionDialogDeferredButtons( + topic: Topic, + questionData: QuestionData, + players: List, + onPointsChange: (Player, Long) -> Unit, + onResolve: () -> Unit, + doublePoints: Boolean +) { + val fontSize = 5.em + + Box( + modifier = Modifier.fillMaxHeight().fillMaxWidth() + .border( + border = BorderStroke(150.dp, topic.color.toColor()) + ) + .padding(150.dp), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround + ) { + Text( + text = """"${topic.topic}" for ${questionData.actualUsagePoints(doublePoints)} Pts.""", + fontSize = 2.em + ) + if (questionData.isDeferredDouble) { + Text( + text = """Deferred Double!""", + fontSize = 3.em + ) + } + Text( + text = questionData.hint, + fontSize = 5.em + ) + for (player in players) { + QuestionDialogPlayer( + player = player, + fontSize = fontSize, + questionData = questionData, + onPointsChange = onPointsChange, + questionPoints = questionData.actualUsagePoints(doublePoints).toLong() + ) + Spacer(Modifier.height(20.dp)) + } + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { + onResolve() + } + ) { + Text("Dismiss", color = Color.LightGray, fontSize = fontSize) + } + } + } + } +} diff --git a/src/main/kotlin/data/ColorData.kt b/src/main/kotlin/data/ColorData.kt new file mode 100644 index 0000000..dac7fa1 --- /dev/null +++ b/src/main/kotlin/data/ColorData.kt @@ -0,0 +1,12 @@ +package data + +import androidx.compose.ui.graphics.Color + +class ColorData( + val red: Int, + val green: Int, + val blue: Int, + val alpha: Int = 0xFF +) + +fun ColorData?.toColor() = this?.let { Color(it.red, it.green, it.blue, it.alpha) } ?: Color.Blue diff --git a/src/main/kotlin/data/Game.kt b/src/main/kotlin/data/Game.kt new file mode 100644 index 0000000..bb0dafc --- /dev/null +++ b/src/main/kotlin/data/Game.kt @@ -0,0 +1,10 @@ +package data + +class Game( + val title: String, + val topics: List, + val players: List, + val headerColor: ColorData? = null, + val doubleAfter: Long = Long.MAX_VALUE, + val endGameAfter: Long = Long.MAX_VALUE +) diff --git a/src/main/kotlin/data/Player.kt b/src/main/kotlin/data/Player.kt new file mode 100644 index 0000000..07c3c0d --- /dev/null +++ b/src/main/kotlin/data/Player.kt @@ -0,0 +1,6 @@ +package data + +class Player( + val name: String, + val color: ColorData? = null +) diff --git a/src/main/kotlin/data/QuestionData.kt b/src/main/kotlin/data/QuestionData.kt new file mode 100644 index 0000000..383fa1a --- /dev/null +++ b/src/main/kotlin/data/QuestionData.kt @@ -0,0 +1,12 @@ +package data + +class QuestionData( + val hint: String, + val answer: String, + val points: UInt, + val isDouble: Boolean = false, + val isDeferredDouble: Boolean = false +) { + fun actualDisplayPoints(double: Boolean) = points * (if (double) 2u else 1u) + fun actualUsagePoints(double: Boolean) = points * (if (double) 2u else 1u) * (if (isDouble) 2u else 1u) +} diff --git a/src/main/kotlin/data/Topic.kt b/src/main/kotlin/data/Topic.kt new file mode 100644 index 0000000..9e4c17d --- /dev/null +++ b/src/main/kotlin/data/Topic.kt @@ -0,0 +1,7 @@ +package data + +class Topic( + val topic: String, + val questions: List, + val color: ColorData? = null +)