diff --git a/.dev/scopes.txt b/.dev/scopes.txt index 5ae017ad7..104c10432 100644 --- a/.dev/scopes.txt +++ b/.dev/scopes.txt @@ -1,6 +1,7 @@ # docs readme contributing +agents # chore git gradle @@ -25,4 +26,4 @@ network logging Game -Board \ No newline at end of file +Board diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 80e36fc72..8350b7cef 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,54 +1,76 @@ -on: [push, pull_request, create] +name: CI + +on: + push: + pull_request: + workflow_dispatch: + jobs: build: runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs - os: [ubuntu-latest] #, windows-latest, macos-latest] - jdk: [8] + os: [ubuntu-latest, windows-latest, macos-latest] + jdk: [11, 17, 21] steps: - - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.jdk }} - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: ${{ matrix.jdk }} - cache: 'gradle' - #- name: Cache Gradle packages - # uses: actions/cache@v4 - # with: - # path: [~/.gradle/caches, ~/.gradle/wrapper] - # key: ${{ matrix.os }}-gradle-jdk${{ matrix.jdk }}-${{ hashFiles('**/*.gradle.kts') }} - # restore-keys: ${{ matrix.os }}-gradle-jdk${{ matrix.jdk }} - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: List Files - run: find -maxdepth 1 -type d - - name: Execute tests - run: ./gradlew clean check || ( exitcode=$?; find build/tests -name '*.log' -type f | while read f; do echo -e "\033[4m$f\033[0m"; cat $f; done && exit $exitcode; ) - - name: Bundle Artifacts - run: ./gradlew bundle - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: - name: software-challenge-backend-${{ github.sha }} - path: | - build/bundle/*.zip - build/bundle/*.jar + - uses: actions/checkout@v4 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.jdk }} + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Build, verify and bundle + run: ./gradlew --no-daemon clean check bundle + - name: Prepare CI diagnostics artifact + if: always() + shell: bash + run: | + { + echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + echo "Job: ${{ github.job }}" + echo "OS: ${{ matrix.os }}" + echo "JDK: ${{ matrix.jdk }}" + echo "SHA: ${{ github.sha }}" + } > ./build/reports/ci-info.txt + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports-${{ matrix.os }}-jdk${{ matrix.jdk }}-${{ github.sha }} + if-no-files-found: warn + path: | + ./build/reports/ci-info.txt + **/build/reports/**/* + **/build/test-results/**/* + - name: Upload bundle artifacts + uses: actions/upload-artifact@v4 + with: + name: software-challenge-backend-${{ matrix.os }}-jdk${{ matrix.jdk }}-${{ github.sha }} + path: | + build/bundle/*.zip + build/bundle/*.jar + release: needs: build runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write steps: - - uses: actions/download-artifact@v4 # https://github.com/actions/download-artifact - with: - name: software-challenge-backend-${{ github.sha }} - path: artifacts - - name: Release ${{ github.ref }} - uses: softprops/action-gh-release@v1 # https://github.com/softprops/action-gh-release - with: - files: artifacts/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + - uses: actions/download-artifact@v4 + with: + pattern: software-challenge-backend-*-${{ github.sha }} + path: artifacts + merge-multiple: true + - name: Release ${{ github.ref }} + uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 311bf6d6e..3f9082b95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ out/ # Artifacts from test runs logs/ log/ -replays/ \ No newline at end of file +replays/ +plans/ diff --git a/.old-plugins/blokus_2020/Board.kt b/.old-plugins/hive_2020/Board.kt similarity index 99% rename from .old-plugins/blokus_2020/Board.kt rename to .old-plugins/hive_2020/Board.kt index ba4ae40fb..220a0907e 100644 --- a/.old-plugins/blokus_2020/Board.kt +++ b/.old-plugins/hive_2020/Board.kt @@ -106,6 +106,7 @@ data class Board( fun getFieldsOwnedBy(owner: PlayerColor): List = fields.filter { it.owner == owner } + /** @suppress */ companion object { private const val SHIFT = (Constants.BOARD_SIZE - 1) / 2 diff --git a/.old-plugins/blokus_2020/Field.kt b/.old-plugins/hive_2020/Field.kt similarity index 100% rename from .old-plugins/blokus_2020/Field.kt rename to .old-plugins/hive_2020/Field.kt diff --git a/.old-plugins/blokus_2020/FieldState.kt b/.old-plugins/hive_2020/FieldState.kt similarity index 100% rename from .old-plugins/blokus_2020/FieldState.kt rename to .old-plugins/hive_2020/FieldState.kt diff --git a/.old-plugins/blokus_2020/GameState.kt b/.old-plugins/hive_2020/GameState.kt similarity index 99% rename from .old-plugins/blokus_2020/GameState.kt rename to .old-plugins/hive_2020/GameState.kt index 8e265eae3..419b2f3fe 100644 --- a/.old-plugins/blokus_2020/GameState.kt +++ b/.old-plugins/hive_2020/GameState.kt @@ -108,6 +108,7 @@ class GameState @JvmOverloads constructor( return result } + /** @suppress */ companion object { fun parsePiecesString(s: String, p: PlayerColor): ArrayList { val l = ArrayList() diff --git a/.old-plugins/blokus_2020/Move.kt b/.old-plugins/hive_2020/Move.kt similarity index 100% rename from .old-plugins/blokus_2020/Move.kt rename to .old-plugins/hive_2020/Move.kt diff --git a/.old-plugins/blokus_2020/Piece.kt b/.old-plugins/hive_2020/Piece.kt similarity index 100% rename from .old-plugins/blokus_2020/Piece.kt rename to .old-plugins/hive_2020/Piece.kt diff --git a/.old-plugins/blokus_2020/util/Configuration.kt b/.old-plugins/hive_2020/util/Configuration.kt similarity index 100% rename from .old-plugins/blokus_2020/util/Configuration.kt rename to .old-plugins/hive_2020/util/Configuration.kt diff --git a/.old-plugins/blokus_2020/util/Constants.kt b/.old-plugins/hive_2020/util/Constants.kt similarity index 100% rename from .old-plugins/blokus_2020/util/Constants.kt rename to .old-plugins/hive_2020/util/Constants.kt diff --git a/.old-plugins/blokus_2020/util/CubeCoordinates.kt b/.old-plugins/hive_2020/util/CubeCoordinates.kt similarity index 100% rename from .old-plugins/blokus_2020/util/CubeCoordinates.kt rename to .old-plugins/hive_2020/util/CubeCoordinates.kt diff --git a/.old-plugins/blokus_2020/util/Direction.kt b/.old-plugins/hive_2020/util/Direction.kt similarity index 100% rename from .old-plugins/blokus_2020/util/Direction.kt rename to .old-plugins/hive_2020/util/Direction.kt diff --git a/.old-plugins/blokus_2020/util/GameRuleLogic.kt b/.old-plugins/hive_2020/util/GameRuleLogic.kt similarity index 100% rename from .old-plugins/blokus_2020/util/GameRuleLogic.kt rename to .old-plugins/hive_2020/util/GameRuleLogic.kt diff --git a/.old-plugins/piranhas_2019/shared/sc/plugin2019/FieldState.kt b/.old-plugins/piranhas_2019/shared/sc/plugin2019/FieldState.kt index 742e03a81..2bae40e71 100644 --- a/.old-plugins/piranhas_2019/shared/sc/plugin2019/FieldState.kt +++ b/.old-plugins/piranhas_2019/shared/sc/plugin2019/FieldState.kt @@ -22,6 +22,7 @@ enum class FieldState { else -> ' ' } + /** @suppress */ companion object { @JvmStatic fun from(color: PlayerColor): FieldState { diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..68426b877 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,168 @@ +# Repository Guidelines + +## Project Specifics +### Structure & Modules +This repository is a multi-module Gradle project for the Software Challenge backend. + +- `sdk/`: shared protocol, framework, networking, and player/server API classes. +- `server/`: game server application (`sc.server.Application`). +- `player/`: default player template and packaging tasks. +- `games/`: yearly plugins mapped as modules (`plugin2023`..`plugin2026`). +- `helpers/test-client` and `helpers/test-config`: integration tooling and shared test setup. +- `gradle/` and `gradlew`: build logic and wrapper. + +### Build, Test & Development Commands +Use the Gradle wrapper from repo root. +Always use the default Gradle user home from the environment; do not pass a custom home (no `-g ...`, no overridden `GRADLE_USER_HOME`). + +- `./gradlew build`: compile, test, and create distribution bundles. +- `./gradlew check`: run verification tasks across modules. +- `./gradlew test`: run unit tests. +- `./gradlew integrationTest`: run end-to-end game/test-client checks. +- `./gradlew :server:run`: start server from source. +- `./gradlew :player:run`: start default player. +- `./gradlew bundle`: assemble release ZIP artifacts. + +For module-scoped work, use qualified tasks (example: `./gradlew :plugin2026:test`). + +### Coding Style & Naming +Primary languages are Kotlin and Java, but all new code must be written in Kotlin. + +- Follow existing module style and keep changes consistent with surrounding code. +- Use clear domain names (`GameState`, `Move`, `...Request`, `...Response`). +- Keep package naming aligned with module/year conventions (example: `sc.plugin2026`). +- Prefer descriptive test names ending in `Test` (example: `GameRuleLogicTest.kt`). +- No repository-wide formatter is enforced in Gradle; run IDE reformat with project defaults. + +## General Workflow Policies +- Follow commit/changelog/test/lint/refactor/process rules in this file for all AI-assisted changes. +- Keep edits focused, reviewable, and behavior-safe unless intentional behavioral changes are requested. + +### Testing & Verification +- Tests are mainly Kotest (`WordSpec` preferred, `FunSpec` for algorithmic cases) on JUnit 5; some legacy JUnit 5 tests remain. +- Place new tests under `src/test/kotlin`. +- Name test files `*Test.kt`. +- Write tests before each change except minor cosmetic-only changes. +- Verify tests after each change. +- For server-player interaction or protocol changes, run `./gradlew integrationTest`. +- Before adjusting failing tests, evaluate whether failures indicate regressions versus intentional behavior changes. +- Prefer behavior/outcome tests over implementation-detail tests. + +### Commit & PR Discipline +- Always commit every completed change. +- Use commit messages in format `type(scope): summary` (example: `fix(plugin26): avoid transient hash checks for observers`). +- Enable local hooks: `git config core.hooksPath .dev/githooks`. +- Commit format is enforced by hook: `type(scope): summary`. +- Allowed types are: `fix`, `feat`, `enhance`, `docs`, `style`, `refactor`, `test`, `build`, `rework`, `release`, `revert`. +- Prefer branch names like `feat/server/login` or `chore/gradle/release-fix`. +- For PRs, include: concise description, linked issue(s), test evidence (commands/results), and logs/screenshots when behavior/output changed. +- Use rebase merge when commits are independently valid; otherwise squash merge with PR title matching final commit message. +- Make atomic commits; if a commit only fixes the previous local commit, squash before handoff. + +### Changelog & Refactoring +- Keep `CHANGELOG.md` updated for notable user-visible behavior changes. +- Keep an `Unreleased` section at top; move entries into versioned section on release. +- Use semantic version sections and ISO dates (`YYYY-MM-DD`). +- After major milestones, run a focused refactor pass for duplication, consistency, and complexity. +- Prefer separate `refactor:` commits for cleanup when functionality does not depend on refactor changes. + +## Agent-Specific Instructions +### Machine-Readable Policy +```yaml +agent_policy: + source_of_truth: + - CONTRIBUTING.md + - GUIDELINES.md + - settings.gradle.kts + setup: + git_hooks: "git config core.hooksPath .dev/githooks" + project: + build_system: gradle + new_code_language: kotlin + modules: + - sdk + - server + - player + - plugin2023 + - plugin2024 + - plugin2025 + - plugin2026 + - test-client + - test-config + verification: + default: "./gradlew test" + interaction_or_protocol: "./gradlew integrationTest" + test_file_patterns: + - "src/test/kotlin/**/*Test.kt" + commit: + format: "type(scope): summary" + scopes_file: ".dev/scopes.txt" + merge: + rebase_if_independent: true + squash_if_experimental: true + constraints: + - "Do not create new Java source files." + - "Do not create new Java test files." +``` + +### Prompt Effort Modes +```yaml +prompt_effort: + quick: + - "Minimize testing/refactoring; run focused checks only." + - "Defer broader cleanup unless explicitly requested." + long: + - "Run thorough workflow with broader verification and edge-case checks." + - "Include refactor/debt review where appropriate." + default: + - "Choose balanced effort based on task complexity and risk." +``` + +### Special Commands +```yaml +special_commands: + squash: + - "Inspect recent commits and propose sensible squashes for repetitive fixups." + - "Do not squash unrelated functional changes." + - "Rewrite only unpushed local history unless explicitly instructed." + push: + - "List unpushed commits: git log origin/..HEAD --oneline" + - "Provide one high-level summary across unpushed commits." + - "Update changelog/version/tag only when explicitly requested." + - "Ask explicit confirmation before pushing branch or tags." +``` + +### Assistant Response Formatting +```yaml +assistant_response_formatting: + summary: + - "Keep summaries compact and scannable." + - "Prefer single-line status items when content fits." + - "Avoid repetitive progress boilerplate." + commit_reporting_format: "✅ : " + status_indicators: + success: "✅" + warning: "⚠️" + failure: "❌" + diagnostics: "🔍" + verification: "🧪" + style: + - "Prefer concise bullets over verbose prose." + - "Use status indicators consistently." +``` + +### Plans & Artifacts +```yaml +planning: + when_user_requests_plan: + - "Write plan files to plans/ at repo root." + - "Use descriptive kebab-case filenames." + - "Do not commit plan files unless explicitly requested." + after_implementation: + - "Delete the corresponding plan file after implementation is complete." + - "Before deleting untracked text artifacts (for example files in plans/), run git add on them once without committing so they are recoverable via index/reflog if deletion was a mistake." +``` + +### AGENTS Maintenance +- When the user gives new standing workflow/process instructions, update `AGENTS.md` in the same session. +- Keep updates concise and place them in the most relevant existing section (create a new section only when needed). diff --git a/GUIDELINES.md b/GUIDELINES.md index cffcda96e..d89c75efa 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -2,6 +2,14 @@ This document captures development standards and architecture decisions of this project as a point of reference. +## Gradle + +We build everything with Gradle, nesting projects if needed. +Current toolchain: Gradle 9.3, Java 25 toolchain (bytecode target 8), Kotlin 2.3, Dokka 2.1. + +Dokka v2 generates Javadoc per project (Javadoc is still alpha and does not support multi-project aggregation), +so we generate per-module docs and collect them during bundling. + ## Testing Unsere Unittests nutzen das [Kotest-Framework](https://kotest.io) @@ -58,7 +66,7 @@ annotate the serialized fields with a concrete type instead. Ideally these fields should then be private with generically typed getters as to not expose the implementation details internally. -## Cloning +## Object Cloning Relevant discussion: https://github.com/software-challenge/backend/pull/148 @@ -80,10 +88,11 @@ We recently introduced the use of the to make some year-specific implementations from the plugin accessible in the sdk and server. Currently there are two interfaces, -[IGamePlugin](sdk/src/server-api/sc/api/plugins/IGamePlugin.java) -and [XStreamProvider]( sdk/src/server-api/sc/networking/XStreamProvider.kt), -which are implemented in the plugin and then loaded through a ServiceLoader. -The information which implementations to use resides in [resources/META-INF/services](plugin/src/resources/META-INF/services). +[IGamePlugin](sdk/src/server-api/sc/api/plugins/IGamePlugin.java) +and [XStreamProvider](sdk/src/server-api/sc/networking/XStreamProvider.kt), +which are implemented in the yearly game plugin and then loaded through a ServiceLoader. +The information which implementations to use resides in each plugin module under +`src/main/resources/META-INF/services` (for example `games/piranhas/src/main/resources/META-INF/services`). ## Networking Protocol Classes @@ -109,4 +118,3 @@ and is then wrapped in a [RoomPacket](sdk/src/server-api/sc/protocol/room/RoomPa The package contains a few standard messages, but most will be implemented in the corresponding plugin. - diff --git a/README.md b/README.md index 880b8e58f..39134ac4a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ # Software-Challenge Logo Spiel-Infrastruktur der Software-Challenge Germany ![.github/workflows/gradle.yml](https://github.com/software-challenge/backend/workflows/.github/workflows/gradle.yml/badge.svg) In diesem Repository befindet sich -der Spiel-Infrastruktur der [Software-Challenge](https://www.software-challenge.de), +die Spiel-Infrastruktur der [Software-Challenge](https://www.software-challenge.de), ein Programmierwettbewerb für Schüler. Dabei wird für ein jährlich wechselndes Spiel eine künstliche Intelligenz entwickelt, die sich dann in Duellen gegen andere durchsetzen muss. Der Code teilt sich auf in gemeinsames SDK, Server, Spieler(vorlage) und Spiel-Plugins. -| Ordner | Beschreibung | -|---------|----------------------------------------------------------------------------------| -| helpers | Zusätzliche Tools (aktuell nur der TestClient) | -| player | Spielervorlage | -| plugin | Plugin des aktuellen Jahres (Spiel-XML, Tests, ...) | -| server | Spielserver | -| sdk | Projektübergreifend verwendete Klassen (XML Networking, Protokoll, Replays, ...) | +| Ordner | Beschreibung | +|----------|-------------------------------------------------------------------| +| games | Spiel-Plugins pro Jahr | +| helpers | Zusätzliche Tools (`test-client`, `test-config`) | +| player | Spielervorlage | +| server | Spielserver | +| sdk | Projektübergreifend verwendete Klassen | +| gradle | Build-Logik, Konventionen und Wrapper-Konfiguration | Die Struktur der Plugins wird aktuell im Einklang mit der GUI neu strukturiert. diff --git a/plugin2025/build.gradle.kts b/games/hui/build.gradle.kts similarity index 52% rename from plugin2025/build.gradle.kts rename to games/hui/build.gradle.kts index a76ff84d4..cc61adbd4 100644 --- a/plugin2025/build.gradle.kts +++ b/games/hui/build.gradle.kts @@ -1,11 +1,9 @@ -val game: String by project - dependencies { api(project(":sdk")) } tasks { jar { - archiveBaseName.set(game) + archiveBaseName.set("hui") } } diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Advance.kt b/games/hui/src/main/kotlin/sc/plugin2025/Advance.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/Advance.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Advance.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Board.kt b/games/hui/src/main/kotlin/sc/plugin2025/Board.kt similarity index 99% rename from plugin2025/src/main/kotlin/sc/plugin2025/Board.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Board.kt index 339fd7c12..e4579e8d1 100644 --- a/plugin2025/src/main/kotlin/sc/plugin2025/Board.kt +++ b/games/hui/src/main/kotlin/sc/plugin2025/Board.kt @@ -45,6 +45,7 @@ class Board( override fun hashCode(): Int = track.contentHashCode() + /** @suppress */ companion object { private fun shuffledFields(vararg fields: Field) = fields.asList().shuffled() diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Card.kt b/games/hui/src/main/kotlin/sc/plugin2025/Card.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/Card.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Card.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/EatSalad.kt b/games/hui/src/main/kotlin/sc/plugin2025/EatSalad.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/EatSalad.kt rename to games/hui/src/main/kotlin/sc/plugin2025/EatSalad.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt b/games/hui/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt rename to games/hui/src/main/kotlin/sc/plugin2025/ExchangeCarrots.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/FallBack.kt b/games/hui/src/main/kotlin/sc/plugin2025/FallBack.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/FallBack.kt rename to games/hui/src/main/kotlin/sc/plugin2025/FallBack.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Field.kt b/games/hui/src/main/kotlin/sc/plugin2025/Field.kt similarity index 98% rename from plugin2025/src/main/kotlin/sc/plugin2025/Field.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Field.kt index 4215b5659..a2a70019a 100644 --- a/plugin2025/src/main/kotlin/sc/plugin2025/Field.kt +++ b/games/hui/src/main/kotlin/sc/plugin2025/Field.kt @@ -27,6 +27,7 @@ enum class Field(val short: String, val unicode: String = short) { /** Das Startfeld */ START("0", "▶"); + /** @suppress */ companion object { val POSITION_3 = CARROTS val POSITION_4 = MARKET diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt b/games/hui/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt rename to games/hui/src/main/kotlin/sc/plugin2025/GameRuleLogic.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/GameState.kt b/games/hui/src/main/kotlin/sc/plugin2025/GameState.kt similarity index 97% rename from plugin2025/src/main/kotlin/sc/plugin2025/GameState.kt rename to games/hui/src/main/kotlin/sc/plugin2025/GameState.kt index b262c6875..916f4d165 100644 --- a/plugin2025/src/main/kotlin/sc/plugin2025/GameState.kt +++ b/games/hui/src/main/kotlin/sc/plugin2025/GameState.kt @@ -17,14 +17,14 @@ import kotlin.math.pow import kotlin.math.sqrt /** - * The GameState class represents the current state of the game. + * Die Klasse GameState repräsentiert den aktuellen Zustand des Spiels. * - * It holds all the information about the current round, - * to provide all information needed to make the next move. + * Sie enthält alle Informationen über die aktuelle Runde, + * um alle benötigten Daten für den nächsten Zug bereitzustellen. * - * @property board The current game board. - * @property turn The number of turns already made in the game. - * @property lastMove The last move made in the game. + * @property board Das aktuelle Spielfeld. + * @property turn Die Anzahl der bereits gespielten Züge. + * @property lastMove Der zuletzt gespielte Zug. */ @XStreamAlias(value = "state") data class GameState @JvmOverloads constructor( @@ -326,4 +326,4 @@ data class GameState @JvmOverloads constructor( fun succeedsState(other: GameState) = other.turn + 1 == turn || other.turn + 2 == turn -} \ No newline at end of file +} diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Hare.kt b/games/hui/src/main/kotlin/sc/plugin2025/Hare.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/Hare.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Hare.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt b/games/hui/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt rename to games/hui/src/main/kotlin/sc/plugin2025/HuIMoveMistake.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/Move.kt b/games/hui/src/main/kotlin/sc/plugin2025/Move.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/Move.kt rename to games/hui/src/main/kotlin/sc/plugin2025/Move.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/util/Constants.kt b/games/hui/src/main/kotlin/sc/plugin2025/util/Constants.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/util/Constants.kt rename to games/hui/src/main/kotlin/sc/plugin2025/util/Constants.kt diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt b/games/hui/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt similarity index 98% rename from plugin2025/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt rename to games/hui/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt index 8bc2d0965..6f7ea8690 100644 --- a/plugin2025/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt +++ b/games/hui/src/main/kotlin/sc/plugin2025/util/GamePlugin.kt @@ -17,6 +17,7 @@ enum class HuIWinReason(override val message: String, override val isRegular: Bo } class GamePlugin: IGamePlugin { + /** @suppress */ companion object { const val PLUGIN_ID = "swc_2025_hase_und_igel" val scoreDefinition: ScoreDefinition = diff --git a/plugin2025/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt b/games/hui/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt similarity index 100% rename from plugin2025/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt rename to games/hui/src/main/kotlin/sc/plugin2025/util/XStreamClasses.kt diff --git a/plugin2025/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin b/games/hui/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin similarity index 100% rename from plugin2025/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin rename to games/hui/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin diff --git a/plugin2025/src/main/resources/META-INF/services/sc.networking.XStreamProvider b/games/hui/src/main/resources/META-INF/services/sc.networking.XStreamProvider similarity index 100% rename from plugin2025/src/main/resources/META-INF/services/sc.networking.XStreamProvider rename to games/hui/src/main/resources/META-INF/services/sc.networking.XStreamProvider diff --git a/plugin2025/src/test/kotlin/sc/GamePlayTest.kt b/games/hui/src/test/kotlin/sc/GamePlayTest.kt similarity index 100% rename from plugin2025/src/test/kotlin/sc/GamePlayTest.kt rename to games/hui/src/test/kotlin/sc/GamePlayTest.kt diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/CardTest.kt b/games/hui/src/test/kotlin/sc/plugin2025/CardTest.kt similarity index 100% rename from plugin2025/src/test/kotlin/sc/plugin2025/CardTest.kt rename to games/hui/src/test/kotlin/sc/plugin2025/CardTest.kt diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/GamePlayTest.java b/games/hui/src/test/kotlin/sc/plugin2025/GamePlayTest.java similarity index 100% rename from plugin2025/src/test/kotlin/sc/plugin2025/GamePlayTest.java rename to games/hui/src/test/kotlin/sc/plugin2025/GamePlayTest.java diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/GameResultTest.kt b/games/hui/src/test/kotlin/sc/plugin2025/GameResultTest.kt similarity index 100% rename from plugin2025/src/test/kotlin/sc/plugin2025/GameResultTest.kt rename to games/hui/src/test/kotlin/sc/plugin2025/GameResultTest.kt diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt b/games/hui/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt similarity index 100% rename from plugin2025/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt rename to games/hui/src/test/kotlin/sc/plugin2025/GameRuleLogicTest.kt diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/GameStateTest.kt b/games/hui/src/test/kotlin/sc/plugin2025/GameStateTest.kt similarity index 100% rename from plugin2025/src/test/kotlin/sc/plugin2025/GameStateTest.kt rename to games/hui/src/test/kotlin/sc/plugin2025/GameStateTest.kt diff --git a/plugin2025/src/test/kotlin/sc/plugin2025/MoveTest.kt b/games/hui/src/test/kotlin/sc/plugin2025/MoveTest.kt similarity index 99% rename from plugin2025/src/test/kotlin/sc/plugin2025/MoveTest.kt rename to games/hui/src/test/kotlin/sc/plugin2025/MoveTest.kt index 7a613e802..7e07c67f0 100644 --- a/plugin2025/src/test/kotlin/sc/plugin2025/MoveTest.kt +++ b/games/hui/src/test/kotlin/sc/plugin2025/MoveTest.kt @@ -208,7 +208,7 @@ class MoveTest: WordSpec({ state.getSensibleMoves() shouldBe listOf(FallBack, ExchangeCarrots(10)) } - "produce concicse XML" { + "produce concise XML" { ExchangeCarrots(-10) shouldSerializeTo "" } } diff --git a/plugin/build.gradle.kts b/games/mississippi-queen/build.gradle.kts similarity index 100% rename from plugin/build.gradle.kts rename to games/mississippi-queen/build.gradle.kts diff --git a/plugin/src/main/kotlin/sc/plugin2024/Action.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Action.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/Action.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Action.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/Board.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Board.kt similarity index 99% rename from plugin/src/main/kotlin/sc/plugin2024/Board.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Board.kt index 8b4f3579f..b8d62ef54 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Board.kt +++ b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Board.kt @@ -22,6 +22,7 @@ data class Board( @XStreamAsAttribute var nextDirection: CubeDirection = segments.lastOrNull()?.direction ?: CubeDirection.RIGHT, ): IBoard { + /** @suppress */ companion object { val logger: Logger = LoggerFactory.getLogger(this::class.java) } diff --git a/plugin/src/main/kotlin/sc/plugin2024/Field.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Field.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/Field.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Field.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/GameState.kt similarity index 98% rename from plugin/src/main/kotlin/sc/plugin2024/GameState.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/GameState.kt index 63449538b..28df7de09 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/GameState.kt +++ b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/GameState.kt @@ -19,14 +19,14 @@ import sc.shared.WinCondition import kotlin.math.absoluteValue /** - * The GameState class represents the current state of the game. + * Die Klasse GameState repräsentiert den aktuellen Zustand des Spiels. * - * It holds all the information about the current round, which is used - * to calculate the next move. + * Sie enthält alle Informationen über die aktuelle Runde, + * die zur Berechnung des nächsten Zuges verwendet werden. * - * @property board The current game board. - * @property turn The number of turns already made in the game. - * @property lastMove The last move made in the game. + * @property board Das aktuelle Spielfeld. + * @property turn Die Anzahl der bereits gespielten Züge. + * @property lastMove Der zuletzt gespielte Zug. */ @XStreamAlias(value = "state") data class GameState @JvmOverloads constructor( diff --git a/plugin/src/main/kotlin/sc/plugin2024/Move.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Move.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/Move.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Move.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Segment.kt similarity index 99% rename from plugin/src/main/kotlin/sc/plugin2024/Segment.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Segment.kt index 98180cf1c..23e7c282b 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/Segment.kt +++ b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Segment.kt @@ -90,6 +90,7 @@ data class Segment( return result } + /** @suppress */ companion object { fun inDirection(previousCenter: CubeCoordinates, direction: CubeDirection, fields: SegmentFields) = Segment(direction, previousCenter + direction.vector * MQConstants.SEGMENT_FIELDS_WIDTH, fields) diff --git a/plugin/src/main/kotlin/sc/plugin2024/Ship.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/Ship.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/Ship.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/Ship.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/actions/Accelerate.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Accelerate.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/actions/Accelerate.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Accelerate.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Advance.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/actions/Advance.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Advance.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/actions/Push.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Push.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/actions/Push.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Push.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/actions/Turn.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Turn.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/actions/Turn.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/actions/Turn.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/AccelerationProblem.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/AdvanceProblem.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/AdvanceProblem.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/mistake/AdvanceProblem.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/AdvanceProblem.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/MQMoveMistake.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/PushProblem.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/PushProblem.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/mistake/PushProblem.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/PushProblem.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/mistake/TurnProblem.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/TurnProblem.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/mistake/TurnProblem.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/mistake/TurnProblem.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/BoardConverter.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/BoardConverter.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/util/BoardConverter.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/BoardConverter.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/Constants.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/util/Constants.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/Constants.kt diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt similarity index 98% rename from plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt index 771bfe48f..9ac09872a 100644 --- a/plugin/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt +++ b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/GamePlugin.kt @@ -19,6 +19,7 @@ enum class MQWinReason(override val message: String, override val isRegular: Boo } class GamePlugin: IGamePlugin { + /** @suppress */ companion object { const val PLUGIN_ID = "swc_2024_mississippi_queen" val scoreDefinition: ScoreDefinition = diff --git a/plugin/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt b/games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt rename to games/mississippi-queen/src/main/kotlin/sc/plugin2024/util/XStreamClasses.kt diff --git a/plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin b/games/mississippi-queen/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin similarity index 100% rename from plugin/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin rename to games/mississippi-queen/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin diff --git a/plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider b/games/mississippi-queen/src/main/resources/META-INF/services/sc.networking.XStreamProvider similarity index 100% rename from plugin/src/main/resources/META-INF/services/sc.networking.XStreamProvider rename to games/mississippi-queen/src/main/resources/META-INF/services/sc.networking.XStreamProvider diff --git a/plugin/src/test/kotlin/sc/GamePlayTest.kt b/games/mississippi-queen/src/test/kotlin/sc/GamePlayTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/GamePlayTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/GamePlayTest.kt diff --git a/plugin/src/test/kotlin/sc/Util.kt b/games/mississippi-queen/src/test/kotlin/sc/Util.kt similarity index 100% rename from plugin/src/test/kotlin/sc/Util.kt rename to games/mississippi-queen/src/test/kotlin/sc/Util.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/AccelerationTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/AccelerationTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/AccelerationTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/AdvanceTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/AdvanceTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/AdvanceTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/AdvanceTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/BoardTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/BoardTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/BoardTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/GameResultTest.kt similarity index 99% rename from plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/GameResultTest.kt index 83a9c058e..46d38865e 100644 --- a/plugin/src/test/kotlin/sc/plugin2024/GameResultTest.kt +++ b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/GameResultTest.kt @@ -130,6 +130,7 @@ class GameResultTest: WordSpec({ } game.currentState.performMoveDirectly(Move(Accelerate(-1), Turn(CubeDirection.DOWN_RIGHT), Advance(1))) "work with regular result" { + game.players.forEach { it.violation = null } game.getResult() shouldSerializeTo """ @@ -224,4 +225,4 @@ class GameResultTest: WordSpec({ "WinConditions" should { // TODO } -}) \ No newline at end of file +}) diff --git a/plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/GameStateTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/GameStateTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/GameStateTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/MoveTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/MoveTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/MoveTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/PushTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/PushTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/PushTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/PushTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/SegmentTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/SegmentTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/SegmentTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/ShipTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/ShipTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/ShipTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2024/TurnTest.kt b/games/mississippi-queen/src/test/kotlin/sc/plugin2024/TurnTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2024/TurnTest.kt rename to games/mississippi-queen/src/test/kotlin/sc/plugin2024/TurnTest.kt diff --git a/games/mississippi-queen/src/test/resources/META-INF/services/sc.networking.XStreamProvider b/games/mississippi-queen/src/test/resources/META-INF/services/sc.networking.XStreamProvider new file mode 100644 index 000000000..804559dc4 --- /dev/null +++ b/games/mississippi-queen/src/test/resources/META-INF/services/sc.networking.XStreamProvider @@ -0,0 +1 @@ +sc.plugin2024.util.XStreamClasses diff --git a/plugin2026/build.gradle.kts b/games/penguins/build.gradle.kts similarity index 52% rename from plugin2026/build.gradle.kts rename to games/penguins/build.gradle.kts index a76ff84d4..cb2929484 100644 --- a/plugin2026/build.gradle.kts +++ b/games/penguins/build.gradle.kts @@ -1,11 +1,9 @@ -val game: String by project - dependencies { api(project(":sdk")) } tasks { jar { - archiveBaseName.set(game) + archiveBaseName.set("penguins") } } diff --git a/plugin/src/main/kotlin/sc/plugin2023/Board.kt b/games/penguins/src/main/kotlin/sc/plugin2023/Board.kt similarity index 99% rename from plugin/src/main/kotlin/sc/plugin2023/Board.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/Board.kt index 2e3f81b8e..103c6ad2b 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/Board.kt +++ b/games/penguins/src/main/kotlin/sc/plugin2023/Board.kt @@ -66,6 +66,7 @@ class Board( override fun clone(): Board = Board(this) + /** @suppress */ companion object { /** Generiert ein neues Spielfeld mit zufällig auf dem Spielbrett verteilten Fischen. */ private fun generateFields(seed: Int = Random.nextInt()): MutableTwoDBoard { diff --git a/plugin/src/main/kotlin/sc/plugin2023/Field.kt b/games/penguins/src/main/kotlin/sc/plugin2023/Field.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/Field.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/Field.kt diff --git a/plugin/src/main/kotlin/sc/plugin2023/GameState.kt b/games/penguins/src/main/kotlin/sc/plugin2023/GameState.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/GameState.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/GameState.kt diff --git a/plugin/src/main/kotlin/sc/plugin2023/Move.kt b/games/penguins/src/main/kotlin/sc/plugin2023/Move.kt similarity index 98% rename from plugin/src/main/kotlin/sc/plugin2023/Move.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/Move.kt index 80a03faf6..58cd26a98 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/Move.kt +++ b/games/penguins/src/main/kotlin/sc/plugin2023/Move.kt @@ -25,6 +25,7 @@ data class Move( override fun toString(): String = from?.let { "Schlittern $from zu $to" } ?: "Setze Pinguin auf $to" + /** @suppress */ companion object { @JvmStatic fun run(start: Coordinates, delta: Vector): Move = diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt b/games/penguins/src/main/kotlin/sc/plugin2023/util/Constants.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/util/Constants.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/util/Constants.kt diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/FieldConverter.kt b/games/penguins/src/main/kotlin/sc/plugin2023/util/FieldConverter.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/util/FieldConverter.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/util/FieldConverter.kt diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt b/games/penguins/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt similarity index 98% rename from plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt index 8e51b4807..c5bca4711 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt +++ b/games/penguins/src/main/kotlin/sc/plugin2023/util/GamePlugin.kt @@ -12,6 +12,7 @@ import sc.shared.ScoreFragment import sc.shared.WinReason class GamePlugin: IGamePlugin { + /** @suppress */ companion object { const val PLUGIN_ID = "swc_2023_penguins" val scoreDefinition: ScoreDefinition = diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/PenguinsMoveMistake.kt b/games/penguins/src/main/kotlin/sc/plugin2023/util/PenguinsMoveMistake.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/util/PenguinsMoveMistake.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/util/PenguinsMoveMistake.kt diff --git a/plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt b/games/penguins/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt similarity index 100% rename from plugin/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt rename to games/penguins/src/main/kotlin/sc/plugin2023/util/XStreamClasses.kt diff --git a/games/penguins/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin b/games/penguins/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin new file mode 100644 index 000000000..48af46cb6 --- /dev/null +++ b/games/penguins/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin @@ -0,0 +1 @@ +sc.plugin2023.util.GamePlugin diff --git a/plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider b/games/penguins/src/main/resources/META-INF/services/sc.networking.XStreamProvider similarity index 50% rename from plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider rename to games/penguins/src/main/resources/META-INF/services/sc.networking.XStreamProvider index e22a2a179..6b8f0a5c7 100644 --- a/plugin/src/test/resources/META-INF/services/sc.networking.XStreamProvider +++ b/games/penguins/src/main/resources/META-INF/services/sc.networking.XStreamProvider @@ -1,2 +1 @@ sc.plugin2023.util.XStreamClasses -sc.plugin2024.util.XStreamClasses \ No newline at end of file diff --git a/plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt b/games/penguins/src/test/kotlin/sc/plugin2023/BoardTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2023/BoardTest.kt rename to games/penguins/src/test/kotlin/sc/plugin2023/BoardTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt b/games/penguins/src/test/kotlin/sc/plugin2023/GameStateTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2023/GameStateTest.kt rename to games/penguins/src/test/kotlin/sc/plugin2023/GameStateTest.kt diff --git a/plugin/src/test/kotlin/sc/plugin2023/MoveTest.kt b/games/penguins/src/test/kotlin/sc/plugin2023/MoveTest.kt similarity index 100% rename from plugin/src/test/kotlin/sc/plugin2023/MoveTest.kt rename to games/penguins/src/test/kotlin/sc/plugin2023/MoveTest.kt diff --git a/games/penguins/src/test/resources/META-INF/services/sc.networking.XStreamProvider b/games/penguins/src/test/resources/META-INF/services/sc.networking.XStreamProvider new file mode 100644 index 000000000..6b8f0a5c7 --- /dev/null +++ b/games/penguins/src/test/resources/META-INF/services/sc.networking.XStreamProvider @@ -0,0 +1 @@ +sc.plugin2023.util.XStreamClasses diff --git a/games/piranhas/build.gradle.kts b/games/piranhas/build.gradle.kts new file mode 100644 index 000000000..df3fa6ac6 --- /dev/null +++ b/games/piranhas/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + api(project(":sdk")) +} + +tasks { + jar { + archiveBaseName.set("piranhas") + } +} diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/Board.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/Board.kt similarity index 84% rename from plugin2026/src/main/kotlin/sc/plugin2026/Board.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/Board.kt index 4a4ac162d..de29183f9 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/Board.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/Board.kt @@ -2,13 +2,12 @@ package sc.plugin2026 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamImplicit +import com.thoughtworks.xstream.annotations.XStreamOmitField import sc.api.plugins.* import sc.framework.deepCopy import sc.plugin2026.util.PiranhaConstants import kotlin.random.Random -val line = "-".repeat(PiranhaConstants.BOARD_LENGTH * 2 + 2) - /** Spielbrett für Piranhas mit [PiranhaConstants.BOARD_LENGTH]² Feldern. */ @XStreamAlias(value = "board") class Board( @@ -16,6 +15,8 @@ class Board( override val gameField: MutableTwoDBoard = randomFields() ): RectangularBoard(), IBoard { + /** Gibt eine kompakte String-Darstellung des Spielfelds mit Koordinaten + * und einer zweibuchstabigen Darstellung der Feldzustände zurück. */ override fun toString() = "Board " + gameField.withIndex().joinToString(" ", "[", "]") { row -> row.value.withIndex().joinToString(", ", prefix = "[", postfix = "]") { @@ -23,6 +24,9 @@ class Board( } } + @Transient + private val line = "-".repeat(PiranhaConstants.BOARD_LENGTH * 2 + 2) + /** Gibt eine visuell formatierte Darstellung des Spielfelds mit ASCII-Rahmen zurück. */ fun prettyString(): String { val map = StringBuilder(line) gameField.forEach { row -> @@ -35,20 +39,25 @@ class Board( return map.toString() } + /** Erstellt eine tiefe Kopie dieses [Board] durch Klonen der zugrunde liegenden Felder. */ override fun clone(): Board { //println("Cloning with ${gameField::class.java}: $this") return Board(gameField.deepCopy()) } + /** Gibt das [Team] eines Fisches an [pos] zurück, + * oder `null` falls sich kein Fisch auf dem Feld befindet. */ fun getTeam(pos: Coordinates): Team? = this[pos].team + /** Gibt eine Zuordnung aller von [team] belegten Felder zu deren Fischgrößen zurück. */ fun fieldsForTeam(team: ITeam): Map = filterValues { field -> field.team == team } .mapValues { (_, field) -> field.size } + /** @suppress */ companion object { - /** Erstellt ein zufälliges Spielbrett. */ + /** Erstellt ein zufälliges Spielbrett. */ fun randomFields( obstacleCount: Int = PiranhaConstants.NUM_OBSTACLES, random: Random = Random.Default, diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/FieldState.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/FieldState.kt similarity index 67% rename from plugin2026/src/main/kotlin/sc/plugin2026/FieldState.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/FieldState.kt index 02547d1a1..40292f874 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/FieldState.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/FieldState.kt @@ -5,22 +5,38 @@ import sc.api.plugins.IField import sc.api.plugins.Team import sc.framework.DeepCloneable +/** + * Feldzustand mit Fischgröße bzw. Spezialfeld. + * + * @property size Fischgröße oder 0 für leere/gesperrte Felder. + */ @XStreamAlias("field") enum class FieldState(val size: Int): IField, DeepCloneable { + /** Team 1, kleiner Fisch. */ ONE_S(1), + /** Team 1, mittlerer Fisch. */ ONE_M(2), + /** Team 1, grosser Fisch. */ ONE_L(3), + /** Team 2, kleiner Fisch. */ TWO_S(1), + /** Team 2, mittlerer Fisch. */ TWO_M(2), + /** Team 2, grosser Fisch. */ TWO_L(3), + /** Gesperrtes Feld (Krake). */ SQUID(0), + /** Leeres Feld. */ EMPTY(0); + /** Liefert eine Kopie dieses Feldzustands. */ override fun deepCopy(): FieldState = this + /** Gibt an, ob das Feld komplett leer ist. */ override val isEmpty: Boolean get() = this == EMPTY + /** Zugehöriges [Team] oder null, falls kein Fisch vorhanden ist. */ val team: Team? get() = when(this) { ONE_S, ONE_M, ONE_L -> Team.ONE @@ -28,6 +44,7 @@ enum class FieldState(val size: Int): IField, DeepCloneable { SQUID, EMPTY -> null } + /** Liefert eine Textrepräsentation des Feldes. */ override fun toString() = when(this) { SQUID -> "Krake" @@ -35,6 +52,7 @@ enum class FieldState(val size: Int): IField, DeepCloneable { else -> team?.color.toString() + size.toString() } + /** Gibt eine zweibuchstabige Darstellung für Textkarten zurück. */ fun asLetters() = when(this) { SQUID -> "X " @@ -42,7 +60,9 @@ enum class FieldState(val size: Int): IField, DeepCloneable { else -> team?.letter.toString() + size.toString() } + /** @suppress */ companion object { + /** Erstellt den passenden [FieldState] für [team] und Fischgröße [size]. */ fun from(team: Team, size: Int): FieldState = when(team) { Team.ONE -> when(size) { diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/GameState.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/GameState.kt similarity index 72% rename from plugin2026/src/main/kotlin/sc/plugin2026/GameState.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/GameState.kt index a14a05509..019d2af34 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/GameState.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/GameState.kt @@ -15,14 +15,17 @@ import sc.shared.WinCondition import sc.shared.WinReasonTie /** - * The GameState class represents the current state of the game. + * Die Klasse GameState repräsentiert den aktuellen Zustand des Spiels. * - * It holds all the information about the current round, - * to provide all information needed to make the next move. + * Sie enthält alle Informationen über die aktuelle Runde, + * um alle benötigten Daten für den nächsten Zug bereitzustellen. * - * @property board The current game board. - * @property turn The number of turns already made in the game. - * @property lastMove The last move made in the game. + * @param turn Die Anzahl der bereits gespielten Züge. + * @param lastMove Der zuletzt gespielte Zug. + * @param board Das aktuelle Spielfeld. + * @property board Das aktuelle Spielfeld. + * @property turn Die Anzahl der bereits gespielten Züge. + * @property lastMove Der zuletzt gespielte Zug. */ @XStreamAlias(value = "state") data class GameState @JvmOverloads constructor( @@ -34,14 +37,17 @@ data class GameState @JvmOverloads constructor( override val board: Board = Board(), ): TwoPlayerGameState(Team.ONE) { + /** Berechnet die Punktwerte für das angegebene [team]. */ override fun getPointsForTeam(team: ITeam): IntArray = intArrayOf(GameRuleLogic.greatestSwarmSize(board, team)) // TODO test if one player is unable to move he loses e.g. in corner + /** Gibt an, ob das Spiel beendet ist. */ override val isOver: Boolean get() = (Team.values().any { GameRuleLogic.isSwarmConnected(board, it) } && turn.mod(2) == 0) || turn / 2 >= PiranhaConstants.ROUND_LIMIT + /** Liefert die aktuelle Gewinnbedingung oder null, falls das Spiel noch nicht entschieden ist. */ override val winCondition: WinCondition? get() = if(Team.values().any { team -> GameRuleLogic.isSwarmConnected(board, team) }) { @@ -52,6 +58,7 @@ data class GameState @JvmOverloads constructor( null } + /** Führt einen gültigen [move] direkt aus und aktualisiert Spielfeld sowie Zähler. */ override fun performMoveDirectly(move: Move) { if(board.getTeam(move.from) != currentTeam) { throw InvalidMoveException(PiranhaMoveMistake.WRONG_START, move) @@ -64,6 +71,7 @@ data class GameState @JvmOverloads constructor( lastMove = move } + /** Gibt alle sinnvollen Züge für den aktuellen Spieler auf dem aktuellen [board] zurück. */ override fun getSensibleMoves(): List { val piranhas = board.filterValues { field -> field.team == currentTeam } val moves = ArrayList(piranhas.size * 2) @@ -73,16 +81,19 @@ data class GameState @JvmOverloads constructor( return moves } + /** Iteriert über alle sinnvollen Züge des aktuellen Spielers. */ override fun moveIterator(): Iterator = getSensibleMoves().iterator() + /** Erstellt eine tiefe Kopie dieses Spielzustands inklusive geklontem [board]. */ override fun clone(): GameState = copy(board = board.clone()) + /** Ermittelt zusammenfassende Statistiken für das angegebene [team]. */ override fun teamStats(team: ITeam): List = listOf( Stat("Anzahl Fische", board.fieldsForTeam(team).size), Stat("Größter Schwarm", GameRuleLogic.greatestSwarmSize(board, team)) ) -} \ No newline at end of file +} diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/Move.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/Move.kt similarity index 78% rename from plugin2026/src/main/kotlin/sc/plugin2026/Move.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/Move.kt index 1068f8ff9..b2cb47813 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/Move.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/Move.kt @@ -19,9 +19,11 @@ data class Move( val direction: Direction, ): IMove { - /** Zugdistanz auf dem gegebenen [board]. Kann mit [direction] multipliziert werden um Zugvektor zu ermitteln. */ + /** Erwartete Zugdistanz auf dem gegebenen [board]. + * Kann mit [direction] multipliziert werden, um den Zugvektor zu ermitteln. */ fun getDistance(board: Board) = GameRuleLogic.movementDistance(board, this) + /** Kurzbeschreibung des Zugs für Log-Ausgaben. */ override fun toString(): String = "Schwimme von $from in Richtung $direction" diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt similarity index 74% rename from plugin2026/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt index f21cda603..88e8e42e7 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/PiranhaMoveMistake.kt @@ -4,7 +4,8 @@ import sc.shared.IMoveMistake /** Spielspezifische Zug-Fehler. Siehe auch [sc.shared.MoveMistake]. */ enum class PiranhaMoveMistake(override val message: String) : IMoveMistake { - // TODO relevant bei performMove(Directly) + /** Startfeld gehoert nicht zum eigenen Team. */ WRONG_START("Das Startfeld ist kein Piranha des eigenen Teams"), + /** Gegnerische Piranhas koennen nicht uebersprungen werden. */ JUMP_OVER_OPPONENT("Gegnerische Piranhas können nicht übersprungen werden"), } diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/util/Constants.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/util/Constants.kt similarity index 100% rename from plugin2026/src/main/kotlin/sc/plugin2026/util/Constants.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/util/Constants.kt diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt similarity index 73% rename from plugin2026/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt index 2b9e315d4..dfb948201 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/util/GamePlugin.kt @@ -9,13 +9,18 @@ import sc.plugin2026.GameState import sc.plugin2026.Move import sc.shared.* +/** Gewinn-Gründe für Piranhas. */ @XStreamAlias(value = "winreason") enum class PiranhasWinReason(override val message: String, override val isRegular: Boolean = true): IWinReason { + /** Groesster zusammenhaengender Schwarm entscheidet. */ BIGGER_SWARM("%s hat den größeren zusammenhängenden Schwarm"), + /** Alle Fische einer Farbe zuerst vereinigt. */ FIRST_UNION("%s hat zuerst alle Fische einer Farbe vereinigt"), } +/** Plugin-Implementierung für das Piranhas-Spiel. */ class GamePlugin: IGamePlugin { + /** @suppress */ companion object { const val PLUGIN_ID = "swc_2026_piranhas" val scoreDefinition: ScoreDefinition = @@ -25,21 +30,28 @@ class GamePlugin: IGamePlugin { )) } + /** Eindeutige Plugin-ID. */ override val id = PLUGIN_ID + /** Anzeigename des Spiels. */ override val name = "Piranhas" + /** Definition der Wertung und Auswertung. */ override val scoreDefinition = Companion.scoreDefinition + /** Maximale Zuganzahl (beide Spieler). */ override val turnLimit: Int = PiranhaConstants.ROUND_LIMIT * 2 + /** Klasse der Zug-Implementierung. */ override val moveClass = Move::class.java + /** Erstellt eine neue Spielinstanz im Startzustand. */ override fun createGame(): IGameInstance = TwoPlayerGame(this, GameState()) + /** Erstellt eine Spielinstanz aus einem vorhandenen [state]. */ override fun createGameFromState(state: IGameState): IGameInstance = TwoPlayerGame(this, state as GameState) diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt similarity index 90% rename from plugin2026/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt index 89dd737a3..063fa66b9 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/util/GameRuleLogic.kt @@ -10,6 +10,7 @@ import sc.plugin2026.PiranhaMoveMistake import sc.shared.IMoveMistake import sc.shared.MoveMistake +/** Regellogik und Hilfsfunktionen für das Piranhas-Spiel. */ object GameRuleLogic { /** Anzahl der Fische in der Bewegungsachse des Zuges. * @return wie viele Felder weit der Zug sein sollte */ @@ -41,7 +42,7 @@ object GameRuleLogic { move.from + move.direction * movementDistance(board, move) /** Prüft, ob ein Zug gültig ist. - * @team null wenn der Zug valide ist, sonst ein entsprechender [IMoveMistake]. */ + * @return null wenn der Zug valide ist, sonst ein entsprechender [IMoveMistake]. */ @JvmStatic fun checkMove(board: Board, move: Move): IMoveMistake? { val distance = movementDistance(board, move) @@ -87,12 +88,13 @@ object GameRuleLogic { return moves } + /** Sequenz aller gültigen Züge des Fisches auf dem Startfeld [pos]. */ fun possibleMovesSequence(board: Board, pos: Coordinates): Sequence = Direction.values().asSequence() .map { direction -> Move(pos, direction)} .filter { move -> checkMove(board, move) == null } - /** @return the [Coordinates] from [parentSet] which are neighbors of [pos] */ + /** @return die [Coordinates] aus [parentSet], die Nachbarn von [pos] sind */ private fun selectNeighbors(pos: Coordinates, parentSet: Collection): Collection { val returnSet = ArrayList(8) for(i in -1..1) { @@ -112,8 +114,8 @@ object GameRuleLogic { return returnSet } - /** Called with a single fish in [swarm] and the [looseFishes] left, - * recursively calling with neighbors added to [swarm] to find a whole swarm. */ + /** Startet mit einem einzelnen Fisch in [swarm] und den verbleibenden [looseFishes], + * ruft sich rekursiv mit hinzugefügten Nachbarn auf, um den gesamten Schwarm zu finden. */ private fun getSwarm(looseFishes: Collection, swarm: List): List { val swarmNeighbors = swarm.flatMap { selectNeighbors(it, looseFishes) } @@ -125,7 +127,7 @@ object GameRuleLogic { return swarm } - /** Finds the most weighty swarm among a set of positions with weights. */ + /** Findet den schwersten Schwarm innerhalb einer Menge gewichteter Positionen. */ @JvmStatic fun greatestSwarm(fieldsToCheck: Map): Map? { // Make a copy, so there will be no conflict with direct calls. @@ -168,4 +170,4 @@ object GameRuleLogic { val greatestSwarm = greatestSwarm(fieldsWithFish) return greatestSwarm?.size == fieldsWithFish.size } -} \ No newline at end of file +} diff --git a/plugin2026/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt b/games/piranhas/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt similarity index 82% rename from plugin2026/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt rename to games/piranhas/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt index 4be7b6aaa..04cf19a99 100644 --- a/plugin2026/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt +++ b/games/piranhas/src/main/kotlin/sc/plugin2026/util/XStreamClasses.kt @@ -3,8 +3,10 @@ package sc.plugin2026.util import sc.networking.XStreamProvider import sc.plugin2026.* +/** @suppress */ class XStreamClasses: XStreamProvider { + /** Klassen, die im Netzwerkverkehr serialisiert werden. */ override val classesToRegister: List> = listOf( GameState::class.java, diff --git a/plugin2026/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin b/games/piranhas/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin similarity index 100% rename from plugin2026/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin rename to games/piranhas/src/main/resources/META-INF/services/sc.api.plugins.IGamePlugin diff --git a/plugin2026/src/main/resources/META-INF/services/sc.networking.XStreamProvider b/games/piranhas/src/main/resources/META-INF/services/sc.networking.XStreamProvider similarity index 100% rename from plugin2026/src/main/resources/META-INF/services/sc.networking.XStreamProvider rename to games/piranhas/src/main/resources/META-INF/services/sc.networking.XStreamProvider diff --git a/plugin2026/src/test/kotlin/sc/GamePlayTest.kt b/games/piranhas/src/test/kotlin/sc/GamePlayTest.kt similarity index 92% rename from plugin2026/src/test/kotlin/sc/GamePlayTest.kt rename to games/piranhas/src/test/kotlin/sc/GamePlayTest.kt index b78a0d4d1..02bbc4a24 100644 --- a/plugin2026/src/test/kotlin/sc/GamePlayTest.kt +++ b/games/piranhas/src/test/kotlin/sc/GamePlayTest.kt @@ -64,10 +64,12 @@ class GamePlayTest: WordSpec({ override fun onStateChanged(data: IGameState, observersOnly: Boolean) { val state = data as? TwoPlayerGameState<*> state?.lastMove.shouldNotBeNull() - data.hashCode() shouldNotBe finalState - // hashing it to avoid cloning, since we get the original object which might be mutable - finalState = data.hashCode() - logger.debug("Updating state hash to $finalState") + if(!observersOnly) { + data.hashCode() shouldNotBe finalState + // hashing it to avoid cloning, since we get the original object which might be mutable + finalState = data.hashCode() + logger.debug("Updating state hash to $finalState") + } } }) diff --git a/plugin2026/src/test/kotlin/sc/GameRuleLogicTest.java b/games/piranhas/src/test/kotlin/sc/GameRuleLogicTest.java similarity index 100% rename from plugin2026/src/test/kotlin/sc/GameRuleLogicTest.java rename to games/piranhas/src/test/kotlin/sc/GameRuleLogicTest.java diff --git a/plugin2026/src/test/kotlin/sc/plugin2026/BoardTest.kt b/games/piranhas/src/test/kotlin/sc/plugin2026/BoardTest.kt similarity index 100% rename from plugin2026/src/test/kotlin/sc/plugin2026/BoardTest.kt rename to games/piranhas/src/test/kotlin/sc/plugin2026/BoardTest.kt diff --git a/plugin2026/src/test/kotlin/sc/plugin2026/GameRuleLogicTest.kt b/games/piranhas/src/test/kotlin/sc/plugin2026/GameRuleLogicTest.kt similarity index 100% rename from plugin2026/src/test/kotlin/sc/plugin2026/GameRuleLogicTest.kt rename to games/piranhas/src/test/kotlin/sc/plugin2026/GameRuleLogicTest.kt diff --git a/plugin2026/src/test/kotlin/sc/plugin2026/GameStateTest.kt b/games/piranhas/src/test/kotlin/sc/plugin2026/GameStateTest.kt similarity index 100% rename from plugin2026/src/test/kotlin/sc/plugin2026/GameStateTest.kt rename to games/piranhas/src/test/kotlin/sc/plugin2026/GameStateTest.kt diff --git a/gradle/build.gradle.kts b/gradle/build.gradle.kts index e749767de..832b87274 100644 --- a/gradle/build.gradle.kts +++ b/gradle/build.gradle.kts @@ -1,20 +1,19 @@ -import org.gradle.kotlin.dsl.support.unzipTo -import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier +import org.jetbrains.dokka.gradle.tasks.DokkaGeneratePublicationTask +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.kotlinExtension -import sc.gradle.ScriptsTask -import java.nio.file.Files -import java.util.concurrent.TimeoutException -import java.util.concurrent.atomic.AtomicBoolean +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { - maven - kotlin("jvm") version "1.6.21" - id("org.jetbrains.dokka") version "0.10.1" + kotlin("jvm") version "2.3.0" + id("org.jetbrains.dokka") version "2.1.0" + id("org.jetbrains.dokka-javadoc") version "2.1.0" id("scripts-task") id("idea") - - id("com.github.ben-manes.versions") version "0.42.0" // only upgrade with Gradle 7: https://github.com/ben-manes/gradle-versions-plugin/issues/778 - id("se.patrikerdes.use-latest-versions") version "0.2.18" + `maven-publish` + + id("com.github.ben-manes.versions") version "0.53.0" + id("se.patrikerdes.use-latest-versions") version "0.2.19" } idea { @@ -24,320 +23,357 @@ idea { } } +// == Core build context (shared via rootProject.extra across split scripts) == + val gameName by extra { property("socha.gameName") as String } val versions = arrayOf("year", "minor", "patch").map { property("socha.version.$it").toString().toInt() } -val versionObject = KotlinVersion(versions[0], versions[1], versions[2]) +val versionObject by extra { KotlinVersion(versions[0], versions[1], versions[2]) } version = versionObject.toString() + property("socha.version.suffix").toString().takeUnless { it.isBlank() }?.let { "-$it" }.orEmpty() val year by extra { "20${versionObject.major}" } val game by extra { "${gameName}_$year" } -val bundleDir by extra { buildDir.resolve("bundle") } +val bundleDir by extra { layout.buildDirectory.dir("bundle").get().asFile } val bundledPlayer by extra { "randomplayer-$gameName-$version.jar" } -val testingDir by extra { buildDir.resolve("tests") } -val documentedProjects = listOf("sdk", "plugin$year") +val bundledJavadocDir by extra { "javadoc" } +val bundledDokkaDir by extra { "dokka" } +val sdkDocsDir by extra { "sdk" } +val pluginDocsDir by extra { "plugin-$gameName" } +val integrationReportsDir by extra { layout.buildDirectory.dir("reports").get().asFile } +val documentedProjects by extra { listOf("sdk", "plugin$year") } val isBeta by extra { versionObject.minor == 0 } val enableTestClient by extra { arrayOf("check", "testTestClient").any { gradle.startParameter.taskNames.contains(it) } || !isBeta } -val enableIntegrationTesting = !project.hasProperty("nointegration") && (!isBeta || enableTestClient) +val enableIntegrationTesting by extra { !project.hasProperty("nointegration") && (!isBeta || enableTestClient) } + +val javaRuntimeVersion: JavaVersion = JavaVersion.current() +println("Current version: $version (unstable: $isBeta) Game: $game (Kotlin ${kotlinExtension.coreLibrariesVersion}, Java runtime $javaRuntimeVersion)") -val javaTargetVersion = JavaVersion.VERSION_1_8 -val javaVersion = JavaVersion.current() -println("Current version: $version (unstable: $isBeta) Game: $game (Kotlin ${kotlinExtension.coreLibrariesVersion}, Java $javaVersion)") -if (javaVersion != javaTargetVersion) - System.err.println("Java version $javaTargetVersion is recommended - expect issues with generating documentation (consider '-x doc' if you don't care)") +// == Split script modules == +apply(from = "gradle/integration-tasks.gradle.kts") +apply(from = "gradle/release-task.gradle.kts") + +// == Root orchestration tasks == val doAfterEvaluate = ArrayList<(Project) -> Unit>() tasks { - val startServer by creating { + register("startServer") { dependsOn(":server:run") group = "application" } - val doc by creating(DokkaTask::class) { - dependsOn(documentedProjects.map { ":$it:classes" }) - group = "documentation" - outputDirectory = bundleDir.resolve("doc").toString() - outputFormat = "javadoc" - subProjects = documentedProjects - configuration { - reportUndocumented = false - moduleName = "Software-Challenge API $version" - jdkVersion = 8 - } - } - - val bundle by creating { - dependsOn(doc) - dependOnSubprojects() - group = "distribution" - description = "Zips everything up for release into ${bundleDir.relativeTo(projectDir)}" - outputs.dir(bundleDir) - } - - val release by creating { - dependsOn(clean, check) - group = "distribution" - description = "Prepares a new Release by bumping the version and pushing a commit tagged with the new version" - doLast { - var newVersion = version - fun String.editVersion(version: String, new: Int) = - if (startsWith("socha.version.$version")) - "socha.version.$version=${new.toString().padStart(2, '0')}" - else this - val versionLineUpdater: (String) -> String = when { - project.hasProperty("manual") -> ({ it }) - project.hasProperty("minor") -> ({ - newVersion = "${versionObject.major}.${versionObject.minor + 1}.0" - it.editVersion("minor", versionObject.minor + 1).editVersion("patch", 0) - }) - project.hasProperty("patch") -> ({ - newVersion = "${versionObject.major}.${versionObject.minor}.${versionObject.patch + 1}" - it.editVersion("patch", versionObject.patch + 1) - }) - else -> throw InvalidUserDataException("Gib entweder -Ppatch oder -Pminor an, um die Versionsnummer automatisch zu inkrementieren, oder ändere sie selbst in gradle.properties und gib dann -Pmanual an!") - } - - val desc = project.properties["m"]?.toString() - ?: throw InvalidUserDataException("Das Argument -Pm=\"Beschreibung dieser Version\" wird benötigt") - - val propsFile = file("gradle.properties") - propsFile.writeText(propsFile.readLines().joinToString("\n") { versionLineUpdater(it) }) - - println("Version: $newVersion") - println("Beschreibung: $desc") - exec { commandLine("git", "add", "gradle.properties", "CHANGELOG.md") } - exec { commandLine("git", "commit", "-m", "release: v$newVersion") } - exec { commandLine("git", "tag", newVersion, "-m", desc) } - exec { commandLine("git", "push", "--follow-tags") } - } - } - clean { dependOnSubprojects() } test { dependOnSubprojects() } + check { + dependOnSubprojects() + } build { - dependsOn(bundle) + dependsOn("bundle") } - // TODO create a global constant which can be shared with testclient & co - maybe a resource? - val maxGameLength = 150L // 2m30s - - val testGame by creating { - dependsOn(":server:makeRunnable", ":player:bundleShadow") - group = "verification" - doFirst { - val testGameDir = testingDir.resolve("game") - testGameDir.deleteRecursively() - testGameDir.mkdirs() - val java = "java" - //File("/usr/lib/jvm").listFiles { f:File -> f.name.contains("java-1") }?.max()?.resolve("bin/java").toString() - val port = "13054" - println("Running on Port: $port") - - val jarTask = project(":server").getTasksByName("jar", false).single() as Jar - val server = - ProcessBuilder( - java, - "-Dlogback.configurationFile=${project(":server").projectDir.resolve("configuration/logback-trace.xml")}", - "-jar", jarTask.archiveFile.get().asFile.absolutePath, - "--port", port - ) - .redirectOutput(testGameDir.resolve("server.log")) - .redirectError(testGameDir.resolve("server-err.log")) - .directory(jarTask.destinationDirectory.get().asFile) - .start() - var i = 0 - while(Files.size(testGameDir.resolve("server.log").toPath()) < 1000 && i++ < 50) - Thread.sleep(300) - val startClient: (Int) -> Process = { - Thread.sleep(300) - ProcessBuilder( - java, "-jar", bundleDir.resolve(bundledPlayer).absolutePath, - "--port", port - ).redirectOutput(testGameDir.resolve("client$it.log")).redirectError(testGameDir.resolve("client$it-err.log")).start() - } - startClient(1) - startClient(2) - val timeout = AtomicBoolean(false) - val thread = Thread { - try { - Thread.sleep(maxGameLength * 1000) - } catch (e: InterruptedException) { - return@Thread - } - timeout.set(true) - println("Killing Server because of timeout!") - server.destroyForcibly() - }.apply { - isDaemon = true - start() + val bundleJavadocDocs by registering(Copy::class) { + dependsOn(documentedProjects.map { ":$it:javadocDocs" }) + group = "documentation" + description = "Collects Javadoc output for documented projects into ${bundleDir.relativeTo(projectDir)}/$bundledJavadocDir" + into(bundleDir.resolve(bundledJavadocDir)) + val sdkJavadoc = project(":sdk").tasks.named("dokkaGeneratePublicationJavadoc") + val pluginJavadoc = project(":plugin$year").tasks.named("dokkaGeneratePublicationJavadoc") + from(sdkJavadoc.flatMap { it.outputDirectory }) { into(sdkDocsDir) } + from(pluginJavadoc.flatMap { it.outputDirectory }) { into(pluginDocsDir) } + } + + val bundleDokkaDocs by registering(Copy::class) { + dependsOn(documentedProjects.map { ":$it:dokkaDocs" }) + group = "documentation" + description = "Collects Dokka HTML output for documented projects into ${bundleDir.relativeTo(projectDir)}/$bundledDokkaDir" + into(bundleDir.resolve(bundledDokkaDir)) + val sdkHtml = project(":sdk").tasks.named("dokkaGeneratePublicationHtml") + val pluginHtml = project(":plugin$year").tasks.named("dokkaGeneratePublicationHtml") + from(sdkHtml.flatMap { it.outputDirectory }) { into(sdkDocsDir) } + from(pluginHtml.flatMap { it.outputDirectory }) { into(pluginDocsDir) } + } + + fun wireDocAlias(taskName: String, taskDescription: String, aggregateTask: TaskProvider) { + if (findByName(taskName) != null) { + named(taskName) { + group = "documentation" + description = taskDescription + dependsOn(aggregateTask) + onlyIf { false } } - try { - for (i in 1..2) { - val logFile = testGameDir.resolve("client$i.log") - var log: String - println("Waiting for client $i to receive game result") - do { - if (!server.isAlive) { - if (timeout.get()) - throw TimeoutException("$this task exceeded $maxGameLength seconds") - throw Exception("Server terminated unexpectedly!") - } - Thread.yield() - Thread.sleep(100) - log = logFile.readText() - } while (!log.contains("stop", true)) - if (!log.contains("Received game result")) - throw Exception("Client $i did not receive the game result - check $logFile") - } - } catch (t: Throwable) { - println("Error in $this - check the logs in $testGameDir") - throw t - } finally { - server.destroy() + } else { + register(taskName) { + group = "documentation" + description = taskDescription + dependsOn(aggregateTask) } - thread.interrupt() - println("Successfully played a game using the bundled server & client!") } } - - val testTestClient by creating { - dependsOn(":server:bundle") - group = "verification" - shouldRunAfter(testGame) - val testClientGames = 3 - doFirst { - testingDir.mkdirs() - val serverDir = testingDir.resolve("testclient") - serverDir.deleteRecursively() - unzipTo(serverDir, bundleDir.resolve("software-challenge-server.zip")) - - val command = (project(":test-client").getTasksByName("createStartScripts", false).single() as ScriptsTask).content.split(' ') + - arrayOf("--start-server", "--tests", testClientGames.toString(), "--port", "13055") - println("Testing TestClient with $command") - val testClient = ProcessBuilder(command).directory(serverDir).start() - if (testClient.waitFor(maxGameLength * testClientGames, TimeUnit.SECONDS)) { - val value = testClient.exitValue() - // TODO check whether TestClient actually played games - if (value == 0) - println("TestClient successfully tested!") - else - throw Exception("TestClient exited with exit code $value - check the logs under $serverDir!") - } else { - throw Exception("TestClient exceeded timeout of ${maxGameLength * testClientGames} seconds - check the logs under $serverDir!") + + wireDocAlias( + taskName = "javadoc", + taskDescription = "Collects Javadoc output for documented projects into ${bundleDir.relativeTo(projectDir)}/$bundledJavadocDir", + aggregateTask = bundleJavadocDocs + ) + wireDocAlias( + taskName = "dokka", + taskDescription = "Collects Dokka HTML output for documented projects into ${bundleDir.relativeTo(projectDir)}/$bundledDokkaDir", + aggregateTask = bundleDokkaDocs + ) + + register("bundle") { + dependsOn("javadoc") + dependOnSubprojects() + group = "distribution" + description = "Zips everything up for release into ${bundleDir.relativeTo(projectDir)}" + outputs.dir(bundleDir) + } + + register("verifyDocs") { + group = "documentation" + description = "Verifies generated docs omit Companion classes and include Java+Kotlin SDK docs." + dependsOn("javadoc", "dokka") + doLast { + val forbidden = Regex("\\bCompanion\\b") + val offending = mutableListOf() + documentedProjects.map { project(":$it") }.forEach { target -> + val javadocDir = target.tasks.named("dokkaGeneratePublicationJavadoc") + .get().outputDirectory.get().asFile + val htmlDir = target.tasks.named("dokkaGeneratePublicationHtml") + .get().outputDirectory.get().asFile + listOf(javadocDir, htmlDir) + .asSequence() + .filter { it.exists() } + .flatMap { dir -> + dir.walkTopDown() + .filter { it.isFile && (it.extension == "html" || it.extension == "js" || it.name == "package-list" || it.name == "element-list") } + } + .forEach { file -> + val text = runCatching { file.readText() }.getOrNull() ?: return@forEach + if (forbidden.containsMatchIn(text)) offending.add(file.relativeTo(rootProject.projectDir).path) + } + } + if (offending.isNotEmpty()) { + throw GradleException("Companion entries found in docs: ${offending.joinToString(", ")}") + } + + val sdkJavadocDir = project(":sdk").tasks + .named("dokkaGeneratePublicationJavadoc") + .get().outputDirectory.get().asFile + val javaDocPage = sdkJavadocDir.resolve("sc/networking/clients/IClient.html") + val kotlinDocPage = sdkJavadocDir.resolve("sc/protocol/room/WelcomeMessage.html") + val missing = buildList { + if (!javaDocPage.exists()) add(javaDocPage.relativeTo(rootProject.projectDir).path) + if (!kotlinDocPage.exists()) add(kotlinDocPage.relativeTo(rootProject.projectDir).path) + } + if (missing.isNotEmpty()) { + throw GradleException("SDK javadoc is missing expected Java/Kotlin pages: ${missing.joinToString(", ")}") + } + if (!javaDocPage.readText().contains("Client interface to send packages to the server.")) { + throw GradleException("SDK javadoc is missing JavaDoc content for sc.networking.clients.IClient.") + } + if (!kotlinDocPage.readText().contains("Nachricht, die zu Beginn eines Spiels")) { + throw GradleException("SDK javadoc is missing KDoc content for sc.protocol.room.WelcomeMessage.") + } + val allClassesPage = sdkJavadocDir.resolve("allclasses.html") + if (!allClassesPage.exists()) { + throw GradleException("SDK javadoc is missing allclasses index page.") + } + val allClassesText = allClassesPage.readText() + if (!allClassesText.contains("sc/networking/clients/IClient.html")) { + throw GradleException("SDK javadoc index is missing Java type link for sc.networking.clients.IClient.") + } + if (!allClassesText.contains("sc/protocol/room/WelcomeMessage.html")) { + throw GradleException("SDK javadoc index is missing Kotlin type link for sc.protocol.room.WelcomeMessage.") } } } - - val integrationTest by creating { - dependsOn(testGame, ":player:playerTest") - if (enableTestClient) - dependsOn(testTestClient) - group = "verification" - shouldRunAfter(test) - } - - check { - dependOnSubprojects() - if (enableIntegrationTesting) - dependsOn(integrationTest) - } } -// == Cross-project configuration == - subprojects { apply(plugin = "java-library") apply(plugin = "kotlin") apply(plugin = "com.github.ben-manes.versions") apply(plugin = "se.patrikerdes.use-latest-versions") - - dependencies { - testImplementation(project(":sdk", "testConfig")) - } - - tasks { - test { - useJUnitPlatform() - } - - withType { - this.sourceCompatibility = javaTargetVersion.toString() - this.targetCompatibility = javaTargetVersion.toString() + + if (name != "test-config") { + dependencies { + add("testImplementation", project(":test-config")) } - - withType { - kotlinOptions { - this.jvmTarget = javaTargetVersion.toString() - this.freeCompilerArgs = listOf("-Xjvm-default=all") - } + } + + tasks.withType().configureEach { + useJUnitPlatform() + } + + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(javaRuntimeVersion.toString())) + freeCompilerArgs.add("-Xjdk-release=${javaRuntimeVersion.majorVersion}") } } } allprojects { + this.group = "software-challenge" + repositories { mavenCentral() maven("https://maven.wso2.org/nexus/content/groups/wso2-public/") } - - if (this.name in documentedProjects) { - apply(plugin = "maven") + + if (name in documentedProjects) { + apply(plugin = "maven-publish") apply(plugin = "org.jetbrains.dokka") + apply(plugin = "org.jetbrains.dokka-javadoc") + + publishing { + publications { + create(name) { + from(components["java"]) + version = rootProject.version.toString() + } + } + } + + java { + withSourcesJar() + withJavadocJar() + } + + dokka { + dokkaPublications.configureEach { + moduleName.set("Software-Challenge ${project.name} \"$gameName\"") + moduleVersion.set(rootProject.version.toString()) + } + dokkaSourceSets.configureEach { + reportUndocumented.set(false) + suppressGeneratedFiles.set(true) + documentedVisibilities.set(setOf(VisibilityModifier.Public)) + jdkVersion.set(javaRuntimeVersion.majorVersion.toInt()) + } + } + tasks { - val doc by creating(DokkaTask::class) { + val javadocTask = named("dokkaGeneratePublicationJavadoc") + val htmlTask = named("dokkaGeneratePublicationHtml") + named("javadoc") { enabled = false } + + val inlineHtmlNav by registering { group = "documentation" - dependsOn(classes) - outputDirectory = buildDir.resolve("doc").toString() - outputFormat = "javadoc" + description = "Inlines navigation.html into Dokka HTML to avoid empty sidebars." + dependsOn(htmlTask) + doLast { + val docDir = htmlTask.get().outputDirectory.get().asFile + val navFile = docDir.resolve("navigation.html") + if (!navFile.exists()) return@doLast + val navHtml = navFile.readText() + val marker = "
" + val linkFixScript = """ + + """.trimIndent() + docDir.walkTopDown() + .filter { it.isFile && it.extension == "html" } + .forEach { file -> + var updated = file.readText() + if (updated.contains(marker)) { + updated = updated.replace(marker, "
$navHtml
") + } + if (!updated.contains("dokka-inline-nav-fix")) { + val taggedScript = linkFixScript.replace("