diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..e40abe2 --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use flake . diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 1114582..7c58a03 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -1,9 +1,7 @@ name: compliance tasks on: push: - branches: [ main, develop ] pull_request: - branches: [ main, develop ] permissions: contents: read @@ -15,7 +13,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: REUSE Compliance check - uses: fsfe/reuse-action@v2 + uses: fsfe/reuse-action@v6.0.0 openapi: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 545531d..1e3e3ac 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,11 +18,11 @@ jobs: key: ${{ runner.os }}-gradle-test-${{ hashFiles('**/*.gradle') }} restore-keys: | ${{ runner.os }}-gradle-test- - - name: Set up JDK 17 + - name: Set up JDK 25 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 25 - name: Docker login uses: docker/login-action@v1 with: diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 11a0006..85e14af 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,9 +1,7 @@ name: Gradle build on: push: - branches: [ main, develop ] pull_request: - branches: [ main, develop ] permissions: contents: read @@ -17,11 +15,13 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 25 + - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: build --no-daemon + uses: gradle/actions/setup-gradle@v5 + + - name: Build with Gradle + run: ./gradlew build --no-daemon checkstyle: runs-on: ubuntu-latest steps: @@ -30,8 +30,9 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 25 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: checkstyleMain checkstyleTest --no-daemon + uses: gradle/actions/setup-gradle@v5 + + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest --no-daemon diff --git a/.gitignore b/.gitignore index 52b2545..2a628ef 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ out/ .settings .classpath .factorypath + +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 631f183..8c44dc6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,17 +5,19 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/python-openapi/openapi-spec-validator - rev: 0.5.5 # The version to use or 'master' for latest + rev: 0.7.2 hooks: - id: openapi-spec-validator - repo: https://github.com/fsfe/reuse-tool - rev: v1.0.0 + rev: v6.1.2 hooks: - - id: reuse + - id: reuse-lint-file + additional_dependencies: + - click diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index ac76fcf..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,21 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: codosseum-backend -Upstream-Contact: JohnnyJayJay -Source: https://github.com/codosseum-org/backend - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -Files: .github/workflows/* -Copyright: JohnnyJayJay -License: CC0-1.0 - -Files: - gradle/* - gradlew - gradlew.bat -Copyright: Gradle, Inc. -License: Apache-2.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..53c02f1 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,16 @@ +version = 1 +SPDX-PackageName = "codosseum-backend" +SPDX-PackageSupplier = "JohnnyJayJay " +SPDX-PackageDownloadLocation = "https://github.com/codosseum-org/backend" + +[[annotations]] +path = ".github/workflows/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "JohnnyJayJay" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["gradle/**", "gradlew", "gradlew.bat"] +precedence = "aggregate" +SPDX-FileCopyrightText = "Gradle, Inc." +SPDX-License-Identifier = "Apache-2.0" diff --git a/build.gradle.kts b/build.gradle.kts index db28916..5c15d9f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,20 @@ +import de.undercouch.gradle.tasks.download.Download + + // SPDX-FileCopyrightText: 2023 Alex Wood // SPDX-License-Identifier: AGPL-3.0-or-later plugins { - id("com.github.johnrengelman.shadow") version "8.1.1" - id("io.micronaut.application") version "4.0.1" - id("io.micronaut.aot") version "4.0.1" - id("io.micronaut.openapi") version "4.0.1" + id("io.micronaut.application") version "4.5.5" + id("io.micronaut.aot") version "4.5.5" + id("io.micronaut.openapi") version "4.5.5" + id("groovy") + id("de.undercouch.download") version "5.6.0" checkstyle } version = "0.1.0" group = "org.developerden" +val challengesOpenApiSpec: Provider = layout.buildDirectory.file("openapi/challenges-openapi.yaml") repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") { @@ -20,38 +25,64 @@ repositories { dependencies { annotationProcessor("io.micronaut.openapi:micronaut-openapi") + implementation("io.micronaut.openapi:micronaut-openapi-annotations") annotationProcessor("io.micronaut.serde:micronaut-serde-processor") annotationProcessor("io.micronaut.validation:micronaut-validation-processor") annotationProcessor("io.micronaut.security:micronaut-security-annotations") - annotationProcessor("io.soabase.record-builder:record-builder-processor:37") + annotationProcessor("io.soabase.record-builder:record-builder-processor:49") + annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3") implementation("io.micronaut.reactor:micronaut-reactor") + implementation("io.micronaut.cache:micronaut-cache-caffeine") implementation("io.micronaut.security:micronaut-security") implementation("io.micronaut.validation:micronaut-validation") - implementation("io.soabase.record-builder:record-builder-core:37") + implementation("io.micronaut.security:micronaut-security-oauth2") + implementation("io.micronaut.security:micronaut-security-jwt") + implementation("io.soabase.record-builder:record-builder-core:49") implementation("io.micronaut.serde:micronaut-serde-jackson") implementation("io.swagger.core.v3:swagger-annotations") implementation("io.micronaut:micronaut-http-client") implementation("com.github.kkuegler:human-readable-ids-java:0.1.1") implementation("com.networknt:json-schema-validator:1.0.86") + implementation("org.spdx:java-spdx-library:(,2.0]") { + exclude("org.apache.logging.log4j") + } + implementation("org.mapstruct:mapstruct:1.6.3") runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.yaml:snakeyaml") + + + testAnnotationProcessor("io.micronaut:micronaut-inject-java") + testImplementation("org.junit.jupiter:junit-jupiter:5.7.1") + testImplementation("io.micronaut.test:micronaut-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("io.micronaut.test:micronaut-test-spock") + testImplementation(platform("org.spockframework:spock-bom:+")) + testImplementation("org.spockframework:spock-core") + } application { - mainClass.set("org.developerden.codosseum.server.Application") + mainClass.set("org.developerden.codosseum.Application") } java { - sourceCompatibility = JavaVersion.toVersion("17") - targetCompatibility = JavaVersion.toVersion("17") + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } checkstyle { - toolVersion = "10.12.2" + toolVersion = "11.1.0" configFile = configDirectory.file("google_checks.xml").get().asFile - sourceSets = emptySet() + sourceSets = listOf(project.sourceSets.main.get()) +} + +tasks.withType { + exclude { + it.file.path.contains("generated/openapi") // TODO: this could be slow? + } } tasks { @@ -67,6 +98,7 @@ tasks { graalvmNative.toolchainDetection.set(false) micronaut { + runtime("netty") testRuntime("junit5") processing { @@ -86,6 +118,38 @@ micronaut { } openapi { - + client( + "challenges-service", + challengesOpenApiSpec + ) { + apiPackageName = "org.developerden.codosseum.challenges.client.api" + modelPackageName = "org.developerden.codosseum.challenges.client.model" + useOptional = true + useSealed = true + } } } + + +val downloadChallengesServiceOpenApi by tasks.registering(Download::class) { + src("https://raw.githubusercontent.com/codosseum-org/challenges-service/refs/heads/openapi/openapi.yaml") + dest(challengesOpenApiSpec) + overwrite(true) + onlyIfModified(false) +} + +tasks.named("generateChallenges-serviceOpenApiModels") { // task created by micronaut-openapi plugin + dependsOn(downloadChallengesServiceOpenApi) +} + +tasks.named("generateChallenges-serviceOpenApiApis") { // task created by micronaut-openapi plugin + dependsOn(downloadChallengesServiceOpenApi) +} + +tasks.named("test") { + useJUnitPlatform() + outputs.upToDateWhen { false } + systemProperties["junit.jupiter.execution.parallel.enabled"] = true + systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) +} diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml index 1728dc8..45af060 100644 --- a/config/checkstyle/google_checks.xml +++ b/config/checkstyle/google_checks.xml @@ -4,8 +4,8 @@ SPDX-FileCopyrightText: Max Vetrenko, Ruslan Diachenko, Roman Ivanov. SPDX-License-Identifier: LGPL-2.1-or-later --> + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - - + - + - - - - - - - - - - - - - - - - - + + + + + + - - - - - + - - - - - - - - - - - - - - - - + + + + - - - + + + + + + - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..eed65d6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1759362264, + "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759735786, + "narHash": "sha256-a0+h02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "20c4598c84a671783f741e02bf05cbfaf4907cff", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754788789, + "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1754340878, + "narHash": "sha256-lgmUyVQL9tSnvvIvBp7x1euhkkCho7n3TMzgjdvgPoU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "cab778239e705082fe97bb4990e0d24c50924c04", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1758728421, + "narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.lock.license b/flake.lock.license new file mode 100644 index 0000000..4fdbaad --- /dev/null +++ b/flake.lock.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e8de626 --- /dev/null +++ b/flake.nix @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + description = "Codosseum Backend Development Flake"; + + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + }; + + nixConfig = { + extra-trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE=" + ]; + extra-substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org/" + "https://numtide.cachix.org" + ]; + }; + + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.treefmt-nix.flakeModule + ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + perSystem = + { + config, + self', + inputs', + pkgs, + system, + ... + }: + { + treefmt = { + flakeCheck = true; + flakeFormatter = true; + + programs.nixfmt.enable = true; + }; + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + pre-commit + reuse + checkstyle + ]; + }; + }; + flake = { + }; + }; +} diff --git a/gradle.properties b/gradle.properties index 4f96351..f9e02d7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ # SPDX-FileCopyrightText: 2023 Alex Wood # SPDX-License-Identifier: AGPL-3.0-or-later -micronautVersion=4.0.1 +micronautVersion=4.9.4 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..8bdaf60 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f4197d..2e11132 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 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. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -111,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -169,7 +171,6 @@ fi # 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" ) @@ -201,16 +202,15 @@ fi # 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"' -# 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. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,93 @@ -@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=. -@rem This is normally unused -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% equ 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% equ 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! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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=. +@rem This is normally unused +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% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/openapi.properties b/openapi.properties new file mode 100644 index 0000000..b758198 --- /dev/null +++ b/openapi.properties @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) +# +# SPDX-License-Identifier: AGPL-3.0-or-later +swagger-ui.enabled=true diff --git a/openapi.yaml b/openapi.yaml index 8a56078..82f7ec5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -699,7 +699,7 @@ components: description: Current game state timeLeft: type: integer - description: Time left in seconds until "next thing" (warmup countdown, round timer, next rount countdown) + description: Time left in seconds until "next thing" (warmup countdown, round timer, next round countdown) EliminatedEvent: type: object diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java deleted file mode 100644 index aec4d8c..0000000 --- a/src/main/java/module-info.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see . - * - */ - -module codosseum { - - requires io.micronaut.core; - requires io.micronaut.reactor.micronaut_reactor; - requires io.micronaut.serde.micronaut_serde_api; - requires io.micronaut.http_client; - requires io.micronaut.http_server; - requires io.swagger.v3.oas.annotations; - requires io.soabase.recordbuilder.core; - requires jakarta.annotation; - requires jakarta.validation; - requires com.fasterxml.jackson.annotation; - requires jakarta.inject; - requires io.micronaut.security.micronaut_security; - requires java.compiler; - requires io.micronaut.http; - requires io.micronaut.validation.micronaut_validation; - requires org.reactivestreams; - requires io.micronaut.context; - requires io.micronaut.router; - requires reactor.core; - -} diff --git a/src/main/java/org/developerden/codosseum/Application.java b/src/main/java/org/developerden/codosseum/Application.java index 49ba3ab..49543f0 100644 --- a/src/main/java/org/developerden/codosseum/Application.java +++ b/src/main/java/org/developerden/codosseum/Application.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum; @@ -28,7 +25,6 @@ ) ) public class Application { - public static void main(String[] args) { Micronaut.run(Application.class, args); } diff --git a/src/main/java/org/developerden/codosseum/auth/CurrentPlayer.java b/src/main/java/org/developerden/codosseum/auth/CurrentPlayer.java new file mode 100644 index 0000000..f71e680 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/auth/CurrentPlayer.java @@ -0,0 +1,30 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.auth; + +import java.util.Set; +import java.util.UUID; + +/** + * Request-scoped view of the authenticated caller in the context of a game. + * Values are parsed and normalised from {@link io.micronaut.security.authentication.Authentication} attributes. + */ +public record CurrentPlayer( + String name, + UUID userId, // present for oauth users + UUID activeGameId, // present for game-scoped operations + Set roles +) { +} diff --git a/src/main/java/org/developerden/codosseum/auth/GameAuthorizationRule.java b/src/main/java/org/developerden/codosseum/auth/GameAuthorizationRule.java index 77bb244..117d4cd 100644 --- a/src/main/java/org/developerden/codosseum/auth/GameAuthorizationRule.java +++ b/src/main/java/org/developerden/codosseum/auth/GameAuthorizationRule.java @@ -1,30 +1,27 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpAttributes; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.rules.AbstractSecurityRule; import io.micronaut.security.rules.SecurityRuleResult; import io.micronaut.security.token.RolesFinder; import io.micronaut.web.router.MethodBasedRouteMatch; +import io.micronaut.web.router.RouteAttributes; import io.micronaut.web.router.RouteMatch; import jakarta.inject.Singleton; import java.util.Arrays; @@ -38,10 +35,16 @@ protected GameAuthorizationRule(RolesFinder rolesFinder) { super(rolesFinder); } + @Override + public int getOrder() { + return Integer.MIN_VALUE + 100; + } + @Override public Publisher check(@Nullable HttpRequest request, @Nullable Authentication authentication) { - RouteMatch routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class) + + RouteMatch routeMatch = RouteAttributes.getRouteMatch(request) .orElse(null); if (routeMatch instanceof MethodBasedRouteMatch methodMatch && methodMatch.hasAnnotation(GameAuthorized.class)) { @@ -66,6 +69,8 @@ public Publisher check(@Nullable HttpRequest request, } private boolean matchesGameId(Authentication authentication, String gameId) { - return gameId.equals(authentication.getAttributes().get("activeGameId")); + return gameId.equals( + String.valueOf(authentication.getAttributes().get(PlayerAuthentication.ACTIVE_GAME_ID)) + ); } } diff --git a/src/main/java/org/developerden/codosseum/auth/GameAuthorized.java b/src/main/java/org/developerden/codosseum/auth/GameAuthorized.java index 45581a2..577d489 100644 --- a/src/main/java/org/developerden/codosseum/auth/GameAuthorized.java +++ b/src/main/java/org/developerden/codosseum/auth/GameAuthorized.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; @@ -22,6 +19,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation to specify that a route requires the user to have one of the specified roles in a game. + * If the user does not have one of the specified roles, a 403 Forbidden response will be returned. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface GameAuthorized { diff --git a/src/main/java/org/developerden/codosseum/auth/GameKeyTokenReader.java b/src/main/java/org/developerden/codosseum/auth/GameKeyTokenReader.java index b8fc3e9..35dad15 100644 --- a/src/main/java/org/developerden/codosseum/auth/GameKeyTokenReader.java +++ b/src/main/java/org/developerden/codosseum/auth/GameKeyTokenReader.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; @@ -24,7 +21,7 @@ public class GameKeyTokenReader extends HttpHeaderTokenReader { @Override protected String getPrefix() { - return "Game "; + return "Game"; } @Override diff --git a/src/main/java/org/developerden/codosseum/auth/GameKeyTokenValidator.java b/src/main/java/org/developerden/codosseum/auth/GameKeyTokenValidator.java index 87a5438..05411c1 100644 --- a/src/main/java/org/developerden/codosseum/auth/GameKeyTokenValidator.java +++ b/src/main/java/org/developerden/codosseum/auth/GameKeyTokenValidator.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; @@ -22,12 +19,34 @@ import io.micronaut.security.authentication.Authentication; import io.micronaut.security.token.validator.TokenValidator; import jakarta.inject.Singleton; +import java.util.Optional; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.repository.AuthRepository; import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; @Singleton public class GameKeyTokenValidator implements TokenValidator> { + private final AuthRepository authRepository; + private final GameKeyTokenReader gameKeyTokenReader; + + public GameKeyTokenValidator(AuthRepository authRepository, + GameKeyTokenReader gameKeyTokenReader) { + this.authRepository = authRepository; + this.gameKeyTokenReader = gameKeyTokenReader; + } + @Override public Publisher validateToken(String token, @Nullable HttpRequest request) { - return null; + if (request == null || token == null || token.isBlank()) { + return Mono.empty(); + } + if (gameKeyTokenReader.findToken(request).isEmpty()) { + return Mono.empty(); // not applicable here + } + Optional playerByGameKey = authRepository.findPlayerByGameKey(token); + return Mono.justOrEmpty(playerByGameKey) + .map(PlayerAuthentication::buildFrom); + } } diff --git a/src/main/java/org/developerden/codosseum/auth/GameRole.java b/src/main/java/org/developerden/codosseum/auth/GameRole.java index c7bc648..f4256f8 100644 --- a/src/main/java/org/developerden/codosseum/auth/GameRole.java +++ b/src/main/java/org/developerden/codosseum/auth/GameRole.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; diff --git a/src/main/java/org/developerden/codosseum/auth/PlayerAuthentication.java b/src/main/java/org/developerden/codosseum/auth/PlayerAuthentication.java index 5f7b4c3..1ecc425 100644 --- a/src/main/java/org/developerden/codosseum/auth/PlayerAuthentication.java +++ b/src/main/java/org/developerden/codosseum/auth/PlayerAuthentication.java @@ -1,36 +1,46 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.auth; import io.micronaut.security.authentication.Authentication; import jakarta.annotation.Nonnull; +import java.util.EnumSet; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; +import org.developerden.codosseum.model.player.EphemeralPlayer; public final class PlayerAuthentication { + public static final String ACTIVE_GAME_ID = "activeGameId"; + public static Authentication build( - @Nonnull String name, @Nonnull String activeGameId, @Nonnull Set roles) { + @Nonnull String name, @Nonnull UUID activeGameId, @Nonnull Set roles) { return Authentication.build( name, roles.stream().map(Enum::name).collect(Collectors.toSet()), - Map.of("activeGameId", activeGameId) + Map.of(ACTIVE_GAME_ID, activeGameId) + ); + } + + public static Authentication buildFrom(EphemeralPlayer ephemeralPlayer) { + return build( + ephemeralPlayer.name(), + ephemeralPlayer.gameId(), + ephemeralPlayer.admin() ? EnumSet.of(GameRole.ADMIN, GameRole.PLAYER) : EnumSet.of(GameRole.PLAYER) ); } diff --git a/src/main/java/org/developerden/codosseum/auth/package-info.java b/src/main/java/org/developerden/codosseum/auth/package-info.java index fdfe0b0..48c58b5 100644 --- a/src/main/java/org/developerden/codosseum/auth/package-info.java +++ b/src/main/java/org/developerden/codosseum/auth/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/challenge/Challenge.java b/src/main/java/org/developerden/codosseum/challenge/Challenge.java index 5b0676a..507ae0e 100644 --- a/src/main/java/org/developerden/codosseum/challenge/Challenge.java +++ b/src/main/java/org/developerden/codosseum/challenge/Challenge.java @@ -1,22 +1,20 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.challenge; +import io.micronaut.core.annotation.Introspected; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; @@ -24,16 +22,17 @@ import jakarta.validation.constraints.Size; import java.util.List; import java.util.Set; +import org.developerden.codosseum.challenge.validation.SpdxId; +@Introspected public record Challenge( @Nullable Author author, - // Custom validator + @SpdxId @Nullable String license, - // Custom validator @Nullable String language, diff --git a/src/main/java/org/developerden/codosseum/challenge/input/package-info.java b/src/main/java/org/developerden/codosseum/challenge/input/package-info.java index d625135..33049b2 100644 --- a/src/main/java/org/developerden/codosseum/challenge/input/package-info.java +++ b/src/main/java/org/developerden/codosseum/challenge/input/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/challenge/package-info.java b/src/main/java/org/developerden/codosseum/challenge/package-info.java index 0151cb5..2a48cf9 100644 --- a/src/main/java/org/developerden/codosseum/challenge/package-info.java +++ b/src/main/java/org/developerden/codosseum/challenge/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/challenge/template/package-info.java b/src/main/java/org/developerden/codosseum/challenge/template/package-info.java index 480d34b..91740be 100644 --- a/src/main/java/org/developerden/codosseum/challenge/template/package-info.java +++ b/src/main/java/org/developerden/codosseum/challenge/template/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/challenge/validation/SpdxId.java b/src/main/java/org/developerden/codosseum/challenge/validation/SpdxId.java new file mode 100644 index 0000000..e2a6110 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/challenge/validation/SpdxId.java @@ -0,0 +1,29 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.challenge.validation; + +import jakarta.validation.Constraint; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = SpdxIdConstraintValidator.class) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SpdxId { +} diff --git a/src/main/java/org/developerden/codosseum/challenge/validation/SpdxIdConstraintValidator.java b/src/main/java/org/developerden/codosseum/challenge/validation/SpdxIdConstraintValidator.java new file mode 100644 index 0000000..8328caa --- /dev/null +++ b/src/main/java/org/developerden/codosseum/challenge/validation/SpdxIdConstraintValidator.java @@ -0,0 +1,43 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.challenge.validation; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.validation.validator.constraints.ConstraintValidator; +import io.micronaut.validation.validator.constraints.ConstraintValidatorContext; +import org.spdx.library.model.license.InvalidLicenseStringException; +import org.spdx.library.model.license.LicenseInfoFactory; + + +public class SpdxIdConstraintValidator implements ConstraintValidator { + + @Override + public boolean isValid(@Nullable String value, + @NonNull AnnotationValue annotationMetadata, + @NonNull ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + try { + LicenseInfoFactory.parseSPDXLicenseString(value); + } catch (InvalidLicenseStringException e) { + return false; + } + return true; + } +} diff --git a/src/main/java/org/developerden/codosseum/challenge/validation/package-info.java b/src/main/java/org/developerden/codosseum/challenge/validation/package-info.java index f9aafbe..436f16f 100644 --- a/src/main/java/org/developerden/codosseum/challenge/validation/package-info.java +++ b/src/main/java/org/developerden/codosseum/challenge/validation/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/config/ChallengeDirectory.java b/src/main/java/org/developerden/codosseum/config/ChallengeDirectory.java new file mode 100644 index 0000000..2091b19 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/config/ChallengeDirectory.java @@ -0,0 +1,25 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.config; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import java.nio.file.Path; + +@JsonTypeName("directory") +public record ChallengeDirectory( + Path path +) implements ChallengeSource { + +} diff --git a/src/main/java/org/developerden/codosseum/config/ChallengeRepository.java b/src/main/java/org/developerden/codosseum/config/ChallengeRepository.java new file mode 100644 index 0000000..d861f4b --- /dev/null +++ b/src/main/java/org/developerden/codosseum/config/ChallengeRepository.java @@ -0,0 +1,36 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.config; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.time.Duration; + +@JsonTypeName("repo") +public record ChallengeRepository( + @Nonnull + String url, + + @Nonnull + Duration pullInterval, + + @Nullable + String username, + + @Nullable + String password +) implements ChallengeSource { +} diff --git a/src/main/java/org/developerden/codosseum/config/ChallengeSource.java b/src/main/java/org/developerden/codosseum/config/ChallengeSource.java new file mode 100644 index 0000000..174812f --- /dev/null +++ b/src/main/java/org/developerden/codosseum/config/ChallengeSource.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.config; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(ChallengeRepository.class), + @JsonSubTypes.Type(ChallengeDirectory.class) +}) +public sealed interface ChallengeSource permits ChallengeRepository, ChallengeDirectory { +} diff --git a/src/main/java/org/developerden/codosseum/config/CodosseumConfig.java b/src/main/java/org/developerden/codosseum/config/CodosseumConfig.java new file mode 100644 index 0000000..c0180a4 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/config/CodosseumConfig.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.config; + +import io.micronaut.context.annotation.ConfigurationProperties; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.time.Duration; +import java.util.List; + + +@ConfigurationProperties("codosseum") +public record CodosseumConfig( + @Nonnull + ChallengeSource challengeSource, + + @Nonnull + Duration gameRetention, + + @Nullable + List disabledLanguages, + + @Nullable + List enabledLanguages + + +) { +} diff --git a/src/main/java/org/developerden/codosseum/config/package-info.java b/src/main/java/org/developerden/codosseum/config/package-info.java index 41c92d4..82ee863 100644 --- a/src/main/java/org/developerden/codosseum/config/package-info.java +++ b/src/main/java/org/developerden/codosseum/config/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/controller/GameController.java b/src/main/java/org/developerden/codosseum/controller/GameController.java index 8db6a29..4ec0596 100644 --- a/src/main/java/org/developerden/codosseum/controller/GameController.java +++ b/src/main/java/org/developerden/codosseum/controller/GameController.java @@ -1,23 +1,21 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.controller; import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; @@ -31,54 +29,145 @@ import io.micronaut.http.sse.Event; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; import io.micronaut.validation.Validated; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.annotation.Nullable; import jakarta.validation.Valid; +import java.net.URI; import java.security.Principal; +import java.util.Optional; +import java.util.UUID; import org.developerden.codosseum.auth.GameAuthorized; import org.developerden.codosseum.auth.GameRole; +import org.developerden.codosseum.controller.binder.GameParam; +import org.developerden.codosseum.dto.GameCreateRequest; import org.developerden.codosseum.dto.GameCreateResponse; import org.developerden.codosseum.dto.GameInfo; import org.developerden.codosseum.dto.GameSettings; import org.developerden.codosseum.event.GameEvent; import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.service.GameService; +import org.developerden.codosseum.service.game.event.SseEventSink; import org.reactivestreams.Publisher; @Validated @Controller("/games") +@Secured(SecurityRule.IS_AUTHENTICATED) public class GameController { + private final GameService gameService; + + private final SseEventSink eventSink; + + public GameController(GameService gameService, SseEventSink eventSink) { + this.gameService = gameService; + this.eventSink = eventSink; + } + @Post - public HttpResponse createGame(@Valid @Body GameSettings settings) { - throw new UnsupportedOperationException(); + @Secured(SecurityRule.IS_ANONYMOUS) + @ApiResponse( + responseCode = "201", + description = "new game created", + content = @Content(), + headers = @Header( + name = "Location", + description = "URL of the newly created game", + required = true, + schema = @Schema( + type = "string", + format = "uri-reference" + ) + ) + ) + public HttpResponse createGame(@Valid @Body GameCreateRequest request) { + GameCreateResponse response = gameService.createGame(request); + return HttpResponse.created(response, URI.create(response.id().toString())); } - // typed argument binding for looking up games @Get("/{id}") - public HttpResponse getGame(@PathVariable("id") Game game) { - throw new UnsupportedOperationException(); + @Secured(SecurityRule.IS_ANONYMOUS) + public HttpResponse getGame(@PathVariable("id") UUID gameId) { + Optional game = gameService.getGame(gameId); + if (game.isEmpty()) { + return HttpResponse.notFound(); + } + return HttpResponse.ok(game.get()); } @Patch("/{id}") @GameAuthorized(GameRole.ADMIN) public HttpResponse updateGame( Principal principal, - @PathVariable("id") Game game, + @PathVariable("id") String gameId, @Valid @Body GameSettings settings ) { - throw new UnsupportedOperationException(); + return HttpResponse.ok(gameService.updateGame(gameId, settings)); } @Delete("/{id}") @GameAuthorized(GameRole.ADMIN) - public HttpResponse deleteGame(Principal principal, @PathVariable("id") Game game) { - throw new UnsupportedOperationException(); + public HttpResponse deleteGame(Principal principal, @PathVariable("id") String gameId) { + gameService.deleteGame(gameId); + return HttpResponse.noContent(); + } + + @Post("/{id}/warmup") + @GameAuthorized(GameRole.ADMIN) + @Operation(operationId = "beginWarmup", + summary = "Force the warmup phase of a game to begin", + description = "Starts the warmup phase of a game, regardless of player-count" + ) + @ApiResponse( + responseCode = "204", + description = "Successfully started the warmup phase. " + + "Further info will be received via server-sent events." + ) + @ApiResponse( + responseCode = "409", + description = "Game is already running or finished") + public HttpResponse beginWarmup(Principal principal, + @PathVariable("id") UUID id, + @Parameter(hidden = true) @GameParam Game game) { + try { + gameService.beginWarmup(game); + return HttpResponse.noContent(); + } catch (IllegalStateException e) { + return HttpResponse.status(HttpStatus.CONFLICT); + } } + @Post("/{id}/start") @GameAuthorized(GameRole.ADMIN) - public HttpResponse startGame(Principal principal, @PathVariable("id") Game game) { - throw new UnsupportedOperationException(); + @Operation(operationId = "startGame", + summary = "Start a game", + description = "Forcefully start a game, regardless of player-count and warmup time") + @ApiResponse( + responseCode = "204", + description = "Successfully started the game. " + + "Further info will be received via server-sent events." + ) + @ApiResponse( + responseCode = "409", + description = "Game is already running or finished" + ) + + public HttpResponse startGame(Principal principal, @PathVariable("id") UUID gameId) { + try { + gameService.startGame(gameId); + } catch (IllegalStateException e) { + return HttpResponse.status(HttpStatus.CONFLICT); + } + + return HttpResponse.noContent(); } @Get("/{id}/template") @@ -86,27 +175,33 @@ public HttpResponse startGame(Principal principal, @PathVariable("id") Gam @Produces(MediaType.TEXT_PLAIN) public HttpResponse getCodeTemplate( Principal principal, - @PathVariable("id") Game game, + @PathVariable("id") String gameId, + // add custom validation annotation here @QueryValue("lang") String language ) { - throw new UnsupportedOperationException(); + return HttpResponse.ok(gameService.getTemplate(gameId, language)); } @Post("/{id}/restart") @GameAuthorized(GameRole.PLAYER) public HttpResponse restartGame( - Principal principal, @PathVariable("id") Game game + Principal principal, + @PathVariable("id") String gameId, + @Valid @Body GameSettings settings ) { - throw new UnsupportedOperationException(); + GameCreateResponse response = gameService.restartGame(gameId); + return HttpResponse.created(response, URI.create(response.id().toString())); } @ExecuteOn(TaskExecutors.IO) @Get("/{id}/events") @Produces(MediaType.TEXT_EVENT_STREAM) + @Secured(SecurityRule.IS_ANONYMOUS) public Publisher> subscribeToGameEvents( @Nullable Principal principal, - @PathVariable("id") Game game + @PathVariable("id") UUID gameId ) { - throw new UnsupportedOperationException(); + return eventSink.subscribeToPublicSse(gameId); } + } diff --git a/src/main/java/org/developerden/codosseum/controller/InfoController.java b/src/main/java/org/developerden/codosseum/controller/InfoController.java index 66c5ff6..80ef664 100644 --- a/src/main/java/org/developerden/codosseum/controller/InfoController.java +++ b/src/main/java/org/developerden/codosseum/controller/InfoController.java @@ -1,27 +1,27 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.controller; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; import java.util.List; @Controller +@Secured(SecurityRule.IS_ANONYMOUS) public class InfoController { @Get("/languages") diff --git a/src/main/java/org/developerden/codosseum/controller/PlayerController.java b/src/main/java/org/developerden/codosseum/controller/PlayerController.java deleted file mode 100644 index 9656fde..0000000 --- a/src/main/java/org/developerden/codosseum/controller/PlayerController.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see . - * - */ - -package org.developerden.codosseum.controller; - -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Delete; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.PathVariable; -import io.micronaut.http.annotation.Post; -import io.micronaut.validation.Validated; -import jakarta.validation.Valid; -import java.security.Principal; -import org.developerden.codosseum.auth.GameAuthorized; -import org.developerden.codosseum.auth.GameRole; -import org.developerden.codosseum.dto.GameJoinResponse; -import org.developerden.codosseum.dto.Player; -import org.developerden.codosseum.dto.Players; -import org.developerden.codosseum.model.Game; - -@Validated -@Controller("/games/{id}/players") -public class PlayerController { - - @Get - public HttpResponse getPlayers(@PathVariable("id") Game game) { - throw new UnsupportedOperationException(); - } - - @Post - public HttpResponse joinGame( - @PathVariable("id") Game game, - @Valid @Body Player player - ) { - throw new UnsupportedOperationException(); - } - - - @Delete("/@self") - @GameAuthorized(GameRole.PLAYER) - public HttpResponse leaveGame(Principal principal, @PathVariable("id") Game game) { - throw new UnsupportedOperationException(); - } - -} diff --git a/src/main/java/org/developerden/codosseum/controller/PlayersController.java b/src/main/java/org/developerden/codosseum/controller/PlayersController.java new file mode 100644 index 0000000..4a82ef7 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/controller/PlayersController.java @@ -0,0 +1,85 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.controller; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.validation.Validated; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import java.security.Principal; +import java.util.UUID; +import org.developerden.codosseum.auth.GameAuthorized; +import org.developerden.codosseum.auth.GameRole; +import org.developerden.codosseum.controller.binder.GameParam; +import org.developerden.codosseum.dto.GameJoinResponse; +import org.developerden.codosseum.dto.Player; +import org.developerden.codosseum.dto.Players; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.service.GameService; + +@Validated +@Controller("/games/{id}/players") +@Secured(SecurityRule.IS_AUTHENTICATED) +public class PlayersController { + + private final GameService gameService; + + + public PlayersController(GameService gameService) { + this.gameService = gameService; + + } + + @Get + @Secured(SecurityRule.IS_ANONYMOUS) + public HttpResponse getPlayers(@PathVariable("id") UUID id, + @Parameter(hidden = true) @GameParam Game game) { + // TODO: this route seems very redundant and unhelpful + return gameService.getGame(game.id()) + .map(g -> HttpResponse.ok(g.players())) + .orElse(HttpResponse.notFound()); + } + + @Post + @Secured(SecurityRule.IS_ANONYMOUS) + public HttpResponse joinGame( + @PathVariable("id") UUID id, @Parameter(hidden = true) @GameParam Game game, + @Valid @Body Player player + ) { + try { + return gameService.addPlayer(game.id(), player) + .map(HttpResponse::ok) + .orElse(HttpResponse.notFound()); + } catch (IllegalStateException e) { + return HttpResponse.status(409, e.getMessage()); + } + } + + @Delete("/@self") + @GameAuthorized(GameRole.PLAYER) + public HttpResponse leaveGame(Principal principal, @PathVariable("id") Game game) { + throw new UnsupportedOperationException(); + } + + +} diff --git a/src/main/java/org/developerden/codosseum/controller/RoundController.java b/src/main/java/org/developerden/codosseum/controller/RoundController.java index d36d31d..5997eef 100644 --- a/src/main/java/org/developerden/codosseum/controller/RoundController.java +++ b/src/main/java/org/developerden/codosseum/controller/RoundController.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.controller; @@ -25,7 +22,6 @@ import java.security.Principal; import org.developerden.codosseum.dto.PlayerRoundResult; import org.developerden.codosseum.dto.Round; -import org.developerden.codosseum.model.Game; @Controller("/games/{id}/rounds") public class RoundController { @@ -33,7 +29,7 @@ public class RoundController { @Get("/{round}") public HttpResponse getRound( Principal principal, - @PathVariable("id") Game game, + @PathVariable("id") String gameId, @PathVariable int round, @QueryValue(defaultValue = "false") boolean withCode ) { @@ -42,7 +38,7 @@ public HttpResponse getRound( @Get("/{round}/results/{player}") public HttpResponse getRoundResults( - @PathVariable("id") Game game, + @PathVariable("id") String gameId, @PathVariable int round, @PathVariable String player ) { diff --git a/src/main/java/org/developerden/codosseum/controller/SolutionController.java b/src/main/java/org/developerden/codosseum/controller/SolutionController.java index 7324d33..93ecc6a 100644 --- a/src/main/java/org/developerden/codosseum/controller/SolutionController.java +++ b/src/main/java/org/developerden/codosseum/controller/SolutionController.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.controller; @@ -31,7 +28,6 @@ import org.developerden.codosseum.auth.GameRole; import org.developerden.codosseum.dto.Submission; import org.developerden.codosseum.dto.TestResponse; -import org.developerden.codosseum.model.Game; @Validated @Controller("/games/{id}/solutions") @@ -40,7 +36,7 @@ public class SolutionController { @Post("/test") @GameAuthorized(GameRole.PLAYER) public HttpResponse testSolution( - @PathVariable("id") Game game, + @PathVariable("id") String gameId, @Nullable @QueryValue List testNumbers, @Valid @Body Submission submission ) { @@ -50,7 +46,7 @@ public HttpResponse testSolution( @Post("/submit") @GameAuthorized(GameRole.PLAYER) public HttpResponse submitSolution( - @PathVariable("id") Game game, + @PathVariable("id") String gameId, @Valid @Body Submission submission ) { throw new UnsupportedOperationException(); diff --git a/src/main/java/org/developerden/codosseum/controller/UserController.java b/src/main/java/org/developerden/codosseum/controller/UserController.java new file mode 100644 index 0000000..07a8e28 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/controller/UserController.java @@ -0,0 +1,57 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.controller; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.validation.Validated; +import jakarta.inject.Inject; +import org.developerden.codosseum.dto.PlayersMapper; +import org.developerden.codosseum.dto.user.User; +import org.developerden.codosseum.model.player.UserMapper; +import org.developerden.codosseum.repository.AuthRepository; +import org.developerden.codosseum.service.AuthService; + +@Validated +@Controller("/users") +@Secured(SecurityRule.IS_AUTHENTICATED) +public class UserController { + private final AuthRepository authRepository; + private final PlayersMapper playersMapper; + + private final AuthService authService; + private final UserMapper userMapper; + + @Inject + UserController(AuthRepository authRepository, PlayersMapper playersMapper, + AuthService authService, UserMapper userMapper) { + this.authRepository = authRepository; + this.playersMapper = playersMapper; + this.authService = authService; + this.userMapper = userMapper; + } + + @Get("/@self") + public HttpResponse getSelf(Authentication principal) { + return authService.getUserInfoFromAuth(principal) + .map(userMapper::toDto) + .map(HttpResponse::ok) + .orElse(HttpResponse.unauthorized()); + } +} diff --git a/src/main/java/org/developerden/codosseum/controller/binder/GameParam.java b/src/main/java/org/developerden/codosseum/controller/binder/GameParam.java new file mode 100644 index 0000000..b9eea7e --- /dev/null +++ b/src/main/java/org/developerden/codosseum/controller/binder/GameParam.java @@ -0,0 +1,28 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.controller.binder; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface GameParam { + String value() default "id"; // The path variable name containing the UUID +} diff --git a/src/main/java/org/developerden/codosseum/controller/binder/GameParamBinder.java b/src/main/java/org/developerden/codosseum/controller/binder/GameParamBinder.java new file mode 100644 index 0000000..882dd61 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/controller/binder/GameParamBinder.java @@ -0,0 +1,80 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.controller.binder; + +import io.micronaut.core.bind.ArgumentBinder; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder; +import io.micronaut.http.exceptions.HttpStatusException; +import io.micronaut.web.router.UriRouteMatch; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.UUID; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.repository.GameRepository; + +@Singleton +public class GameParamBinder implements TypedRequestArgumentBinder { + private final GameRepository gameRepository; + + public GameParamBinder(GameRepository gameRepository) { + this.gameRepository = gameRepository; + } + + @Override + public Argument argumentType() { + return Argument.of(Game.class); + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + // Check if the parameter has @GameParam annotation + if (!context.getArgument().isAnnotationPresent(GameParam.class)) { + return ArgumentBinder.BindingResult.UNSATISFIED; + } + + // Get the path variable name from the annotation + String pathVariableName = context.getArgument() + .getAnnotation(GameParam.class) + .stringValue() + .orElse("id"); + + // Extract the UUID from path variables using getParameters() + String uuidStr = (String) source.getAttribute("micronaut.http.route.match", UriRouteMatch.class) + .map(m -> m.getVariableValues().get(pathVariableName)) + .filter(x -> x instanceof String).orElse(null); + + + if (uuidStr == null) { + return ArgumentBinder.BindingResult.UNSATISFIED; + } + + try { + UUID gameId = UUID.fromString(uuidStr); + Optional game = gameRepository.findGameById(gameId); + + if (game.isEmpty()) { + throw new HttpStatusException(HttpStatus.NOT_FOUND, "Game not found"); + } + + return () -> game; + } catch (IllegalArgumentException e) { + throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid game ID format"); + } + } +} diff --git a/src/main/java/org/developerden/codosseum/controller/package-info.java b/src/main/java/org/developerden/codosseum/controller/package-info.java index c6f35db..c3a0022 100644 --- a/src/main/java/org/developerden/codosseum/controller/package-info.java +++ b/src/main/java/org/developerden/codosseum/controller/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/dto/ChallengeInfo.java b/src/main/java/org/developerden/codosseum/dto/ChallengeInfo.java index 99be388..4843bfb 100644 --- a/src/main/java/org/developerden/codosseum/dto/ChallengeInfo.java +++ b/src/main/java/org/developerden/codosseum/dto/ChallengeInfo.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; diff --git a/src/main/java/org/developerden/codosseum/dto/GameCreateRequest.java b/src/main/java/org/developerden/codosseum/dto/GameCreateRequest.java index 2352a19..e7eb50d 100644 --- a/src/main/java/org/developerden/codosseum/dto/GameCreateRequest.java +++ b/src/main/java/org/developerden/codosseum/dto/GameCreateRequest.java @@ -1,34 +1,41 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; import io.micronaut.core.annotation.Introspected; -import jakarta.annotation.Nonnull; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; @Introspected +@Serdeable +@Schema( + description = "Settings for the new game" +) public record GameCreateRequest( - @Nonnull @NotNull GameSettings settings, - - @Nonnull @NotNull + @Schema(description = """ + Player info of the person creating the game. + Without special authorisation, this must not be omitted and the provided + player will join the newly created game automatically. + + In the future, there might be a mechanism for technical users to create games without joining them, + where this property is optional. + """) Player player -){ +) { } diff --git a/src/main/java/org/developerden/codosseum/dto/GameCreateResponse.java b/src/main/java/org/developerden/codosseum/dto/GameCreateResponse.java index e738fcf..78fce21 100644 --- a/src/main/java/org/developerden/codosseum/dto/GameCreateResponse.java +++ b/src/main/java/org/developerden/codosseum/dto/GameCreateResponse.java @@ -1,31 +1,34 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; -import jakarta.annotation.Nonnull; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import javax.annotation.Nonnull; @RecordBuilder +@Serdeable public record GameCreateResponse( + @NotNull @Nonnull String adminKey, + @NotNull @Nonnull - String id + UUID id ) { } diff --git a/src/main/java/org/developerden/codosseum/dto/GameInfo.java b/src/main/java/org/developerden/codosseum/dto/GameInfo.java index 3f44638..5d28aa5 100644 --- a/src/main/java/org/developerden/codosseum/dto/GameInfo.java +++ b/src/main/java/org/developerden/codosseum/dto/GameInfo.java @@ -1,36 +1,41 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import java.util.List; +import java.util.UUID; import org.developerden.codosseum.mode.GameMode; -import org.developerden.codosseum.model.GameState; +import org.developerden.codosseum.dto.phase.ApiGamePhase; + @RecordBuilder +@Serdeable +@Schema( + description = "Public information about a game", + title = "GameInfo" +) public record GameInfo( @Nonnull GameSettings settings, @Nonnull - String id, + UUID id, @Nonnull GameMode gameMode, @@ -39,14 +44,7 @@ public record GameInfo( Players players, @Nonnull - GameState state, - - int timeLeft, - - long nextStateAt, - - @Nullable - Integer round, + ApiGamePhase phase, @Nonnull List results diff --git a/src/main/java/org/developerden/codosseum/dto/GameJoinResponse.java b/src/main/java/org/developerden/codosseum/dto/GameJoinResponse.java index 7803054..e16f796 100644 --- a/src/main/java/org/developerden/codosseum/dto/GameJoinResponse.java +++ b/src/main/java/org/developerden/codosseum/dto/GameJoinResponse.java @@ -1,26 +1,25 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; import jakarta.annotation.Nonnull; @RecordBuilder +@Serdeable public record GameJoinResponse( @Nonnull String key diff --git a/src/main/java/org/developerden/codosseum/dto/GameSettings.java b/src/main/java/org/developerden/codosseum/dto/GameSettings.java index 6ed1ce3..aac26e4 100644 --- a/src/main/java/org/developerden/codosseum/dto/GameSettings.java +++ b/src/main/java/org/developerden/codosseum/dto/GameSettings.java @@ -1,54 +1,68 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import java.util.List; -import org.developerden.codosseum.mode.GameMode; +import org.developerden.codosseum.mode.GameModeType; @Introspected @RecordBuilder +@Serdeable +@Schema( + description = "Settings for a Codosseum game" +) public record GameSettings( // custom validator for elements @Nullable + @Schema(description = """ + Which programming languages are allowed to be used for submissions. + If omitted, all languages are allowed.""") List allowedLanguages, @Nullable - GameMode gameMode, + @Schema(description = "Game modes from which the server will make a random selection. " + + "If omitted, all game modes are allowed") + List allowedGameModes, @Nullable @Min(2) @Max(50) + @Schema(description = "Maximum player count for the game") Integer maxPlayers, @Nullable @Min(60) @Max(1800) + @Schema(description = "Time limit for the game, in seconds.") Integer timeLimit, @Nullable @Min(0) @Max(300) + @Schema(description = + """ + Maximum warmup time for a game in seconds - that is, a timer that starts once 2 or more players have joined the game + that delays the starting of the game to allow more players to join.""") Integer maxWarmupTime ) { } diff --git a/src/main/java/org/developerden/codosseum/dto/Player.java b/src/main/java/org/developerden/codosseum/dto/Player.java index ba1a378..9a45d5d 100644 --- a/src/main/java/org/developerden/codosseum/dto/Player.java +++ b/src/main/java/org/developerden/codosseum/dto/Player.java @@ -1,26 +1,33 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; import jakarta.annotation.Nonnull; +/** + * A player in a game. + * + * @param name the chosen name of the player. + */ @RecordBuilder +@Introspected +@Serdeable +// TODO: Store whether the player is an admin public record Player( @Nonnull String name diff --git a/src/main/java/org/developerden/codosseum/dto/PlayerGameResult.java b/src/main/java/org/developerden/codosseum/dto/PlayerGameResult.java index 79badb1..ae7d28f 100644 --- a/src/main/java/org/developerden/codosseum/dto/PlayerGameResult.java +++ b/src/main/java/org/developerden/codosseum/dto/PlayerGameResult.java @@ -1,25 +1,26 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; @RecordBuilder +@Introspected +@Serdeable public record PlayerGameResult( double score ) { diff --git a/src/main/java/org/developerden/codosseum/dto/PlayerRoundResult.java b/src/main/java/org/developerden/codosseum/dto/PlayerRoundResult.java index 0e058db..061ce46 100644 --- a/src/main/java/org/developerden/codosseum/dto/PlayerRoundResult.java +++ b/src/main/java/org/developerden/codosseum/dto/PlayerRoundResult.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; @@ -23,6 +20,7 @@ @RecordBuilder public record PlayerRoundResult( + @Nonnull Player player, @Nonnull String name, diff --git a/src/main/java/org/developerden/codosseum/dto/Players.java b/src/main/java/org/developerden/codosseum/dto/Players.java index 55f21bf..759312a 100644 --- a/src/main/java/org/developerden/codosseum/dto/Players.java +++ b/src/main/java/org/developerden/codosseum/dto/Players.java @@ -1,32 +1,45 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; +import io.micronaut.serde.annotation.Serdeable; import io.soabase.recordbuilder.core.RecordBuilder; import jakarta.annotation.Nonnull; -import java.util.List; +import jakarta.annotation.Nullable; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +@RecordBuilder() +@RecordBuilder.Options( + useImmutableCollections = true, + addSingleItemCollectionBuilders = true +) +@Serdeable -@RecordBuilder public record Players( @Nonnull - List players, + Set players, - @Nonnull - String admin + @Nullable + Player admin ) { + + public Stream allPlayers() { + return Stream.concat(players.stream(), Stream.of(admin).filter(Objects::nonNull)) + .distinct(); + } + } diff --git a/src/main/java/org/developerden/codosseum/dto/PlayersMapper.java b/src/main/java/org/developerden/codosseum/dto/PlayersMapper.java new file mode 100644 index 0000000..a52bbd8 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/PlayersMapper.java @@ -0,0 +1,42 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto; + +import org.developerden.codosseum.model.GamePlayers; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.model.player.GamePlayer; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +@Mapper( + componentModel = "jsr330" +) +public interface PlayersMapper { + + @Mapping(target = "name", source = "player.name") + Player toDto(EphemeralPlayer player); + + @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.COMPILE_ERROR) + @SubclassMapping(target = Player.class, source = EphemeralPlayer.class) + @Mapping(target = "name", ignore = true) // Handled in EphemeralPlayer mapping + Player toDto(GamePlayer player); + + @Mapping(target = "players", source = "others") + Players toDto(GamePlayers gamePlayers); + +} diff --git a/src/main/java/org/developerden/codosseum/dto/Round.java b/src/main/java/org/developerden/codosseum/dto/Round.java index 3c09e62..c64a67a 100644 --- a/src/main/java/org/developerden/codosseum/dto/Round.java +++ b/src/main/java/org/developerden/codosseum/dto/Round.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; diff --git a/src/main/java/org/developerden/codosseum/dto/Submission.java b/src/main/java/org/developerden/codosseum/dto/Submission.java index 4d43b44..cb5c1f4 100644 --- a/src/main/java/org/developerden/codosseum/dto/Submission.java +++ b/src/main/java/org/developerden/codosseum/dto/Submission.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; diff --git a/src/main/java/org/developerden/codosseum/dto/TestResponse.java b/src/main/java/org/developerden/codosseum/dto/TestResponse.java index 4478dbc..b69c483 100644 --- a/src/main/java/org/developerden/codosseum/dto/TestResponse.java +++ b/src/main/java/org/developerden/codosseum/dto/TestResponse.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; diff --git a/src/main/java/org/developerden/codosseum/dto/TestResult.java b/src/main/java/org/developerden/codosseum/dto/TestResult.java index 0658641..4383a55 100644 --- a/src/main/java/org/developerden/codosseum/dto/TestResult.java +++ b/src/main/java/org/developerden/codosseum/dto/TestResult.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.dto; diff --git a/src/main/java/org/developerden/codosseum/dto/package-info.java b/src/main/java/org/developerden/codosseum/dto/package-info.java index 10d0e67..77ce99b 100644 --- a/src/main/java/org/developerden/codosseum/dto/package-info.java +++ b/src/main/java/org/developerden/codosseum/dto/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/dto/phase/ApiGamePhase.java b/src/main/java/org/developerden/codosseum/dto/phase/ApiGamePhase.java new file mode 100644 index 0000000..3e5c972 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/ApiGamePhase.java @@ -0,0 +1,29 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto.phase; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; + +@Serdeable +@Introspected +@Schema(description = "The current phase of the game and any associated data.", + discriminatorProperty = "kind") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind", visible = true) +public sealed interface ApiGamePhase + permits ApiUndefinedPhase, ApiWaitingForPlayersPhase, ApiWarmupPhase, ApiInProgressPhase { +} diff --git a/src/main/java/org/developerden/codosseum/dto/phase/ApiInProgressPhase.java b/src/main/java/org/developerden/codosseum/dto/phase/ApiInProgressPhase.java new file mode 100644 index 0000000..4fb91c5 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/ApiInProgressPhase.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package org.developerden.codosseum.dto.phase; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; +import org.developerden.codosseum.model.phase.GamePhaseKind; + +@Serdeable +@Schema(description = "A round is in progress") +@JsonTypeName("IN_PROGRESS") +public record ApiInProgressPhase(int currentRound) implements ApiGamePhase { + +} diff --git a/src/main/java/org/developerden/codosseum/dto/phase/ApiUndefinedPhase.java b/src/main/java/org/developerden/codosseum/dto/phase/ApiUndefinedPhase.java new file mode 100644 index 0000000..44b63ac --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/ApiUndefinedPhase.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package org.developerden.codosseum.dto.phase; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; +import org.developerden.codosseum.model.phase.GamePhaseKind; + +@Serdeable +@Schema(description = "Undefined phase (error state; should rarely be seen)") +@JsonTypeName("UNDEFINED") +public record ApiUndefinedPhase() implements ApiGamePhase { + +} diff --git a/src/main/java/org/developerden/codosseum/dto/phase/ApiWaitingForPlayersPhase.java b/src/main/java/org/developerden/codosseum/dto/phase/ApiWaitingForPlayersPhase.java new file mode 100644 index 0000000..c27bb6f --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/ApiWaitingForPlayersPhase.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto.phase; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; + +@Serdeable +@Schema(description = "Waiting for players to join before the game can begin") +@JsonTypeName("WAITING_FOR_PLAYERS") +public record ApiWaitingForPlayersPhase() implements ApiGamePhase { + +} diff --git a/src/main/java/org/developerden/codosseum/dto/phase/ApiWarmupPhase.java b/src/main/java/org/developerden/codosseum/dto/phase/ApiWarmupPhase.java new file mode 100644 index 0000000..85d12d8 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/ApiWarmupPhase.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto.phase; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.micronaut.serde.annotation.Serdeable; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; + +@Serdeable +@Schema(description = "Warmup countdown before the game starts") +@JsonTypeName("WARMUP") +public record ApiWarmupPhase(Instant warmupEndsAt) implements ApiGamePhase { +} diff --git a/src/main/java/org/developerden/codosseum/dto/phase/PhaseMapper.java b/src/main/java/org/developerden/codosseum/dto/phase/PhaseMapper.java new file mode 100644 index 0000000..7555cfb --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/phase/PhaseMapper.java @@ -0,0 +1,51 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto.phase; + +import jakarta.inject.Singleton; +import org.developerden.codosseum.model.phase.GamePhase; +import org.developerden.codosseum.model.phase.InProgressPhase; +import org.developerden.codosseum.model.phase.UndefinedPhase; +import org.developerden.codosseum.model.phase.WaitingForPlayersPhase; +import org.developerden.codosseum.model.phase.WarmupPhase; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +/** + * MapStruct Mapper for converting game phases to their API DTO representations. + */ +@Mapper( + componentModel = "jsr330" +) +public interface PhaseMapper { + + ApiUndefinedPhase toDto(UndefinedPhase gamePhase); + + ApiWaitingForPlayersPhase toDto(WaitingForPlayersPhase phase); + + ApiWarmupPhase toDto(WarmupPhase phase); + + ApiInProgressPhase toDto(InProgressPhase phase); + + @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.COMPILE_ERROR) + @SubclassMapping(source = InProgressPhase.class, target = ApiInProgressPhase.class) + @SubclassMapping(source = UndefinedPhase.class, target = ApiUndefinedPhase.class) + @SubclassMapping(source = WaitingForPlayersPhase.class, target = ApiWaitingForPlayersPhase.class) + @SubclassMapping(source = WarmupPhase.class, target = ApiWarmupPhase.class) + ApiGamePhase toDto(GamePhase phase); + +} diff --git a/src/main/java/org/developerden/codosseum/dto/user/User.java b/src/main/java/org/developerden/codosseum/dto/user/User.java new file mode 100644 index 0000000..065d77e --- /dev/null +++ b/src/main/java/org/developerden/codosseum/dto/user/User.java @@ -0,0 +1,41 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.dto.user; + +import io.micronaut.serde.annotation.Serdeable; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.UUID; +import org.developerden.codosseum.dto.Player; +import org.developerden.codosseum.model.player.CodosseumUser; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.model.player.RegisteredUser; + +/** + * DTO for a user, with minimal game-specific information. + * + * @param id the unique identifier of the user, or null if the user is not a {@link RegisteredUser}. + * @param name the display name of the user. + * @param gameId the unique identifier of the game the user is registered in, if the user is a {@link EphemeralPlayer}, null otherwise + * @see CodosseumUser the internal representation of this type + * @see Player the game-specific DTO representation of a user + */ +@Serdeable +public record User( + @Nullable UUID id, + @Nonnull String name, + @Nullable UUID gameId +) { +} diff --git a/src/main/java/org/developerden/codosseum/event/EliminatedEvent.java b/src/main/java/org/developerden/codosseum/event/EliminatedEvent.java index 89afafb..79b0e8e 100644 --- a/src/main/java/org/developerden/codosseum/event/EliminatedEvent.java +++ b/src/main/java/org/developerden/codosseum/event/EliminatedEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/GameEvent.java b/src/main/java/org/developerden/codosseum/event/GameEvent.java index 612aa87..2ad6a3a 100644 --- a/src/main/java/org/developerden/codosseum/event/GameEvent.java +++ b/src/main/java/org/developerden/codosseum/event/GameEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/GameOverEvent.java b/src/main/java/org/developerden/codosseum/event/GameOverEvent.java index 12ebbda..297a201 100644 --- a/src/main/java/org/developerden/codosseum/event/GameOverEvent.java +++ b/src/main/java/org/developerden/codosseum/event/GameOverEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/PlayerCodeRevealEvent.java b/src/main/java/org/developerden/codosseum/event/PlayerCodeRevealEvent.java index 200bc6d..efcd419 100644 --- a/src/main/java/org/developerden/codosseum/event/PlayerCodeRevealEvent.java +++ b/src/main/java/org/developerden/codosseum/event/PlayerCodeRevealEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/PlayerJoinEvent.java b/src/main/java/org/developerden/codosseum/event/PlayerJoinEvent.java new file mode 100644 index 0000000..57b8cb7 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/event/PlayerJoinEvent.java @@ -0,0 +1,21 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.event; + +import java.util.UUID; +import org.developerden.codosseum.dto.Player; + +public record PlayerJoinEvent(UUID gameId, Player player) implements GameEvent { +} diff --git a/src/main/java/org/developerden/codosseum/event/PlayerSubmitEvent.java b/src/main/java/org/developerden/codosseum/event/PlayerSubmitEvent.java index e8aeb44..ed157ab 100644 --- a/src/main/java/org/developerden/codosseum/event/PlayerSubmitEvent.java +++ b/src/main/java/org/developerden/codosseum/event/PlayerSubmitEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/RoundStartEvent.java b/src/main/java/org/developerden/codosseum/event/RoundStartEvent.java index 196bd73..c33b56c 100644 --- a/src/main/java/org/developerden/codosseum/event/RoundStartEvent.java +++ b/src/main/java/org/developerden/codosseum/event/RoundStartEvent.java @@ -1,31 +1,31 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; import io.soabase.recordbuilder.core.RecordBuilder; import jakarta.annotation.Nonnull; -import org.developerden.codosseum.dto.ChallengeInfo; +import java.time.Duration; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; @RecordBuilder public record RoundStartEvent( @Nonnull ChallengeInfo challenge, - int round + int round, + + Duration roundLength ) implements GameEvent { } diff --git a/src/main/java/org/developerden/codosseum/event/SyncEvent.java b/src/main/java/org/developerden/codosseum/event/SyncEvent.java index caa071f..1df63b4 100644 --- a/src/main/java/org/developerden/codosseum/event/SyncEvent.java +++ b/src/main/java/org/developerden/codosseum/event/SyncEvent.java @@ -1,30 +1,27 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; import io.soabase.recordbuilder.core.RecordBuilder; import jakarta.annotation.Nonnull; -import org.developerden.codosseum.model.GameState; +import org.developerden.codosseum.model.GamePhase; @RecordBuilder public record SyncEvent( @Nonnull - GameState state, + GamePhase state, int timeLeft ) implements GameEvent { diff --git a/src/main/java/org/developerden/codosseum/event/TestResultEvent.java b/src/main/java/org/developerden/codosseum/event/TestResultEvent.java index 08f9447..a5e87fe 100644 --- a/src/main/java/org/developerden/codosseum/event/TestResultEvent.java +++ b/src/main/java/org/developerden/codosseum/event/TestResultEvent.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.event; diff --git a/src/main/java/org/developerden/codosseum/event/package-info.java b/src/main/java/org/developerden/codosseum/event/package-info.java index 439403a..bf84e65 100644 --- a/src/main/java/org/developerden/codosseum/event/package-info.java +++ b/src/main/java/org/developerden/codosseum/event/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/exception/GameNotFoundException.java b/src/main/java/org/developerden/codosseum/exception/GameNotFoundException.java new file mode 100644 index 0000000..0907481 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/exception/GameNotFoundException.java @@ -0,0 +1,22 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.exception; + +public class GameNotFoundException extends RuntimeException { + + public GameNotFoundException(String gameId) { + super("No game with ID '" + gameId + "' found"); + } +} diff --git a/src/main/java/org/developerden/codosseum/exception/IllegalGameStateException.java b/src/main/java/org/developerden/codosseum/exception/IllegalGameStateException.java new file mode 100644 index 0000000..fe6f715 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/exception/IllegalGameStateException.java @@ -0,0 +1,29 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.exception; + + +import java.util.Set; +import java.util.stream.Collectors; +import org.developerden.codosseum.model.GamePhase; + +public class IllegalGameStateException extends RuntimeException { + + public IllegalGameStateException(String gameId, GamePhase state, Set expected) { + super("Game '" + gameId + "' is in state '" + state + "' but this action can only be " + + "taken while in one of the following states: " + + expected.stream().map(GamePhase::name).collect(Collectors.joining(", "))); + } +} diff --git a/src/main/java/org/developerden/codosseum/execution/package-info.java b/src/main/java/org/developerden/codosseum/execution/package-info.java index 1774f20..26c4fb1 100644 --- a/src/main/java/org/developerden/codosseum/execution/package-info.java +++ b/src/main/java/org/developerden/codosseum/execution/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/mode/FastestGameMode.java b/src/main/java/org/developerden/codosseum/mode/FastestGameMode.java new file mode 100644 index 0000000..786cba2 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/mode/FastestGameMode.java @@ -0,0 +1,44 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.mode; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import org.developerden.codosseum.dto.Player; +import org.developerden.codosseum.dto.PlayerRoundResult; + +public class FastestGameMode implements GameMode { + @Override + public GameModeType getType() { + return GameModeType.FASTEST; + } + + @Override + public double computeScore(Player player, List results) { + var orderedScores = results.stream() + .sorted(Comparator.comparing(PlayerRoundResult::timeLeft).reversed()) + .toList(); + + // find the result of the player + Optional any = results.stream() + .filter(res -> res.player().equals(player)) + .findAny(); + + // warn? + return any.map(playerRoundResult -> orderedScores.indexOf(playerRoundResult) + 1.0) + .orElse(0.0); + } +} diff --git a/src/main/java/org/developerden/codosseum/mode/GameMode.java b/src/main/java/org/developerden/codosseum/mode/GameMode.java index 09ce624..0f5f68b 100644 --- a/src/main/java/org/developerden/codosseum/mode/GameMode.java +++ b/src/main/java/org/developerden/codosseum/mode/GameMode.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.mode; @@ -20,12 +17,20 @@ import java.util.List; import org.developerden.codosseum.dto.Player; import org.developerden.codosseum.dto.PlayerRoundResult; -import org.developerden.codosseum.model.Game; // should have a serialiser/deserialiser using the game mode name and the available // implementations on the classpath public interface GameMode { + GameModeType getType(); + /** + * Compute the score for the given player and results. + * The results are all the player's results for the current round. + * + * @param player the player to compute the score for + * @param results all player's results for the current round + * @return a score between 0 and 100 (inclusive) + */ double computeScore(Player player, List results); } diff --git a/src/main/java/org/developerden/codosseum/mode/GameModeFactory.java b/src/main/java/org/developerden/codosseum/mode/GameModeFactory.java index 044649d..f999e89 100644 --- a/src/main/java/org/developerden/codosseum/mode/GameModeFactory.java +++ b/src/main/java/org/developerden/codosseum/mode/GameModeFactory.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.mode; @@ -21,4 +18,6 @@ public interface GameModeFactory { GameMode initForGame(String gameId); + GameMode fromType(GameModeType type); + } diff --git a/src/main/java/org/developerden/codosseum/mode/GameModeFactoryImpl.java b/src/main/java/org/developerden/codosseum/mode/GameModeFactoryImpl.java new file mode 100644 index 0000000..90a0d89 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/mode/GameModeFactoryImpl.java @@ -0,0 +1,33 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.mode; + +import jakarta.inject.Singleton; + +@Singleton +public class GameModeFactoryImpl implements GameModeFactory { + @Override + public GameMode initForGame(String gameId) { + return null; + } + + @Override + public GameMode fromType(GameModeType type) { + return switch (type) { + case FASTEST -> new FastestGameMode(); + default -> throw new UnsupportedOperationException("Unsupported game mode type: " + type); + }; + } +} diff --git a/src/main/java/org/developerden/codosseum/mode/GameModeSerializer.java b/src/main/java/org/developerden/codosseum/mode/GameModeSerializer.java new file mode 100644 index 0000000..08b2e3d --- /dev/null +++ b/src/main/java/org/developerden/codosseum/mode/GameModeSerializer.java @@ -0,0 +1,48 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.mode; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Encoder; +import io.micronaut.serde.Serde; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.io.IOException; + +@Singleton +public class GameModeSerializer implements Serde { + private final @Inject GameModeFactory gameModeFactory; + + public GameModeSerializer(GameModeFactory gameModeFactory) { + this.gameModeFactory = gameModeFactory; + } + + @Override + public @Nullable GameMode deserialize(@NonNull Decoder decoder, @NonNull DecoderContext context, + @NonNull Argument type) + throws IOException { + return gameModeFactory.fromType(GameModeType.valueOf(decoder.decodeString().toUpperCase())); + } + + @Override + public void serialize(@NonNull Encoder encoder, @NonNull EncoderContext context, + @NonNull Argument type, @NonNull GameMode value) + throws IOException { + encoder.encodeString(value.getType().name().toLowerCase()); + } +} diff --git a/src/main/java/org/developerden/codosseum/mode/GameModeType.java b/src/main/java/org/developerden/codosseum/mode/GameModeType.java new file mode 100644 index 0000000..0c8d791 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/mode/GameModeType.java @@ -0,0 +1,21 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.mode; + +public enum GameModeType { + FASTEST, + REVERSE, + GOLF +} diff --git a/src/main/java/org/developerden/codosseum/mode/package-info.java b/src/main/java/org/developerden/codosseum/mode/package-info.java index 42c0513..a70cdbf 100644 --- a/src/main/java/org/developerden/codosseum/mode/package-info.java +++ b/src/main/java/org/developerden/codosseum/mode/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/model/Game.java b/src/main/java/org/developerden/codosseum/model/Game.java index 6be1be5..b0ee00b 100644 --- a/src/main/java/org/developerden/codosseum/model/Game.java +++ b/src/main/java/org/developerden/codosseum/model/Game.java @@ -1,31 +1,36 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.model; import io.soabase.recordbuilder.core.RecordBuilder; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import java.util.List; +import java.util.UUID; +import org.developerden.codosseum.dto.GameSettings; import org.developerden.codosseum.mode.GameMode; -// TODO: 19/08/23 internal model +/** + * Internal model representing the immutable only data about a game. + * All mutable data is held in {@link GameState}. + * + * @param id the unique identifier of the game. + * @param settings the settings for the game. + * @param mode the game mode. + */ @RecordBuilder public record Game( - + UUID id, + GameSettings settings, + GameMode mode ) { } diff --git a/src/main/java/org/developerden/codosseum/model/GamePhase.java b/src/main/java/org/developerden/codosseum/model/GamePhase.java new file mode 100644 index 0000000..c72016c --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/GamePhase.java @@ -0,0 +1,35 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +public enum GamePhase { + @Schema(description = """ + The game phase is not defined, usually indicating an error state. + Clients should generally not have to handle this state.""") + UNDEFINED, + @Schema(description = "The game has not yet started and is waiting for more players before it can begin") + WAITING_FOR_PLAYERS, + @Schema(description = "The game is in a warmup phase and is ready to begin") + WARMUP, + @Schema(description = "The game is currently in progress") + IN_PROGRESS, + @Schema(description = "The current round of the game is over") + ROUND_OVER, + @Schema(description = "The game has ended") + GAME_OVER + +} diff --git a/src/main/java/org/developerden/codosseum/model/GamePlayers.java b/src/main/java/org/developerden/codosseum/model/GamePlayers.java new file mode 100644 index 0000000..7d2134d --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/GamePlayers.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model; + +import io.micronaut.serde.annotation.Serdeable; +import io.soabase.recordbuilder.core.RecordBuilder; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.developerden.codosseum.model.player.GamePlayer; + +/** + * Internal model representing the players in a game. + * + * @param admin the admin player + * @param others the other players. For future proofing in the case of multi-admin games, this will also include the {@link #admin} + */ +@RecordBuilder() +@RecordBuilder.Options( + useImmutableCollections = true, + addSingleItemCollectionBuilders = true +) +public record GamePlayers(@Nullable GamePlayer admin, @Nonnull Set others) { + + public GamePlayersBuilder builder() { + return GamePlayersBuilder.builder(this); + } +} diff --git a/src/main/java/org/developerden/codosseum/model/GameState.java b/src/main/java/org/developerden/codosseum/model/GameState.java index b1ea89b..c27da22 100644 --- a/src/main/java/org/developerden/codosseum/model/GameState.java +++ b/src/main/java/org/developerden/codosseum/model/GameState.java @@ -1,27 +1,63 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.model; -public enum GameState { +import io.soabase.recordbuilder.core.RecordBuilder; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Positive; +import java.util.UUID; +import java.util.function.UnaryOperator; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.model.phase.GamePhase; +import org.developerden.codosseum.model.phase.WithPlayersPhase; + +/** + * Internal model representing the current mutable state of a game. + * + * @param gameId the unique identifier of the game + * @param phase the current phase of the game + * @param currentChallengeInfo the current challenge information, if a challenge is active + * @param currentRound the current round number of the game + */ +@RecordBuilder +@RecordBuilder.Options(defaultNotNull = true) +public record GameState(@Nonnull UUID gameId, + @Nonnull GamePhase phase, + @Nullable ChallengeInfo currentChallengeInfo, + boolean acceptingSolutions, + @Nullable @Positive Integer currentRound +) implements GameStateBuilder.With { - WARMUP, - IN_PROGRESS, - ROUND_OVER, - GAME_OVER + public GameState updatePlayers(UnaryOperator mutate) { + var phase = phase(); + if (phase instanceof WithPlayersPhase wp) { + var newPlayers = mutate.apply(wp.players()); + var newPhase = wp.withPlayers(newPlayers); + return GameStateBuilder.from(this).withPhase(newPhase); + } + throw new IllegalStateException( + "Phase does not contain players: " + phase.getClass().getSimpleName()); + } + public GamePlayers players() { + var phase = phase(); + if (phase instanceof WithPlayersPhase wp) { + return wp.players(); + } + throw new IllegalStateException( + "Phase does not contain players: " + phase.getClass().getSimpleName()); + } } diff --git a/src/main/java/org/developerden/codosseum/model/package-info.java b/src/main/java/org/developerden/codosseum/model/package-info.java index 65296e4..677f599 100644 --- a/src/main/java/org/developerden/codosseum/model/package-info.java +++ b/src/main/java/org/developerden/codosseum/model/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/model/phase/GamePhase.java b/src/main/java/org/developerden/codosseum/model/phase/GamePhase.java new file mode 100644 index 0000000..2fa1440 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/GamePhase.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.annotation.Nonnull; +import org.developerden.codosseum.dto.phase.ApiGamePhase; +import org.developerden.codosseum.model.GameState; + +/** + * Internal model of a phase of the game. + * The phase is the state of a game that is specific to the current part of the game lifecycle. + * For example, the state needed while waiting for players to join is different to the state needed while the game is in progress. + * + * @see GamePhaseKind for the different kinds of phases. + * @see GameState for state across the entire game lifecycle. + * @see ApiGamePhase for the DTO representation of this interface. + */ +@Schema(description = "A phase of the game.") +public sealed interface GamePhase + permits InProgressPhase, UndefinedPhase, WaitingForPlayersPhase, WarmupPhase, WithPlayersPhase { + /** + * The kind of phase this is. Serves as a simple discriminator for serialization or for checking the type of phase. + * + * @return the kind of phase, which should generally be unique across implementations of this interface. + */ + @Nonnull GamePhaseKind getKind(); +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/GamePhaseKind.java b/src/main/java/org/developerden/codosseum/model/phase/GamePhaseKind.java new file mode 100644 index 0000000..c7e27a2 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/GamePhaseKind.java @@ -0,0 +1,34 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.swagger.v3.oas.annotations.media.Schema; + +public enum GamePhaseKind { + @Schema(description = """ + The game phase is not defined, usually indicating an error state. + Clients should generally not have to handle this state.""") + UNDEFINED, + @Schema(description = "The game has not yet started and is waiting for more players before it can begin") + WAITING_FOR_PLAYERS, + @Schema(description = "The game is in a warmup phase and is ready to begin") + WARMUP, + @Schema(description = "The game is currently in progress") + IN_PROGRESS, + @Schema(description = "The current round of the game is over") + ROUND_OVER, + @Schema(description = "The game has ended") + GAME_OVER +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/InProgressPhase.java b/src/main/java/org/developerden/codosseum/model/phase/InProgressPhase.java new file mode 100644 index 0000000..4f28645 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/InProgressPhase.java @@ -0,0 +1,49 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.micronaut.serde.annotation.Serdeable; +import io.soabase.recordbuilder.core.RecordBuilder; +import jakarta.validation.constraints.Positive; +import javax.annotation.Nonnull; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.model.GamePlayers; + +/** + * The game is in progress. + * This phase includes information about the current challenge and round + * + * @param players the players in the game + * @param currentChallenge the current challenge + * @param currentRound the current round number (1-based) + */ +@Serdeable +@RecordBuilder +public record InProgressPhase(GamePlayers players, + ChallengeInfo currentChallenge, + @Positive int currentRound) + implements GamePhase, WithPlayersPhase { + + @Nonnull + @Override + public GamePhaseKind getKind() { + return GamePhaseKind.IN_PROGRESS; + } + + @Override + public WithPlayersPhase withPlayers(GamePlayers players) { + return new InProgressPhase(players, currentChallenge, currentRound); + } +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/UndefinedPhase.java b/src/main/java/org/developerden/codosseum/model/phase/UndefinedPhase.java new file mode 100644 index 0000000..8fcd0a3 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/UndefinedPhase.java @@ -0,0 +1,27 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.micronaut.serde.annotation.Serdeable; +import javax.annotation.Nonnull; + +@Serdeable +public record UndefinedPhase() implements GamePhase { + @Nonnull + @Override + public GamePhaseKind getKind() { + return GamePhaseKind.UNDEFINED; + } +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/WaitingForPlayersPhase.java b/src/main/java/org/developerden/codosseum/model/phase/WaitingForPlayersPhase.java new file mode 100644 index 0000000..5377e66 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/WaitingForPlayersPhase.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.NotNull; +import javax.annotation.Nonnull; +import org.developerden.codosseum.model.GamePlayers; + +/** + * Phase representing waiting for players to join, i.e. a "lobby" + * + * @param players the players in the game so far + */ +@Serdeable +public record WaitingForPlayersPhase(@NotNull GamePlayers players) + implements GamePhase, WithPlayersPhase { + @Nonnull + @Override + public GamePhaseKind getKind() { + return GamePhaseKind.WAITING_FOR_PLAYERS; + } + + @Override + public WithPlayersPhase withPlayers(GamePlayers players) { + return new WaitingForPlayersPhase(players); + } +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/WarmupPhase.java b/src/main/java/org/developerden/codosseum/model/phase/WarmupPhase.java new file mode 100644 index 0000000..6e301d6 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/WarmupPhase.java @@ -0,0 +1,35 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.micronaut.serde.annotation.Serdeable; +import java.time.Instant; +import javax.annotation.Nonnull; +import org.developerden.codosseum.model.GamePlayers; + +@Serdeable +public record WarmupPhase(GamePlayers players, Instant warmupEndsAt) + implements GamePhase, WithPlayersPhase { + @Nonnull + @Override + public GamePhaseKind getKind() { + return GamePhaseKind.WARMUP; + } + + @Override + public WithPlayersPhase withPlayers(GamePlayers players) { + return new WarmupPhase(players, warmupEndsAt); + } +} diff --git a/src/main/java/org/developerden/codosseum/model/phase/WithPlayersPhase.java b/src/main/java/org/developerden/codosseum/model/phase/WithPlayersPhase.java new file mode 100644 index 0000000..e74b0b4 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/phase/WithPlayersPhase.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.phase; + +import io.micronaut.serde.annotation.Serdeable; +import org.developerden.codosseum.model.GamePlayers; + +@Serdeable +public sealed interface WithPlayersPhase extends GamePhase + permits InProgressPhase, WaitingForPlayersPhase, WarmupPhase { + GamePlayers players(); + + WithPlayersPhase withPlayers(GamePlayers players); +} diff --git a/src/main/java/org/developerden/codosseum/model/player/CodosseumUser.java b/src/main/java/org/developerden/codosseum/model/player/CodosseumUser.java new file mode 100644 index 0000000..6621841 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/CodosseumUser.java @@ -0,0 +1,24 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +/** + * Internal representation of a user, as in, someone with some information registered on the system. + * This can be either a {@link RegisteredUser}, who has an account, or an {@link EphemeralPlayer}, who does not. + * + * @apiNote This is distinct from a {@link GamePlayer}, which is a user in the context of a specific game. + */ +public sealed interface CodosseumUser permits EphemeralPlayer, RegisteredUser { +} diff --git a/src/main/java/org/developerden/codosseum/model/player/EphemeralPlayer.java b/src/main/java/org/developerden/codosseum/model/player/EphemeralPlayer.java new file mode 100644 index 0000000..6f691e4 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/EphemeralPlayer.java @@ -0,0 +1,35 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +import java.util.UUID; +import javax.annotation.Nonnull; + + +/** + * An ephemeral player, not tied to any persistent identity. + * + * @param name the name of the player, which must be unique within a game. + * @param gameId the ID of the game the player is in. + * @param key a unique key for the player, used to identify them in the game. + * @param admin whether the player is an admin - if this is true, the {@link #key} can be also used to authenticate admin actions. + */ +public record EphemeralPlayer(@Nonnull String name, + @Nonnull UUID gameId, + @Nonnull String key, + boolean admin) + implements GamePlayer, CodosseumUser { + +} diff --git a/src/main/java/org/developerden/codosseum/model/player/GamePlayer.java b/src/main/java/org/developerden/codosseum/model/player/GamePlayer.java new file mode 100644 index 0000000..f44a646 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/GamePlayer.java @@ -0,0 +1,19 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +public sealed interface GamePlayer permits EphemeralPlayer, RegisteredPlayer { + String name(); +} diff --git a/src/main/java/org/developerden/codosseum/model/player/RegisteredPlayer.java b/src/main/java/org/developerden/codosseum/model/player/RegisteredPlayer.java new file mode 100644 index 0000000..8a6be18 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/RegisteredPlayer.java @@ -0,0 +1,36 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +import java.util.Set; +import java.util.UUID; +import org.developerden.codosseum.auth.GameRole; +import org.developerden.codosseum.model.Game; + +/** + * Membership of a {@link RegisteredUser} in a specific game. + * + * @param userId the unique identifier of the {@link RegisteredUser} + * @param gameId the unique identifier of the {@link Game} + * @param name the display name of the player in the game + * @param roles the roles assigned to the player in the game + */ +public record RegisteredPlayer( + UUID userId, + UUID gameId, + String name, + Set roles +) implements GamePlayer { +} diff --git a/src/main/java/org/developerden/codosseum/model/player/RegisteredUser.java b/src/main/java/org/developerden/codosseum/model/player/RegisteredUser.java new file mode 100644 index 0000000..66bbe2f --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/RegisteredUser.java @@ -0,0 +1,31 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +import java.util.UUID; + +/** + * Registered user linked to an OAuth/OIDC identity. + * + * @param id the unique identifier of the user. + * @param provider the identity provider (e.g., "google", "github"). + * @param subject the subject identifier from the identity provider. + * @param displayName the display name of the user. + */ +public record RegisteredUser(UUID id, + String provider, + String subject, + String displayName) implements CodosseumUser { +} diff --git a/src/main/java/org/developerden/codosseum/model/player/UserMapper.java b/src/main/java/org/developerden/codosseum/model/player/UserMapper.java new file mode 100644 index 0000000..6c27a63 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/model/player/UserMapper.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.model.player; + +import org.developerden.codosseum.dto.user.User; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; + +@Mapper(componentModel = "jsr330") +public interface UserMapper { + @Mapping(source = "gameId", target = "gameId") + @Mapping(source = "name", target = "name") + @Mapping(target = "id", ignore = true) + User toDto(EphemeralPlayer ephemeralPlayer); + + @Mapping(source = "id", target = "id") + @Mapping(source = "displayName", target = "name") + @Mapping(target = "gameId", ignore = true) + User toDto(RegisteredUser registeredUser); + + @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.COMPILE_ERROR) + @SubclassMapping(source = EphemeralPlayer.class, target = User.class) + @SubclassMapping(source = RegisteredUser.class, target = User.class) + User toDto(CodosseumUser user); +} diff --git a/src/main/java/org/developerden/codosseum/repository/AuthRepository.java b/src/main/java/org/developerden/codosseum/repository/AuthRepository.java index cb0853f..76364c4 100644 --- a/src/main/java/org/developerden/codosseum/repository/AuthRepository.java +++ b/src/main/java/org/developerden/codosseum/repository/AuthRepository.java @@ -1,26 +1,75 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.repository; -import org.developerden.codosseum.model.EphemeralPlayer; +import java.util.Optional; +import java.util.UUID; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.model.player.GamePlayer; +import org.developerden.codosseum.model.player.RegisteredUser; +/** + * Repository for storing and retrieving authentication-related data. + * + *

There are 2 methods of authentication supported by Codosseum: + *

    + *
  • Anonymous: When creating or joining a game, a player is assigned a name of their choosing, and a unique game key, which is used to authenticate them for the duration of the game. There is no long term persistence
  • + *
  • Registered: A user signs up by providing email and password, or uses OIDC (e.g. to sign up with GitHub). This creates a long term user account, which can be used to create and join games. Users authenticated through this method can be in multiple games at once with the same key.
  • + *
+ * + *

This type provides state management for both types of authentication. + */ public interface AuthRepository { - EphemeralPlayer findPlayerByGameKey(String gameKey); + /** + * Find a player by its game/admin key. + * + * @param gameKey The game/admin key. + * @return The player, or null if not found. + */ + Optional findPlayerByGameKey(String gameKey); + + + /** + * Get all players in the repository. + * + * @return all players. + */ + Iterable allPlayers(); + + /** + * Save a player to the repository. + * + * @param game the game the player is in. + * @param player the player to save. + */ + void save(Game game, EphemeralPlayer player); + + /** + * Find a player by its name and game id. + * + * @param name the name of the player. + * @param id the id of the game. + * @return a player in the given game going by the given name, or empty if not found. + */ + Optional findPlayerByNameAndGameId(String name, UUID id); + + + Optional findRegisteredUserById(UUID id); + + void saveRegisteredUser(RegisteredUser user); } diff --git a/src/main/java/org/developerden/codosseum/repository/AuthRepositoryImpl.java b/src/main/java/org/developerden/codosseum/repository/AuthRepositoryImpl.java new file mode 100644 index 0000000..abd7ffb --- /dev/null +++ b/src/main/java/org/developerden/codosseum/repository/AuthRepositoryImpl.java @@ -0,0 +1,82 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.repository; + +import jakarta.inject.Singleton; +import java.util.HashSet; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.model.player.GamePlayer; +import org.developerden.codosseum.model.player.RegisteredUser; + +@Singleton +public class AuthRepositoryImpl implements AuthRepository { + + private final ConcurrentMap players = new ConcurrentHashMap<>(); + private final ConcurrentMap registeredUsers = new ConcurrentHashMap<>(); + + @Override + public Optional findPlayerByGameKey(String gameKey) { + return players.values().stream() + .filter(player -> player.key().equals(gameKey)) + .findFirst(); // TODO: not very efficient + } + + @Override + public Iterable allPlayers() { + return new HashSet<>(players.values()); + } + + @Override + public void save(Game game, EphemeralPlayer player) { + players.put(new PlayerKey(game.id(), player.name()), player); + } + + @Override + public Optional findPlayerByNameAndGameId(String name, UUID id) { + return Optional.ofNullable(players.get(new PlayerKey(id, name))); + } + + @Override + public Optional findRegisteredUserById(UUID id) { + return Optional.ofNullable(registeredUsers.get(id)); + } + + @Override + public void saveRegisteredUser(RegisteredUser user) { + registeredUsers.put(user.id(), user); + } + + + public Optional findOrCreateRegisteredUser(String provider, String subject, + String displayName) { + return registeredUsers.values().stream() + .filter(user -> user.provider().equals(provider) && user.subject().equals(subject)) + .findFirst() + .or(() -> { + RegisteredUser newUser = + new RegisteredUser(UUID.randomUUID(), provider, subject, displayName); + registeredUsers.put(newUser.id(), newUser); + return Optional.of(newUser); + }); + } + + private record PlayerKey(UUID gameId, String playerName) { + } +} diff --git a/src/main/java/org/developerden/codosseum/repository/GameRepository.java b/src/main/java/org/developerden/codosseum/repository/GameRepository.java index ee99552..a7f5f43 100644 --- a/src/main/java/org/developerden/codosseum/repository/GameRepository.java +++ b/src/main/java/org/developerden/codosseum/repository/GameRepository.java @@ -1,27 +1,27 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.repository; +import java.util.Optional; +import java.util.UUID; import org.developerden.codosseum.model.Game; public interface GameRepository { - Game findGameById(String id); + Optional findGameById(UUID id); + void insertGame(Game game); diff --git a/src/main/java/org/developerden/codosseum/repository/InMemoryGameRepository.java b/src/main/java/org/developerden/codosseum/repository/InMemoryGameRepository.java new file mode 100644 index 0000000..c99af3a --- /dev/null +++ b/src/main/java/org/developerden/codosseum/repository/InMemoryGameRepository.java @@ -0,0 +1,37 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.repository; + +import jakarta.inject.Singleton; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.developerden.codosseum.model.Game; + +@Singleton +public class InMemoryGameRepository implements GameRepository { + private final Map games = new ConcurrentHashMap<>(); + + @Override + public Optional findGameById(UUID id) { + return Optional.ofNullable(games.get(id)); + } + + @Override + public void insertGame(Game game) { + games.put(game.id(), game); + } +} diff --git a/src/main/java/org/developerden/codosseum/repository/package-info.java b/src/main/java/org/developerden/codosseum/repository/package-info.java index 4662569..1d66409 100644 --- a/src/main/java/org/developerden/codosseum/repository/package-info.java +++ b/src/main/java/org/developerden/codosseum/repository/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/service/AuthService.java b/src/main/java/org/developerden/codosseum/service/AuthService.java new file mode 100644 index 0000000..5797597 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/AuthService.java @@ -0,0 +1,62 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service; + +import io.micronaut.security.authentication.Authentication; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.UUID; +import org.developerden.codosseum.auth.PlayerAuthentication; +import org.developerden.codosseum.model.player.CodosseumUser; +import org.developerden.codosseum.model.player.RegisteredUser; +import org.developerden.codosseum.repository.AuthRepository; + +@Singleton +public class AuthService { + private final AuthRepository authRepository; + + public AuthService(AuthRepository authRepository) { + this.authRepository = authRepository; + } + + public Optional getUserInfoFromAuth(Authentication authentication) { + if (authentication.getAttributes().containsKey(PlayerAuthentication.ACTIVE_GAME_ID)) { + // it's a GameKey auth + return authRepository.findPlayerByNameAndGameId( + authentication.getName(), + (UUID) authentication.getAttributes().get(PlayerAuthentication.ACTIVE_GAME_ID)) + .map(x -> x); // lol + } + // otherwise assume it's an OAuth user + UUID uuid; + try { + uuid = UUID.fromString(authentication.getName()); + } catch (Exception e) { + return Optional.empty(); + } + return authRepository.findRegisteredUserById(uuid) + .or(() -> { + var user = new RegisteredUser( + uuid, + (String) authentication.getAttributes().get("iss"), + (String) authentication.getAttributes().get("sub"), + (String) authentication.getAttributes().get("name") + ); + authRepository.saveRegisteredUser(user); + return Optional.of(user); + }) + .map(x -> (CodosseumUser) x); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/EventService.java b/src/main/java/org/developerden/codosseum/service/EventService.java index f6da885..d328cac 100644 --- a/src/main/java/org/developerden/codosseum/service/EventService.java +++ b/src/main/java/org/developerden/codosseum/service/EventService.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.service; diff --git a/src/main/java/org/developerden/codosseum/service/GameService.java b/src/main/java/org/developerden/codosseum/service/GameService.java index e2f3471..dfac0a4 100644 --- a/src/main/java/org/developerden/codosseum/service/GameService.java +++ b/src/main/java/org/developerden/codosseum/service/GameService.java @@ -1,39 +1,148 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.service; +import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.validation.Valid; +import java.util.ArrayList; +import java.util.Optional; +import java.util.UUID; +import org.developerden.codosseum.dto.GameCreateRequest; import org.developerden.codosseum.dto.GameCreateResponse; import org.developerden.codosseum.dto.GameInfo; +import org.developerden.codosseum.dto.GameJoinResponse; import org.developerden.codosseum.dto.GameSettings; +import org.developerden.codosseum.dto.Player; +import org.developerden.codosseum.dto.PlayersMapper; +import org.developerden.codosseum.dto.phase.PhaseMapper; +import org.developerden.codosseum.mode.GameModeFactory; +import org.developerden.codosseum.mode.GameModeType; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.model.phase.GamePhaseKind; +import org.developerden.codosseum.model.phase.WithPlayersPhase; +import org.developerden.codosseum.model.player.EphemeralPlayer; +import org.developerden.codosseum.repository.AuthRepository; +import org.developerden.codosseum.repository.GameRepository; +import org.developerden.codosseum.service.game.GameCommand; +import org.developerden.codosseum.service.game.GameRunner; +import org.developerden.codosseum.service.game.GameRunnerRegistry; +import org.developerden.codosseum.service.game.state.SnapshotStore; +import org.developerden.codosseum.utils.EnumUtils; @Singleton public class GameService { + private final GameRepository gameRepository; + private final GameModeFactory gameModeFactory; + private final GameRunnerRegistry gameRunnerRegistry; + private final SnapshotStore snapshotStore; + + private final AuthRepository authRepository; + private final PlayersMapper playersMapper; + private final PhaseMapper phaseMapper; + + @Inject + GameService(GameRepository gameRepository, GameModeFactory gameModeFactory, + GameRunnerRegistry gameRunnerRegistry, SnapshotStore snapshotStore, + AuthRepository authRepository, + PlayersMapper playersMapper, + PhaseMapper phaseMapper) { + this.gameRepository = gameRepository; + this.gameModeFactory = gameModeFactory; + this.gameRunnerRegistry = gameRunnerRegistry; + this.snapshotStore = snapshotStore; + this.authRepository = authRepository; + this.playersMapper = playersMapper; + this.phaseMapper = phaseMapper; + } + + private String generateFreshKey() { + return UUID.randomUUID().toString(); + } + + /** + * Create a new game with the given settings and player as the creator. + * + * @param request the game creation request + * @return the response containing the game ID and player key + */ + public GameCreateResponse createGame(GameCreateRequest request) { + var gameModeType = EnumUtils.random(GameModeType.class, request.settings().allowedGameModes()); + var gameMode = gameModeFactory.fromType(gameModeType); + + var game = new Game(UUID.randomUUID(), request.settings(), gameMode); + + gameRepository.insertGame(game); - public GameCreateResponse createGame(String creator) { + var ephemeralPlayer = + new EphemeralPlayer(request.player().name(), game.id(), generateFreshKey(), true); + authRepository.save(game, ephemeralPlayer); + gameRunnerRegistry.getOrCreate(game.id()) + .tell(new GameCommand.CreateGame(game.id(), ephemeralPlayer)); + + return new GameCreateResponse(ephemeralPlayer.key(), game.id()); + } + + public GameInfo updateGame(String gameId, GameSettings settings) { throw new UnsupportedOperationException(); } - public GameInfo updateGame(GameSettings settings) { + public void deleteGame(String gameId) { throw new UnsupportedOperationException(); } - public GameInfo getGame(String id) { + public Optional getGame(UUID id) { + var gameOpt = gameRepository.findGameById(id); + + if (gameOpt.isEmpty()) { + return Optional.empty(); + } + var game = gameOpt.get(); + + var state = gameRunnerRegistry + .find(id) + .map(GameRunner::getCurrentState) + .or(() -> snapshotStore.load(id)) + .orElseThrow(() -> new IllegalStateException("No game state found for game " + id)); + + var phase = state.phase(); + if (!(phase instanceof WithPlayersPhase playersPhase)) { + throw new IllegalStateException("Game phase does not have players: " + phase); + } + + return Optional.of(new GameInfo( + game.settings(), + game.id(), + game.mode(), + playersMapper.toDto(playersPhase.players()), + phaseMapper.toDto(state.phase()), + new ArrayList<>() + )); + } + + public void startGame(UUID gameId) { + GameRunner runner = gameRunnerRegistry.getOrCreate(gameId); + if (runner.getCurrentState().phase().getKind() != GamePhaseKind.WAITING_FOR_PLAYERS) { + throw new IllegalStateException("Game is already running or finished"); + } + + runner.tell(new GameCommand.StartGame(gameId)); + } + + + public String getTemplate(String gameId, String lang) { throw new UnsupportedOperationException(); } @@ -41,4 +150,42 @@ public void initiateNextRound(String gameId) { throw new UnsupportedOperationException(); } + public GameCreateResponse restartGame(String gameId) { + throw new UnsupportedOperationException(); + } + + public Optional addPlayer(UUID id, @Valid Player player) { + var gameOpt = gameRepository.findGameById(id); + if (gameOpt.isEmpty()) { + return Optional.empty(); + } + + var game = gameOpt.get(); + authRepository.findPlayerByNameAndGameId(player.name(), game.id()) + .ifPresent(p -> { + throw new IllegalStateException( + "Player with name " + player.name() + " already exists in game"); + }); + + var runner = gameRunnerRegistry + .getOrCreate(id); + var state = runner + .getCurrentState(); + if (state.phase().getKind() != GamePhaseKind.WAITING_FOR_PLAYERS) { + throw new IllegalStateException("Game is already running or finished"); + } + + var playerKey = generateFreshKey(); + var ephemeralPlayer = new EphemeralPlayer(player.name(), game.id(), playerKey, false); + authRepository.save(game, ephemeralPlayer); + + runner.tell(new GameCommand.AddPlayer(game.id(), ephemeralPlayer)); + + return Optional.of(new GameJoinResponse(playerKey)); + + } + + public void beginWarmup(Game game) { + gameRunnerRegistry.sendCommand(new GameCommand.StartWarmup(game.id())); + } } diff --git a/src/main/java/org/developerden/codosseum/service/SubmissionService.java b/src/main/java/org/developerden/codosseum/service/SubmissionService.java index 77865bf..ebb1d58 100644 --- a/src/main/java/org/developerden/codosseum/service/SubmissionService.java +++ b/src/main/java/org/developerden/codosseum/service/SubmissionService.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ package org.developerden.codosseum.service; diff --git a/src/main/java/org/developerden/codosseum/service/game/GameAggregate.java b/src/main/java/org/developerden/codosseum/service/game/GameAggregate.java new file mode 100644 index 0000000..abd3246 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameAggregate.java @@ -0,0 +1,281 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.model.GameState; +import org.developerden.codosseum.model.GameStateBuilder; +import org.developerden.codosseum.model.phase.GamePhaseKind; +import org.developerden.codosseum.model.phase.InProgressPhase; +import org.developerden.codosseum.model.phase.WarmupPhase; +import org.developerden.codosseum.service.game.effect.SideEffect; +import org.developerden.codosseum.service.game.event.InternalGameEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles commands, produces events, and applies events to the game state. + * + *

There should be one instance of this class per game. + * + * @see GameState + * @see GameCommand + * @see InternalGameEvent + */ +public class GameAggregate { + private static final Duration DEFAULT_WARMUP_DURATION = Duration.ofSeconds(5); + private static final String KEY_WARMUP_TO_START = "warmup->start"; + private static final String KEY_START_RETRY = "warmup->await-challenge"; + private static final String KEY_ROUND_OVER = "round->over"; + private static final Logger log = LoggerFactory.getLogger(GameAggregate.class); + private final UUID gameId; + + private final GameState gameState; + + GameAggregate(UUID gameId, GameState gameState) { + this.gameId = gameId; + this.gameState = gameState; + } + + public GameState getGameState() { + return gameState; + } + + /** + * Handle a command, producing events and side effects, and returning the next state of the aggregate. + * + * @param cmd the command to handle. + * @return a result containing the events, side effects, and next state. + */ + public Result handle(GameCommand cmd) { + var decision = decide(cmd); + var newState = applyAll(gameState, decision.events()); + + var next = new GameAggregate(gameId, newState); + + return new Result(decision.events(), decision.effects(), next); + } + + /** + * Require that the game is in the given phase, throwing an exception if not. + * + * @param gamePhase the required phase. + */ + private void requirePhase(GamePhaseKind gamePhase) { + if (gameState.phase().getKind() != gamePhase) { + throw new IllegalStateException( + "Game is not in required phase: " + gamePhase + ", current phase: " + gameState.phase()); + } + } + + private

P requirePhase( + Class

phaseClass) { + if (!phaseClass.isInstance(gameState.phase())) { + throw new IllegalStateException( + "Game is not in required phase: " + phaseClass.getSimpleName() + ", current phase: " + + gameState.phase()); + } + return phaseClass.cast(gameState.phase()); + } + + /** + * Decide how to handle a command, producing events to send and side effects. + * + * @param cmd the command to handle. + * @return the decision containing events and side effects. + */ + private Decision decide(GameCommand cmd) { + if (!cmd.gameId().equals(gameId)) { + throw new IllegalArgumentException("Command gameId does not match aggregate gameId"); + } + + return switch (cmd) { + // Create Game command can only be handled in WAITING_FOR_PLAYERS phase + // and produces a GameCreated event + case GameCommand.CreateGame(var id, var creator) -> { + requirePhase(GamePhaseKind.WAITING_FOR_PLAYERS); + yield Decision.pure( + new InternalGameEvent.GameCreated(gameId, creator) + ); + + } + /* + Start Game command can only be handled in WARMUP phase + and produces a RoundStarted event for round 1 + If no challenge is set yet, it retries after a short delay in case the challenges service query is slow + The challenge is set by [WarmupStartedHandler] as soon as the warmup starts + */ + case GameCommand.StartGame(var id) -> { + requirePhase(GamePhaseKind.WARMUP); + + if (getGameState().currentChallengeInfo() == null) { + log.warn("Cannot start game {}, no challenge set, retrying...", gameId); + yield Decision.empty().withEffects( + new SideEffect.ScheduleAfter(KEY_START_RETRY, Duration.ofMillis(250), + new GameCommand.StartGame(gameId)) + ); + } + yield startRound(1); + } + // Add Player command can only be handled in WAITING_FOR_PLAYERS phase + // and produces a PlayerJoined event + case GameCommand.AddPlayer(var id, var player) -> { + requirePhase(GamePhaseKind.WAITING_FOR_PLAYERS); + yield Decision.pure(new InternalGameEvent.PlayerJoined(gameId, player)); + } + + // Start Warmup command can only be handled in WAITING_FOR_PLAYERS phase + // and produces a WarmupStarted event and schedules a StartGame command after the warmup duration + case GameCommand.StartWarmup(var id) -> { + requirePhase(GamePhaseKind.WAITING_FOR_PLAYERS); + // Emit warmup started and schedule transition to start after countdown + yield Decision.pure( + new InternalGameEvent.WarmupStarted(gameId, DEFAULT_WARMUP_DURATION) + ).withEffects( + new SideEffect.ScheduleAfter( + KEY_WARMUP_TO_START, + DEFAULT_WARMUP_DURATION, + new GameCommand.StartGame(gameId) + ) + ); + } + // Set Challenge Info command can be handled in any phase (TODO: is this correct?) + // and produces a ChallengeSet event + case GameCommand.SetChallengeInfo(var id, var challenge) -> Decision.pure( + new InternalGameEvent.ChallengeSet(gameId, challenge) + ); + case GameCommand.StartRound(var id) -> { + var phase = requirePhase(InProgressPhase.class); + + var nextRound = phase.currentRound() + 1; + + yield startRound(nextRound); + } + case GameCommand.EndRound endRound -> { + var phase = requirePhase(InProgressPhase.class); + // For now, ending a round is a no-op + yield Decision.pure( + new InternalGameEvent.RoundEnded(gameId, phase.currentRound()) + ); + } + }; + } + + + private Decision startRound(int roundNumber) { + var length = getRoundLength(); + ChallengeInfo challenge = Objects.requireNonNull( + getGameState().currentChallengeInfo(), + "Cannot start round without a challenge set" + ); + return Decision.pure( + new InternalGameEvent.RoundStarted(gameId, challenge, roundNumber, length) + ).withEffects( + new SideEffect.ScheduleAfter( + KEY_ROUND_OVER, + length, + new GameCommand.EndRound(gameId) + ) + ); + } + + private Duration getRoundLength() { + // TODO: make configurable + return Duration.ofMinutes(5); + } + + private GameState applyAll(GameState state, List events) { + var newState = state; + for (var event : events) { + newState = apply(newState, event); + } + return newState; + } + + private GameState apply(GameState state, InternalGameEvent event) { + if (!event.gameId().equals(gameId)) { + throw new IllegalArgumentException("Event gameId does not match aggregate gameId"); + } + return switch (event) { + case InternalGameEvent.PlayerJoined(var gameId, var player) -> + state.updatePlayers(players -> players + .builder() + .addOthers(player).build()); + case InternalGameEvent.GameCreated(var gameId, var player) -> state.updatePlayers(players -> + players.builder() + .admin(player) + .addOthers(player) + .build() + ); + case InternalGameEvent.WarmupStarted(var gameId, var duration) -> GameStateBuilder.from(state) + .withPhase(new WarmupPhase( + state.players(), + Instant.now().plus(duration) + )); + + case InternalGameEvent.ChallengeSet challengeSet -> GameStateBuilder.from(state) + .withCurrentChallengeInfo(challengeSet.challengeInfo()); + case InternalGameEvent.RoundEnded roundEnded -> state; // TODO: implement round end logic + + case InternalGameEvent.RoundStarted roundStarted -> GameStateBuilder.from(state) + .withCurrentChallengeInfo(roundStarted.challenge()) + .withPhase(new InProgressPhase( + state.players(), + roundStarted.challenge(), + roundStarted.roundNumber() + )); + }; + + } + + /** + * The result of deciding how to handle a command. + * + * @param events the events that were produced. + * @param effects the side effects that should be executed. + */ + private record Decision(List events, List effects) { + static Decision empty() { + return new Decision(List.of(), List.of()); + } + + static Decision pure(InternalGameEvent... events) { + return new Decision(Arrays.asList(events), List.of()); + } + + + public Decision withEffects(SideEffect... effects) { + return new Decision(events, Arrays.asList(effects)); + } + } + + /** + * The result of handling a command. + * + * @param events the events that were produced + * @param effects the side effects that should be executed + * @param next the next state of the aggregate + */ + public record Result(List events, List effects, + GameAggregate next) { + } + +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactory.java b/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactory.java new file mode 100644 index 0000000..949b5cf --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactory.java @@ -0,0 +1,22 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import java.util.UUID; +import org.developerden.codosseum.model.GameState; + +public interface GameAggregateFactory { + GameAggregate create(UUID gameId, GameState snapshot); +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactoryImpl.java b/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactoryImpl.java new file mode 100644 index 0000000..d09f2e3 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameAggregateFactoryImpl.java @@ -0,0 +1,31 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import jakarta.inject.Singleton; +import java.util.UUID; +import org.developerden.codosseum.model.GameState; + +/** + * Implementation of {@link GameAggregateFactory}, used to create new instances of {@link GameAggregate}. + */ +@Singleton +public class GameAggregateFactoryImpl implements GameAggregateFactory { + + @Override + public GameAggregate create(UUID gameId, GameState snapshot) { + return new GameAggregate(gameId, snapshot); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameCommand.java b/src/main/java/org/developerden/codosseum/service/game/GameCommand.java new file mode 100644 index 0000000..4e7306a --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameCommand.java @@ -0,0 +1,98 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import java.util.UUID; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.model.GamePhase; +import org.developerden.codosseum.model.player.GamePlayer; + +/** + * A command that can be executed on a {@link org.developerden.codosseum.model.Game} by sending it to its {@link GameRunner}. + */ +public sealed interface GameCommand { + /** + * The id of the game this command is for. + * + * @return the game id + */ + UUID gameId(); + + + /** + * Command to create a new game, triggering initial state setup. + * + * @param gameId the id of the game to create. This should be unique. + * @param creator the player who created the game. + */ + record CreateGame(UUID gameId, GamePlayer creator) implements GameCommand { + } + + /** + * Command to start the warmup phase of a game. + * + * @param gameId the id of the game to start the warmup for + */ + record StartWarmup(UUID gameId) implements GameCommand { + } + + /** + * Command to start a game. + * + * @param gameId the id of the game to start + */ + record StartGame(UUID gameId) implements GameCommand { + } + + /** + * Command to add a player to a game. + * + * @param gameId the id of the game to add the player to + * @param player the player to add + */ + record AddPlayer(UUID gameId, GamePlayer player) implements GameCommand { + } + + /** + * Command to set the current challenge for a game. + * It is undefined what this does if the game is not in a state to accept a new challenge (i.e. is already in progress). + * Generally, this command is only safe to send if the game is in the {@link GamePhase#WAITING_FOR_PLAYERS}, {@link GamePhase#WARMUP}, or {@link GamePhase#ROUND_OVER} phases. + * + * @param gameId the id of the game to set the challenge for + * @param info the challenge info to set + */ + record SetChallengeInfo(UUID gameId, ChallengeInfo info) implements GameCommand { + } + + /** + * Command to start a new round in a game. + * The game phase should be {@link GamePhase#IN_PROGRESS} for this to have any effect, + * and an error may be thrown if it is not. + * + * @param gameId the id of the game to start the round for + */ + record StartRound(UUID gameId) implements GameCommand { + } + + /** + * Command to end the current round in a game. + * The game phase should be {@link GamePhase#IN_PROGRESS} for this to have any effect. + * + * @param gameId the id of the game to end the round for + */ + record EndRound(UUID gameId) implements GameCommand { + } + +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameRunner.java b/src/main/java/org/developerden/codosseum/service/game/GameRunner.java new file mode 100644 index 0000000..a8f5a96 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameRunner.java @@ -0,0 +1,143 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import io.micronaut.scheduling.TaskScheduler; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import org.developerden.codosseum.model.GameState; +import org.developerden.codosseum.service.game.effect.SideEffect; +import org.developerden.codosseum.service.game.event.EventSink; +import org.developerden.codosseum.service.game.state.SnapshotStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Overseer of a single game's state and progression. + * Runs all game logic in a single-threaded executor. + * + */ +public class GameRunner { + private static final Logger log = LoggerFactory.getLogger(GameRunner.class); + private final UUID gameId; + + private final ExecutorService loop; + private final TaskScheduler scheduler; + private final EventSink eventSink; + private final SnapshotStore snapshotStore; + private final Map> scheduled = new ConcurrentHashMap<>(); + private volatile GameAggregate gameAggregate; + + public GameRunner(UUID gameId, TaskScheduler scheduler, EventSink eventSink, + SnapshotStore snapshotStore, + GameAggregateFactory aggregateFactory) { + this.gameId = gameId; + this.scheduler = scheduler; + this.eventSink = eventSink; + this.snapshotStore = snapshotStore; + + this.loop = Executors.newSingleThreadExecutor(r -> new Thread(r, "game-" + gameId)); + + GameState snapshot = this.snapshotStore.load(gameId) + .orElseGet(() -> this.snapshotStore.createInitial(gameId)); + this.gameAggregate = aggregateFactory.create(gameId, snapshot); + } + + /** + * Send a command to this game runner. + * + * @param cmd the command to handle. + */ + public void tell(GameCommand cmd) { + log.debug("tell game command: {}", cmd); + loop.execute(() -> handle(cmd)); + } + + public GameState getCurrentState() { + return gameAggregate.getGameState(); + } + + /** + * Handle a command. Must be called from the game loop thread for safety. + * + *

The command is first sent to the {@link GameAggregate} for handling, which produces events and side effects. + * Any events are published to the event sink, and side effects are executed. + * Finally, the new game state is persisted in {@link SnapshotStore}. + * + * @param cmd the command to handle. + */ + private void handle(GameCommand cmd) { + log.debug("handle command: {}", cmd); + var result = gameAggregate.handle(cmd); + this.gameAggregate = result.next(); + for (var event : result.events()) { + eventSink.publish(event); + } + // execute side effects after state mutation and publication + for (var effect : result.effects()) { + execute(effect); + } + snapshotStore.save(gameId, gameAggregate.getGameState()); + } + + private void execute(SideEffect effect) { + log.debug("execute effect: {}", effect); + switch (effect) { + case SideEffect.ScheduleAfter(var key, var delay, var command) -> { + // cancel any existing scheduled task for the same key + var existing = scheduled.remove(key); + if (existing != null) { + log.info("Cancelling existing scheduled task with key {} in game {}", key, gameId); + existing.cancel(false); + } + var future = scheduler.schedule(delay, () -> { + scheduled.remove(key); + tell(command); + }); + scheduled.put(key, future); + log.info("Scheduled task with key {} in game {} to run after {}", key, gameId, delay); + } + case SideEffect.CancelScheduled(var key) -> { + var existing = scheduled.remove(key); + if (existing != null) { + existing.cancel(false); + log.info("Cancelled scheduled task with key {} in game {}", key, gameId); + } + } + } + } + + /** + * Shutdown this game runner, cancelling any scheduled tasks. + * This should be called when the game is over and the runner is no longer needed. + */ + public void shutdown() { + // cancel scheduled tasks + for (var entry : scheduled.entrySet()) { + boolean cancel = entry.getValue().cancel(false); + if (!cancel) { + log.warn("Could not cancel scheduled task {} in game {}", entry.getKey(), gameId); + } + } + scheduled.clear(); + loop.shutdownNow(); + } + + +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameRunnerFactory.java b/src/main/java/org/developerden/codosseum/service/game/GameRunnerFactory.java new file mode 100644 index 0000000..db3b583 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameRunnerFactory.java @@ -0,0 +1,44 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import io.micronaut.scheduling.TaskScheduler; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.UUID; +import org.developerden.codosseum.service.game.event.EventSink; +import org.developerden.codosseum.service.game.state.SnapshotStore; + +@Singleton +public class GameRunnerFactory { + + private final EventSink eventSink; + private final SnapshotStore snapshotStore; + private final GameAggregateFactory aggregateFactory; + private final TaskScheduler taskScheduler; + + @Inject + public GameRunnerFactory(EventSink eventSink, SnapshotStore snapshotStore, + GameAggregateFactory aggregateFactory, TaskScheduler taskScheduler) { + this.eventSink = eventSink; + this.snapshotStore = snapshotStore; + this.aggregateFactory = aggregateFactory; + this.taskScheduler = taskScheduler; + } + + public GameRunner create(UUID gameId) { + return new GameRunner(gameId, taskScheduler, eventSink, snapshotStore, aggregateFactory); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/GameRunnerRegistry.java b/src/main/java/org/developerden/codosseum/service/game/GameRunnerRegistry.java new file mode 100644 index 0000000..850c768 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/GameRunnerRegistry.java @@ -0,0 +1,114 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; + +/** + * Registry for active game runners. + * Uses a concurrent hash map to store the runners, allowing for thread-safe access and modification. + * + *

This also provides a convenient way to send commands to the appropriate game runner without needing to manually look it up each time - see {@link #sendCommand(GameCommand)} and {@link #trySendCommand(GameCommand)}. + * + * @see GameRunner + */ +@Singleton +public class GameRunnerRegistry { + private final GameRunnerFactory factory; + private final Map runners = new ConcurrentHashMap<>(); + + @Inject + GameRunnerRegistry(GameRunnerFactory factory) { + this.factory = factory; + } + + /** + * Get an existing game runner or create a new one if it doesn't exist. + * This method should be used sparingly, considering whether a runner is expected to exist already or not. + * + *

For example, when creating a game, it's expected that no runner exists yet, so creating one is appropriate. + * Conversely, when sending a command to an existing game, it's expected that the runner already exists, and its absence should be treated as an error. + * As a law of thumb, assume that a runner should exist unless you are in the process of creating a new game. + * + *

It is generally better to use {@link #find(UUID)} or {@link #sendCommand(GameCommand)} depending on the context. + * + * @param gameId the id of the game + * @return the game runner + */ + public GameRunner getOrCreate(UUID gameId) { + return runners.computeIfAbsent(gameId, factory::create); + } + + + /** + * Find an existing game runner by game id. + * Returns an empty optional if no runner exists for the given game id. + * + * @param gameId the id of the game + * @return an optional containing the game runner if it exists, or empty if it doesn't + */ + public Optional find(UUID gameId) { + return Optional.ofNullable(runners.get(gameId)); + } + + /** + * Send a command to the game runner for the given game id, looking up the {@link GameRunner} from the registry automatically. + * If no runner exists for the given game id, an {@link IllegalStateException} is thrown. + * + *

This is the preferred method to use when sending commands to existing games, as it enforces the expectation that the game runner should already exist. + * + * @param command the command to send. + * @see #getOrCreate(UUID) for information about when a runner is expected to exist or not. + */ + public void sendCommand(@Nonnull GameCommand command) { + var runner = runners.get(command.gameId()); + if (runner != null) { + runner.tell(command); + } else { + throw new IllegalStateException("No game runner found for game id: " + command.gameId()); + } + } + + /** + * Try to send a command to the game runner for the given game id. + * If no runner exists for the given game id, the command is silently ignored. + * + * @param command the command to send. + */ + public void trySendCommand(@Nonnull GameCommand command) { + var runner = runners.get(command.gameId()); + if (runner != null) { + runner.tell(command); + } + } + + /** + * Stop and remove the game runner for the given game id. + * + * @param gameId the id of the game + */ + public void stop(UUID gameId) { + var r = runners.remove(gameId); + if (r != null) { + r.shutdown(); + } + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/effect/SideEffect.java b/src/main/java/org/developerden/codosseum/service/game/effect/SideEffect.java new file mode 100644 index 0000000..721a0ac --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/effect/SideEffect.java @@ -0,0 +1,36 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.effect; + +import java.time.Duration; +import org.developerden.codosseum.service.game.GameCommand; + +/** + * Side effects that a GameAggregate requests the runner to perform. + * Effects are executed by the {@code GameRunner} and are not part of state mutation. + */ +public sealed interface SideEffect permits SideEffect.ScheduleAfter, SideEffect.CancelScheduled { + + /** + * Schedule a command to be sent to this game after the given delay. + * The key is used for idempotency and cancellation/replacement. + */ + record ScheduleAfter(String key, Duration delay, GameCommand command) implements SideEffect {} + + /** + * Cancel a previously scheduled command by key. + */ + record CancelScheduled(String key) implements SideEffect {} +} diff --git a/src/main/java/org/developerden/codosseum/service/game/event/EventMapper.java b/src/main/java/org/developerden/codosseum/service/game/event/EventMapper.java new file mode 100644 index 0000000..cf4d2bb --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/event/EventMapper.java @@ -0,0 +1,62 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.event; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Optional; +import org.developerden.codosseum.dto.PlayersMapper; +import org.developerden.codosseum.event.GameEvent; +import org.developerden.codosseum.event.PlayerJoinEvent; +import org.developerden.codosseum.event.RoundStartEvent; +import org.developerden.codosseum.event.SyncEvent; +import org.developerden.codosseum.model.GamePhase; + +@Singleton +public class EventMapper { + private final PlayersMapper playersMapper; + + @Inject + public EventMapper(PlayersMapper playersMapper) { + this.playersMapper = playersMapper; + } + + public Optional fromInternal(InternalGameEvent internalEvent) { + return switch (internalEvent) { + case InternalGameEvent.GameCreated ignored -> Optional.empty(); + + case InternalGameEvent.PlayerJoined playerJoined -> Optional.of( + new PlayerJoinEvent(playerJoined.gameId(), playersMapper.toDto(playerJoined.player())) + ); + + case InternalGameEvent.WarmupStarted(var ignored, var warmupLength) -> Optional.of( + new SyncEvent(GamePhase.WARMUP, + // how long until warmup ends + Math.toIntExact(warmupLength.toSeconds()) + ) + ); + + case InternalGameEvent.ChallengeSet ignored -> Optional.empty(); + case InternalGameEvent.RoundStarted roundStarted -> Optional.of( + new RoundStartEvent( + roundStarted.challenge(), + roundStarted.roundNumber(), + roundStarted.roundLength() + ) + ); + case InternalGameEvent.RoundEnded roundEnded -> Optional.empty(); + }; + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/event/EventSink.java b/src/main/java/org/developerden/codosseum/service/game/event/EventSink.java new file mode 100644 index 0000000..b53b36f --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/event/EventSink.java @@ -0,0 +1,19 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.event; + +public interface EventSink { + void publish(InternalGameEvent event); +} diff --git a/src/main/java/org/developerden/codosseum/service/game/event/InternalGameEvent.java b/src/main/java/org/developerden/codosseum/service/game/event/InternalGameEvent.java new file mode 100644 index 0000000..8c341af --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/event/InternalGameEvent.java @@ -0,0 +1,59 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.event; + +import java.time.Duration; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.event.GameEvent; +import org.developerden.codosseum.model.Game; +import org.developerden.codosseum.model.player.GamePlayer; +import org.developerden.codosseum.service.game.GameCommand; + +/// An event that happened to a [Game] +/// This is the internal version of [GameEvent], and is used for internal messaging. +/// +/// Some events may not have a public counterpart, see [EventMapper] +/// +/// See also [GameCommand] +public sealed interface InternalGameEvent { + UUID gameId(); + + /// Emitted when a new game is created and the lobby is opened + record GameCreated(UUID gameId, GamePlayer creator) implements InternalGameEvent { + } + + record PlayerJoined(UUID gameId, GamePlayer player) implements InternalGameEvent { + } + + /** + * Emitted when the warmup countdown begins. + */ + record WarmupStarted(UUID gameId, Duration warmupLength) implements InternalGameEvent { + } + + record ChallengeSet(UUID gameId, ChallengeInfo challengeInfo) implements InternalGameEvent { + + } + + record RoundStarted(UUID gameId, @Nonnull ChallengeInfo challenge, int roundNumber, + @Nonnull Duration roundLength) + implements InternalGameEvent { + } + + record RoundEnded(UUID gameId, int roundNumber) implements InternalGameEvent { + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/event/MicronautEventSink.java b/src/main/java/org/developerden/codosseum/service/game/event/MicronautEventSink.java new file mode 100644 index 0000000..ad3d5c7 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/event/MicronautEventSink.java @@ -0,0 +1,33 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.event; + +import io.micronaut.context.event.ApplicationEventPublisher; +import jakarta.inject.Singleton; + +@Singleton +public class MicronautEventSink implements EventSink { + + private final ApplicationEventPublisher publisher; + + public MicronautEventSink(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void publish(InternalGameEvent event) { + publisher.publishEvent(event); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/event/SseEventSink.java b/src/main/java/org/developerden/codosseum/service/game/event/SseEventSink.java new file mode 100644 index 0000000..c5a477a --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/event/SseEventSink.java @@ -0,0 +1,89 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.event; + +import io.micronaut.http.sse.Event; +import io.micronaut.runtime.event.annotation.EventListener; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.UUID; +import org.developerden.codosseum.event.GameEvent; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +/** + * Publishes {@link InternalGameEvent}s to subscribed SSE clients. + */ +@Singleton +public class SseEventSink { + private final Sinks.Many eventSink = + Sinks.many().multicast().onBackpressureBuffer(); + private final EventMapper eventMapper; + private final Logger logger = LoggerFactory.getLogger(SseEventSink.class); + + + @Inject + public SseEventSink(EventMapper eventMapper) { + this.eventMapper = eventMapper; + } + + + @EventListener + public void on(InternalGameEvent event) { + UUID gameId = event.gameId(); + logger.info("Publishing event {} for game {}", event, gameId); + + eventSink.emitNext(event, (signalType, emitResult) -> + switch (emitResult) { + case FAIL_NON_SERIALIZED -> true; // retry until serialized + case FAIL_OVERFLOW -> { + logger.warn("Dropping event {} due to overflow", event); + yield false; + } + default -> { + if (emitResult.isFailure()) { + logger.debug("Emit failed: {}", emitResult); + } + yield false; + } + }); + } + + /** + * Subscribe to all events across all games. + * This is primarily for testing or logging purposes. + */ + public Publisher> allEvents() { + return eventSink.asFlux() + .flatMap(this::fromInternal); + } + + + public Publisher> subscribeToPublicSse(UUID gameId) { + return eventSink.asFlux() + .filter(e -> e.gameId().equals(gameId)) + .flatMap(this::fromInternal); + } + + private Mono> fromInternal(InternalGameEvent internalEvent) { + return eventMapper.fromInternal(internalEvent) + .map(publicEvent -> Event.of(publicEvent).name(publicEvent.getClass().getSimpleName())) + .map(Mono::just) + .orElse(Mono.empty()); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/process/WarmupStartedHandler.java b/src/main/java/org/developerden/codosseum/service/game/process/WarmupStartedHandler.java new file mode 100644 index 0000000..dda52f2 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/process/WarmupStartedHandler.java @@ -0,0 +1,69 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.process; + +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.scheduling.annotation.Async; +import jakarta.inject.Singleton; +import org.developerden.codosseum.challenges.client.api.DefaultApi; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.model.phase.WarmupPhase; +import org.developerden.codosseum.repository.GameRepository; +import org.developerden.codosseum.service.game.GameCommand; +import org.developerden.codosseum.service.game.GameRunnerRegistry; +import org.developerden.codosseum.service.game.event.InternalGameEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Listens for games going into their {@link WarmupPhase} and queries a random challenge for them. + */ +@Singleton +public class WarmupStartedHandler implements ApplicationEventListener { + + private final DefaultApi defaultApi; + private final GameRepository gameRepository; + private final GameRunnerRegistry gameRunnerRegistry; + private final Logger log = LoggerFactory.getLogger(WarmupStartedHandler.class); + + public WarmupStartedHandler(DefaultApi defaultApi, GameRepository gameRepository, + GameRunnerRegistry gameRunnerRegistry) { + this.defaultApi = defaultApi; + this.gameRepository = gameRepository; + this.gameRunnerRegistry = gameRunnerRegistry; + } + + @Override + @Async + public void onApplicationEvent(InternalGameEvent event) { + var gameStarted = (InternalGameEvent.WarmupStarted) event; + + var game = gameRepository.findGameById(gameStarted.gameId()) + .orElseThrow(); + + + ChallengeInfo info = defaultApi.challengesRandomGet( + null, + null + ).block(); + + gameRunnerRegistry.sendCommand(new GameCommand.SetChallengeInfo(game.id(), info)); + } + + @Override + public boolean supports(InternalGameEvent event) { + return event instanceof InternalGameEvent.WarmupStarted; + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/state/DefaultInitialGameStateProvider.java b/src/main/java/org/developerden/codosseum/service/game/state/DefaultInitialGameStateProvider.java new file mode 100644 index 0000000..062eeb5 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/state/DefaultInitialGameStateProvider.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.state; + +import jakarta.inject.Singleton; +import java.util.HashSet; +import java.util.UUID; +import org.developerden.codosseum.model.GamePlayers; +import org.developerden.codosseum.model.GameState; +import org.developerden.codosseum.model.GameStateBuilder; +import org.developerden.codosseum.model.phase.WaitingForPlayersPhase; + +/** + * Default initial state: lobby waiting for players. + */ +@Singleton +public class DefaultInitialGameStateProvider implements InitialGameStateProvider { + @Override + public GameState create(UUID gameId) { + return GameStateBuilder.builder() + .gameId(gameId) + .phase(new WaitingForPlayersPhase( + new GamePlayers(null, new HashSet<>()) + )) + .currentRound(null) + .build(); + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/state/InMemorySnapshotStore.java b/src/main/java/org/developerden/codosseum/service/game/state/InMemorySnapshotStore.java new file mode 100644 index 0000000..d1e1a5e --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/state/InMemorySnapshotStore.java @@ -0,0 +1,50 @@ + +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.state; + +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.developerden.codosseum.model.GameState; + +@Singleton +public final class InMemorySnapshotStore implements SnapshotStore { + private final ConcurrentMap store = new ConcurrentHashMap<>(); + private final InitialGameStateProvider initialProvider; + + public InMemorySnapshotStore(InitialGameStateProvider initialProvider) { + this.initialProvider = initialProvider; + } + + @Override + public Optional load(UUID gameId) { + return Optional.ofNullable(store.get(gameId)); + } + + @Override + public void save(UUID gameId, GameState state) { + store.put(gameId, state); + } + + @Override + public GameState createInitial(UUID gameId) { + var state = initialProvider.create(gameId); + store.put(gameId, state); + return state; + } +} diff --git a/src/main/java/org/developerden/codosseum/service/game/state/InitialGameStateProvider.java b/src/main/java/org/developerden/codosseum/service/game/state/InitialGameStateProvider.java new file mode 100644 index 0000000..779093e --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/state/InitialGameStateProvider.java @@ -0,0 +1,26 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.state; + +import java.util.UUID; +import org.developerden.codosseum.model.GameState; + +/** + * Produces the initial GameState for a given game id. + */ +@FunctionalInterface +public interface InitialGameStateProvider { + GameState create(UUID gameId); +} diff --git a/src/main/java/org/developerden/codosseum/service/game/state/SnapshotStore.java b/src/main/java/org/developerden/codosseum/service/game/state/SnapshotStore.java new file mode 100644 index 0000000..e294b5a --- /dev/null +++ b/src/main/java/org/developerden/codosseum/service/game/state/SnapshotStore.java @@ -0,0 +1,31 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.service.game.state; + +import java.util.Optional; +import java.util.UUID; +import org.developerden.codosseum.model.GameState; + +/** + * Persistence boundary for GameState snapshots. + * Implementations must be thread-safe. + */ +public interface SnapshotStore { + Optional load(UUID gameId); + + void save(UUID gameId, GameState state); + + GameState createInitial(UUID gameId); +} diff --git a/src/main/java/org/developerden/codosseum/service/package-info.java b/src/main/java/org/developerden/codosseum/service/package-info.java index d1593a8..0ed6f07 100644 --- a/src/main/java/org/developerden/codosseum/service/package-info.java +++ b/src/main/java/org/developerden/codosseum/service/package-info.java @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay - * SPDX-License-Identifier: AGPL-3.0-or-later + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later * - * This program is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, either - * version 3 of the License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ /** diff --git a/src/main/java/org/developerden/codosseum/utils/CollectionUtils.java b/src/main/java/org/developerden/codosseum/utils/CollectionUtils.java new file mode 100644 index 0000000..8ad5d39 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/utils/CollectionUtils.java @@ -0,0 +1,29 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.utils; + +import jakarta.validation.constraints.NotEmpty; +import java.util.Collection; + +public class CollectionUtils { + public static E pickRandom(@NotEmpty Collection collection) { + if (collection.isEmpty()) { + throw new IllegalArgumentException("Collection is empty"); + } + + return collection.stream().skip((int) (Math.random() * collection.size())).findFirst() + .orElseThrow(); + } +} diff --git a/src/main/java/org/developerden/codosseum/utils/EnumUtils.java b/src/main/java/org/developerden/codosseum/utils/EnumUtils.java new file mode 100644 index 0000000..7bc96e1 --- /dev/null +++ b/src/main/java/org/developerden/codosseum/utils/EnumUtils.java @@ -0,0 +1,40 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.utils; + +import java.util.Collection; +import java.util.SplittableRandom; + +public class EnumUtils { + + private static final SplittableRandom RANDOM = new SplittableRandom(); + + /** + * Picks a random enum value from the given enum type, optionally including only the specified values. + * + * @param enumType the enum type to pick from. + * @param including the enum values to include, or an empty collection to include all values. + * @param the enum type. + * @return a random enum value from the given enum type. + */ + public static synchronized > E random(Class enumType, Collection including) { + if (including.isEmpty()) { + int index = RANDOM.nextInt(0, enumType.getEnumConstants().length); + return enumType.getEnumConstants()[index]; + } + + return CollectionUtils.pickRandom(including); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..b919797 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2023 JohnnyJayJay +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify it under the terms of +# the GNU Affero General Public License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. +# If not, see . +codosseum: + challenge-source: + type: repo + url: https://github.com/codosseum-org/challenges.git + pull-interval: PT1M + game-retention: PT30S + enabled-languages: + - clojure + - java + - haskell diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..c172c76 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Alex Wood +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify it under the terms of +# the GNU Affero General Public License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. +# If not, see . +micronaut: + security: + authentication: idtoken + oauth2: + enabled: false + +openapi-micronaut-client: + base-path: http://localhost:8081 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eab238a..9b7aa9f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,5 +12,41 @@ # You should have received a copy of the GNU Affero General Public License along with this program. # If not, see . micronaut: + application: name: codosseum + security: + authentication: idtoken + oauth2: + clients: + keycloak: + client-secret: '${OAUTH_CLIENT_SECRET:secret}' + client-id: '${OAUTH_CLIENT_ID:myclient}' + openid: + issuer: '${OIDC_ISSUER_DOMAIN:`http://localhost:8080`}/realms/${KEYCLOAK_REALM:myrealm}' + endpoints: + logout: + get-allowed: true + intercept-url-map: + - pattern: /swagger/** + http-method: GET + access: + - isAnonymous() + - pattern: /swagger-ui/** + http-method: GET + access: + - isAnonymous() + + caches: + templates: + expire-after-write: 5m + + router: + static-resources: + swagger: + paths: classpath:META-INF/swagger + mapping: /swagger/** + + swagger-ui: + paths: classpath:META-INF/swagger/views/swagger-ui + mapping: /swagger-ui/** \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index d0196c7..eddba0d 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,21 +1,16 @@ - + diff --git a/src/test/groovy/org/developerden/codosseum/GameControllerSpec.groovy b/src/test/groovy/org/developerden/codosseum/GameControllerSpec.groovy new file mode 100644 index 0000000..020003e --- /dev/null +++ b/src/test/groovy/org/developerden/codosseum/GameControllerSpec.groovy @@ -0,0 +1,87 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.developerden.codosseum.dto.* +import org.developerden.codosseum.mode.GameModeType +import spock.lang.Specification + +@MicronautTest +class GameControllerSpec extends Specification { + + @Inject + @Client("/") + HttpClient http + + def "GET /games/{id} not found returns 404"() { + given: + def id = UUID.randomUUID() + + when: + http.toBlocking().exchange("/games/${id}", String) + + then: + def e = thrown(HttpClientResponseException) + e.status.code == 404 + } + + def "POST /games returns 201 and has a valid body"() { + when: + def response = http.toBlocking().exchange(HttpRequest.POST("/games", + new GameCreateRequest( + GameSettingsBuilder.builder().allowedGameModes(List.of(GameModeType.FASTEST)).build(), + new Player("test player") + ) + ), GameCreateResponse) + + then: + response.status.code == 201 + response.body().id() != null + } + + def "POST /games returns 201 and GET /games/{id} returns 200"() { + when: + def response = http.toBlocking().exchange(HttpRequest.POST("/games", + new GameCreateRequest( + GameSettingsBuilder.builder().allowedGameModes(List.of(GameModeType.FASTEST)).build(), + new Player("test player") + ) + ), GameCreateResponse) + + then: + response.status.code == 201 + def body = response.body() + + body.id() != null + def id = body.id() + + when: + def getResponse = http.toBlocking().exchange("/games/${id}", GameInfo) + then: + getResponse.status.code == 200 + def info = getResponse.body() + info.id() == id + info.players().allPlayers().count() == 1 + info.settings().allowedGameModes() == [GameModeType.FASTEST] + } + + +} diff --git a/src/test/groovy/org/developerden/codosseum/GameFlowSpec.groovy b/src/test/groovy/org/developerden/codosseum/GameFlowSpec.groovy new file mode 100644 index 0000000..eed7772 --- /dev/null +++ b/src/test/groovy/org/developerden/codosseum/GameFlowSpec.groovy @@ -0,0 +1,127 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.sse.Event +import io.micronaut.test.annotation.MockBean +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.developerden.codosseum.challenges.client.api.DefaultApi +import org.developerden.codosseum.dto.* +import org.developerden.codosseum.dto.phase.ApiInProgressPhase +import org.developerden.codosseum.event.GameEvent +import org.developerden.codosseum.event.RoundStartEvent +import org.developerden.codosseum.event.SyncEvent +import org.developerden.codosseum.mode.GameModeType +import org.developerden.codosseum.model.GamePhase +import org.developerden.codosseum.service.game.event.SseEventSink +import org.developerden.codosseum.stubs.Stubs +import org.reactivestreams.Subscription +import reactor.core.publisher.BaseSubscriber +import reactor.core.publisher.Mono +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.util.concurrent.CopyOnWriteArrayList + +@MicronautTest +class GameFlowSpec extends Specification { + @Inject + @Client("/") + HttpClient http + + @Inject + SseEventSink eventSink + @Inject + DefaultApi defaultApi + + // has to be thread safe as events come from another thread + List eventsReceived = new CopyOnWriteArrayList<>() + + + void setup() { + eventsReceived.clear() + eventSink.allEvents().subscribe(new BaseSubscriber>() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + request(Long.MAX_VALUE) + } + + @Override + protected void hookOnNext(Event value) { + eventsReceived.add(value.data) + } + }) + } + + def "Warmup phase starts correctly"() { + given: + def challenge = Stubs.fakeChallengeInfo() + 1 * defaultApi.challengesRandomGet(_, _) >> Mono.just(challenge) + + when: + def response = http.toBlocking().exchange(HttpRequest.POST("/games", + new GameCreateRequest( + GameSettingsBuilder.builder().allowedGameModes(List.of(GameModeType.FASTEST)).build(), + new Player("test player") + ) + ), GameCreateResponse) + + then: + response.status.code == 201 + response.body().id() != null + + def gameId = response.body().id() + def key = response.body().adminKey() + + + when: + def warmupResponse = http.toBlocking() + .exchange(HttpRequest.POST("/games/${gameId}/warmup", null) + .header("Authorization", "Game $key"), Void) + + then: + warmupResponse.status.code == 204 + + and: "wait until both events arrive" + def conditions = new PollingConditions(timeout: 7, initialDelay: 0.1, delay: 0.1) + conditions.eventually { + assert eventsReceived.size() == 2 + assert eventsReceived[0] instanceof SyncEvent + assert (eventsReceived[0] as SyncEvent).state() == GamePhase.WARMUP + assert eventsReceived[1] instanceof RoundStartEvent + assert (eventsReceived[1] as RoundStartEvent).round() == 1 + + } + + + def infoResponse = http.toBlocking().exchange("/games/${gameId}", GameInfo) + then: + infoResponse.status.code == 200 + def info = infoResponse.body() + info.id() == gameId + info.phase() instanceof ApiInProgressPhase + info.players().allPlayers().count() == 1 + + } + + @MockBean(DefaultApi) + DefaultApi defaultApi() { + Mock(DefaultApi) + } +} diff --git a/src/test/groovy/org/developerden/codosseum/PlayersControllerSpec.groovy b/src/test/groovy/org/developerden/codosseum/PlayersControllerSpec.groovy new file mode 100644 index 0000000..4fce271 --- /dev/null +++ b/src/test/groovy/org/developerden/codosseum/PlayersControllerSpec.groovy @@ -0,0 +1,64 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import org.developerden.codosseum.dto.* +import org.developerden.codosseum.mode.GameModeType +import spock.lang.Specification + +@MicronautTest +class PlayersControllerSpec extends Specification { + @Inject + @Client("/") + HttpClient http + + def "Players can join game"() { + given: + def createResponse = http.toBlocking().exchange(HttpRequest.POST("/games", + new GameCreateRequest( + GameSettingsBuilder.builder().allowedGameModes(List.of(GameModeType.FASTEST)).build(), + new Player("test player") + ) + ), GameCreateResponse) + def gameId = createResponse.body().id() + + when: + def joinResponse = http.toBlocking().exchange(HttpRequest.POST("/games/${gameId}/players", + new Player("second player")), GameJoinResponse) + + then: + joinResponse.status.code == 200 + def joinBody = joinResponse.body() + joinBody.key() != null + + when: + def getResponse = http.toBlocking().exchange("/games/${gameId}", GameInfo) + then: + getResponse.status.code == 200 + def info = getResponse.body() + info.id() == gameId + info.players().allPlayers().count() == 2 + info.players().allPlayers().find { it.name() == "second player" } != null + info.settings().allowedGameModes() == [GameModeType.FASTEST] + + } + + +} diff --git a/src/test/java/org/developerden/codosseum/OpenApiExposedTest.java b/src/test/java/org/developerden/codosseum/OpenApiExposedTest.java new file mode 100644 index 0000000..a9fd778 --- /dev/null +++ b/src/test/java/org/developerden/codosseum/OpenApiExposedTest.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +@MicronautTest +class OpenApiExposedTest { + + @Test + void openApi(@Client("/") HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + assertDoesNotThrow(() -> client.exchange("/swagger/codosseum-0.0.yml")); + } +} diff --git a/src/main/java/org/developerden/codosseum/model/EphemeralPlayer.java b/src/test/java/org/developerden/codosseum/OpenApiGeneratedTest.java similarity index 56% rename from src/main/java/org/developerden/codosseum/model/EphemeralPlayer.java rename to src/test/java/org/developerden/codosseum/OpenApiGeneratedTest.java index c2e3e87..d1fb756 100644 --- a/src/main/java/org/developerden/codosseum/model/EphemeralPlayer.java +++ b/src/test/java/org/developerden/codosseum/OpenApiGeneratedTest.java @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 JohnnyJayJay + * SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under the terms of @@ -12,10 +12,21 @@ * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . - * */ -package org.developerden.codosseum.model; +package org.developerden.codosseum; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.micronaut.core.io.ResourceLoader; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +class OpenApiGeneratedTest { -public record EphemeralPlayer(String name, String key, boolean admin) { + @Test + void buildGeneratesOpenApi(ResourceLoader resourceLoader) { + assertTrue(resourceLoader.getResource("META-INF/swagger/codosseum-0.0.yml").isPresent()); + } } diff --git a/src/test/java/org/developerden/codosseum/SwaggerUiGeneratedTest.java b/src/test/java/org/developerden/codosseum/SwaggerUiGeneratedTest.java new file mode 100644 index 0000000..53066ed --- /dev/null +++ b/src/test/java/org/developerden/codosseum/SwaggerUiGeneratedTest.java @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.micronaut.core.io.ResourceLoader; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +@MicronautTest(startApplication = false) +class SwaggerUiGeneratedTest { + + @Test + void buildGeneratesOpenApi(ResourceLoader resourceLoader) { + assertTrue( + resourceLoader.getResource("META-INF/swagger/views/swagger-ui/index.html").isPresent()); + } +} diff --git a/src/test/java/org/developerden/codosseum/SwaggerUiTest.java b/src/test/java/org/developerden/codosseum/SwaggerUiTest.java new file mode 100644 index 0000000..028212b --- /dev/null +++ b/src/test/java/org/developerden/codosseum/SwaggerUiTest.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +@MicronautTest +class SwaggerUiTest { + + @Test + void openApi(@Client("/") HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + assertDoesNotThrow(() -> client.exchange("/swagger-ui/index.html")); + } +} diff --git a/src/test/java/org/developerden/codosseum/auth/GameKeyAuthIntegrationTest.java b/src/test/java/org/developerden/codosseum/auth/GameKeyAuthIntegrationTest.java new file mode 100644 index 0000000..107a1a7 --- /dev/null +++ b/src/test/java/org/developerden/codosseum/auth/GameKeyAuthIntegrationTest.java @@ -0,0 +1,92 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + + +package org.developerden.codosseum.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.uri.UriBuilder; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import java.net.URI; +import java.util.List; +import java.util.UUID; +import org.developerden.codosseum.dto.GameCreateRequest; +import org.developerden.codosseum.dto.GameCreateResponse; +import org.developerden.codosseum.dto.GameSettingsBuilder; +import org.developerden.codosseum.dto.Player; +import org.developerden.codosseum.dto.user.User; +import org.developerden.codosseum.mode.GameModeType; +import org.junit.jupiter.api.Test; + +@MicronautTest +class GameKeyAuthIntegrationTest { + + private static final URI SELF_URI = UriBuilder.of("/users").path("@self").build(); + @Inject + @Client("/") + HttpClient client; + + + @Test + void allows_with_valid_game_key() { + HttpResponse testPlayer = + client.toBlocking().exchange(HttpRequest.POST("/games", + new GameCreateRequest( + GameSettingsBuilder.builder().allowedGameModes(List.of(GameModeType.FASTEST)) + .build(), + new Player("test player") + ) + ), GameCreateResponse.class); + + String key = testPlayer.body().adminKey(); + + HttpResponse response = client.toBlocking().exchange( + HttpRequest.GET(SELF_URI) + .header(HttpHeaders.AUTHORIZATION, "Game " + key), + User.class); + assertEquals(HttpStatus.OK, response.getStatus()); + assertEquals("test player", response.body().name()); + assertEquals(testPlayer.body().id(), response.body().gameId()); + } + + @Test + void disallows_with_valid_game_key() { + String key = UUID.randomUUID().toString(); + + HttpClientResponseException e = + assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange( + HttpRequest.GET(SELF_URI) + .header(HttpHeaders.AUTHORIZATION, "Game " + key))); + + assertEquals(HttpStatus.UNAUTHORIZED, e.getStatus()); + } + + + @Test + void unauthorized_without_token() { + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, + () -> client.toBlocking().exchange(HttpRequest.GET(SELF_URI), String.class)); + assertEquals(HttpStatus.UNAUTHORIZED, e.getStatus()); + } +} diff --git a/src/test/java/org/developerden/codosseum/auth/JwtAuthIntegrationTest.java b/src/test/java/org/developerden/codosseum/auth/JwtAuthIntegrationTest.java new file mode 100644 index 0000000..cc3d18e --- /dev/null +++ b/src/test/java/org/developerden/codosseum/auth/JwtAuthIntegrationTest.java @@ -0,0 +1,88 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + + +package org.developerden.codosseum.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.cookie.Cookie; +import io.micronaut.http.uri.UriBuilder; +import io.micronaut.security.token.generator.TokenGenerator; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import jakarta.inject.Inject; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.developerden.codosseum.dto.user.User; +import org.junit.jupiter.api.Test; + +@MicronautTest +class JwtAuthIntegrationTest implements TestPropertyProvider { + + private static final URI SELF_URI = UriBuilder.of("/users").path("@self").build(); + @Inject + @Client("/") + HttpClient client; + @Inject + TokenGenerator tokenGenerator; + + @Override + public Map getProperties() { + Map p = new HashMap<>(); + p.put("micronaut.security.enabled", "true"); + p.put("micronaut.security.authentication", "idtoken"); // use ID Token cookie + p.put("micronaut.security.oauth2.enabled", "false"); // no real IdP in tests + p.put("micronaut.security.token.jwt.enabled", "true"); + p.put("micronaut.security.token.jwt.cookie.enabled", "true"); // cookie name defaults to JWT + p.put("micronaut.security.token.jwt.signatures.secret.generator.secret", + "please-change-test-secret"); + p.put("micronaut.security.token.roles-name", "roles"); // map 'roles' claim to authorities + return p; + } + + private String jwt(Map claims) { + return tokenGenerator.generateToken(claims).orElseThrow(); + } + + @Test + void allows_with_required_role() { + // Match your route annotations: use "ADMIN" for @Secured("ADMIN") or "ROLE_ADMIN" for @Secured("ROLE_ADMIN") + UUID userId = UUID.randomUUID(); + String token = jwt(Map.of("sub", userId.toString(), "name", "Test User")); + HttpRequest req = HttpRequest.GET(SELF_URI).cookie(Cookie.of("JWT", token)); + HttpResponse resp = client.toBlocking().exchange(req, User.class); + assertEquals(HttpStatus.OK, resp.getStatus()); + assertEquals(userId, resp.body().id()); + assertEquals("Test User", resp.body().name()); + assertNull(resp.body().gameId()); + } + + @Test + void unauthorized_without_token() { + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, + () -> client.toBlocking().exchange(HttpRequest.GET(SELF_URI), String.class)); + assertEquals(HttpStatus.UNAUTHORIZED, e.getStatus()); + } +} diff --git a/src/test/java/org/developerden/codosseum/stubs/Stubs.java b/src/test/java/org/developerden/codosseum/stubs/Stubs.java new file mode 100644 index 0000000..343518d --- /dev/null +++ b/src/test/java/org/developerden/codosseum/stubs/Stubs.java @@ -0,0 +1,62 @@ +/* + * # SPDX-FileCopyrightText: 2025 Alexander Wood (BristerMitten) + * # SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package org.developerden.codosseum.stubs; + +import java.util.List; +import org.developerden.codosseum.challenges.client.model.Author; +import org.developerden.codosseum.challenges.client.model.ChallengeInfo; +import org.developerden.codosseum.challenges.client.model.Contact; +import org.developerden.codosseum.challenges.client.model.Difficulty; +import org.developerden.codosseum.challenges.client.model.Example; +import org.developerden.codosseum.challenges.client.model.Solution; +import org.developerden.codosseum.challenges.client.model.Test; + +public class Stubs { + + public static ChallengeInfo fakeChallengeInfo() { + return new ChallengeInfo( + "empty-schema", + new Author( + "Author", + List.of( + new Contact( + "Author Contact Type", + "Author Contact Value" + ) + ) + ), + "License", + "Language", + "Test Challenge", + Difficulty.EASY, + List.of("tag1", "tag2"), + "This is a test challenge.", + "Input Format", + List.of(new Example( + List.of("Example Input"), + List.of("Example Output") + )), + List.of(new Test( + "Test", + List.of("Test Input"), + List.of("Test Output" + ))), + new Solution( + "Solution Language", + "solution file" + ) + ); + } +}