Compare commits

..

11 Commits

Author SHA1 Message Date
Janis e7d556fa06 feat: update Scala library version and enhance test configurations
Build & Test (NowChessSystems) TeamCity build failed
2026-04-21 14:48:57 +02:00
Janis 9a36555f1a feat: add lint command to CLAUDE.md and create lint script 2026-04-21 14:31:36 +02:00
Janis e5a6cc30eb feat: NCS-53 changed IO to MicroService for easier scaling 2026-04-21 14:16:29 +02:00
TeamCity d7f7c37111 ci: bump version with Build-44 2026-04-21 11:13:30 +00:00
Janis 2f1e3aded0 refactor: align JSON string formatting in JsonParserTest
Build & Test (NowChessSystems) TeamCity build finished
2026-04-21 13:02:18 +02:00
shosho996 1401297e7f test: NCS-45 IO Test reduction (#32)
Build & Test (NowChessSystems) TeamCity build failed
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #32
Co-authored-by: Shahd Lala <shosho996@blackhole.local>
Co-committed-by: Shahd Lala <shosho996@blackhole.local>
2026-04-21 12:39:19 +02:00
Janis 5ad5efb41e feat: NCS-37 Quarkus integration (#35)
Build & Test (NowChessSystems) TeamCity build failed
Reviewed-on: #35
Reviewed-by: Leon Hermann <lq@blackhole.local>
2026-04-21 12:35:20 +02:00
TeamCity f215ec681a ci: bump version with Build-43 2026-04-19 20:53:56 +00:00
Janis 0091d50467 feat: NCS-40 Rework Draw System (#34)
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #34
Reviewed-by: Shahd Lala <shosho996@blackhole.local>
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2026-04-19 22:44:48 +02:00
TeamCity 2e4c7549b5 ci: bump version with Build-42 2026-04-19 14:01:11 +00:00
Janis dceab0875e feat: NCS-41 Bot Platform (#33)
Build & Test (NowChessSystems) TeamCity build finished
Co-authored-by: Janis <janis@nowchess.de>
Reviewed-on: #33
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2026-04-19 15:52:08 +02:00
411 changed files with 2525345 additions and 14815 deletions
+63 -392
View File
@@ -2,58 +2,11 @@
> **Stack:** raw-http | none | unknown | scala
> 0 routes + 40 rpc | 0 models | 0 components | 146 lib files | 1 env vars | 1 middleware
> 0 routes | 0 models | 0 components | 63 lib files | 1 env vars | 1 middleware
> **Token savings:** this file is ~0 tokens. Without it, AI exploration would cost ~0 tokens. **Saves ~0 tokens per conversation.**
---
# Routes
## gRPC
- `/CoordinatorService/BatchResubscribeGames` (BatchResubscribeRequest) → BatchResubscribeResponse
- `/CoordinatorService/UnsubscribeGames` (UnsubscribeGamesRequest) → UnsubscribeGamesResponse
- `/CoordinatorService/EvictGames` (EvictGamesRequest) → EvictGamesResponse
- `/CoordinatorService/DrainInstance` (DrainInstanceRequest) → DrainInstanceResponse
- `/CoordinatorService/BatchResubscribeGames` (BatchResubscribeRequest) → BatchResubscribeResponse
- `/CoordinatorService/UnsubscribeGames` (UnsubscribeGamesRequest) → UnsubscribeGamesResponse
- `/CoordinatorService/EvictGames` (EvictGamesRequest) → EvictGamesResponse
- `/CoordinatorService/DrainInstance` (DrainInstanceRequest) → DrainInstanceResponse
- `/IoService/ImportFen` (ProtoImportFenRequest) → ProtoGameContext
- `/IoService/ImportPgn` (ProtoImportPgnRequest) → ProtoGameContext
- `/IoService/ExportCombined` (ProtoGameContext) → ProtoCombinedExport
- `/IoService/ExportFen` (ProtoGameContext) → ProtoStringResult
- `/IoService/ExportPgn` (ProtoGameContext) → ProtoStringResult
- `/RuleService/CandidateMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/LegalMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/AllLegalMoves` (ProtoGameContext) → ProtoMoveList
- `/RuleService/IsCheck` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsCheckmate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsStalemate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsInsufficientMaterial` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsFiftyMoveRule` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsThreefoldRepetition` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/ApplyMove` (ProtoMoveRequest) → ProtoGameContext
- `/RuleService/PostMoveStatus` (ProtoGameContext) → ProtoPostMoveStatus
- `/IoService/ImportFen` (ProtoImportFenRequest) → ProtoGameContext
- `/IoService/ImportPgn` (ProtoImportPgnRequest) → ProtoGameContext
- `/IoService/ExportCombined` (ProtoGameContext) → ProtoCombinedExport
- `/IoService/ExportFen` (ProtoGameContext) → ProtoStringResult
- `/IoService/ExportPgn` (ProtoGameContext) → ProtoStringResult
- `/RuleService/CandidateMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/LegalMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/AllLegalMoves` (ProtoGameContext) → ProtoMoveList
- `/RuleService/IsCheck` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsCheckmate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsStalemate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsInsufficientMaterial` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsFiftyMoveRule` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsThreefoldRepetition` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/ApplyMove` (ProtoMoveRequest) → ProtoGameContext
- `/RuleService/PostMoveStatus` (ProtoGameContext) → ProtoPostMoveStatus
---
# Libraries
- `jacoco-reporter/scoverage_coverage_gaps.py`
@@ -72,74 +25,6 @@
- function main: () -> None
- class TestCase
- _...2 more_
- `modules/account/src/main/scala/de/nowchess/account/client/CoreGameClient.scala` — class CoreGameClient, function createGame
- `modules/account/src/main/scala/de/nowchess/account/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/account/src/main/scala/de/nowchess/account/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/account/src/main/scala/de/nowchess/account/domain/Challenge.scala`
- class Challenge
- function gameIdOpt
- function declineReasonOpt
- function timeControlLimitOpt
- function timeControlIncrementOpt
- `modules/account/src/main/scala/de/nowchess/account/domain/ChallengeColorConverter.scala` — class ChallengeColorConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/ChallengeStatusConverter.scala` — class ChallengeStatusConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/DeclineReasonConverter.scala` — class DeclineReasonConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/TimeControl.scala` — class TimeControl
- `modules/account/src/main/scala/de/nowchess/account/domain/UserAccount.scala`
- class UserAccount
- function getBotAccounts
- class BotAccount
- class OfficialBotAccount
- `modules/account/src/main/scala/de/nowchess/account/error/AccountError.scala` — function message
- `modules/account/src/main/scala/de/nowchess/account/error/ChallengeError.scala` — function message
- `modules/account/src/main/scala/de/nowchess/account/filter/AlreadyLoggedInFilter.scala` — class AlreadyLoggedInFilter
- `modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala`
- class UserAccountRepository
- function findByUsername
- function findById
- function persist
- function findByEmail
- function findAll
- _...12 more_
- `modules/account/src/main/scala/de/nowchess/account/repository/ChallengeRepository.scala`
- class ChallengeRepository
- function findActiveByChallengerId
- function findActiveByDestUserId
- function findDuplicateChallenge
- function findById
- function persist
- _...1 more_
- `modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala`
- class AccountResource
- function register
- function login
- function me
- function publicProfile
- function banUser
- _...10 more_
- `modules/account/src/main/scala/de/nowchess/account/resource/ChallengeResource.scala`
- class ChallengeResource
- function create
- function list
- function accept
- function decline
- function cancel
- `modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala`
- class AccountService
- function register
- function login
- function findByUsername
- function findById
- function createBotAccount
- _...11 more_
- `modules/account/src/main/scala/de/nowchess/account/service/ChallengeService.scala`
- class ChallengeService
- function create
- function accept
- function decline
- function cancel
- function listForUser
- _...1 more_
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala`
- class Board
- function apply
@@ -169,15 +54,6 @@
- `modules/api/src/main/scala/de/nowchess/api/dto/ErrorEventDto.scala` — class ErrorEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/dto/GameFullEventDto.scala` — class GameFullEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/dto/GameStateEventDto.scala` — class GameStateEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/error/GameError.scala` — function message
- `modules/api/src/main/scala/de/nowchess/api/game/ClockState.scala`
- function activeColor
- function afterMove
- function remainingMs
- function remainingMs
- function afterMove
- function remainingMs
- _...3 more_
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`
- function kingSquare
- function withBoard
@@ -186,21 +62,11 @@
- function withEnPassantSquare
- function withHalfMoveClock
- _...4 more_
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
- class ApiResponse
- function error
- function totalPages
- `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`
- class RuleSet
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/bot/python/nnue.py`
- function get_weights_dir: ()
- function get_data_dir: ()
@@ -305,136 +171,30 @@
- class ZobristHash
- function hash
- function nextHash
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/CoordinatorApp.scala` — class CoordinatorApp
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala`
- class BeansProducer
- function redissonClient
- function kubernetesClient
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/CoordinatorConfig.scala`
- class CoordinatorConfig
- function maxGamesPerCore
- function maxDeviationPercent
- function rebalanceInterval
- function rebalanceMinInterval
- function heartbeatTtl
- _...11 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoordinatorGrpcServer.scala` — class CoordinatorGrpcServer
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoreGrpcClient.scala`
- class CoreGrpcClient
- function shutdown
- function batchResubscribeGames
- function unsubscribeGames
- function evictGames
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/resource/CoordinatorResource.scala`
- class CoordinatorResource
- function listInstances
- function getMetrics
- function triggerRebalance
- function triggerFailover
- function triggerScaleUp
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/AutoScaler.scala`
- class AutoScaler
- function checkAndScale
- function scaleUp
- function scaleDown
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala`
- class CacheEvictionManager
- function setRedisPrefix
- function evictStaleGames
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala`
- class FailoverService
- function setRedisPrefix
- function onInstanceStreamDropped
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala`
- class HealthMonitor
- function setRedisPrefix
- function checkInstanceHealth
- function watchK8sPods
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala`
- class InstanceRegistry
- function setRedisPrefix
- function getInstance
- function getAllInstances
- function updateInstanceFromRedis
- function markInstanceDead
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala`
- class LoadBalancer
- function setRedisPrefix
- function shouldRebalance
- function rebalance
- `modules/core/src/main/scala/de/nowchess/chess/adapter/RuleSetRestAdapter.scala`
- class RuleSetRestAdapter
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...5 more_
- `modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala`
- class IoServiceClient
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- function exportCombined
- `modules/core/src/main/scala/de/nowchess/chess/client/RuleServiceClient.scala`
- class RuleServiceClient
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/core/src/main/scala/de/nowchess/chess/client/StoreServiceClient.scala` — class StoreServiceClient, function getGame
- `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`
- class Command
- function execute
- function undo
- function description
- class MoveResult
- `modules/core/src/main/scala/de/nowchess/chess/command/CommandInvoker.scala`
- class CommandInvoker
- function execute
- function undo
- function redo
- function history
- function getCurrentIndex
- _...3 more_
- `modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala` — class RedisConfig
- `modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala`
- class RedissonProducer
- function produceRedissonClient
- function shutdown
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — class Parser, function parseMove
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`
- class GameEngine
- function board
- function turn
- function context
- function pendingDrawOfferBy
- function currentClockState
- _...22 more_
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiException.scala`
- class ApiException
- class GameNotFoundException
- class BadRequestException
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiExceptionMapper.scala` — class ApiExceptionMapper, function toResponse
- `modules/core/src/main/scala/de/nowchess/chess/grpc/CoordinatorServiceHandler.scala` — class CoordinatorServiceHandler
- `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`
- class CoreProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/core/src/main/scala/de/nowchess/chess/grpc/IoGrpcClientWrapper.scala`
- class IoGrpcClientWrapper
- function exportCombined
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- `modules/core/src/main/scala/de/nowchess/chess/grpc/RuleSetGrpcAdapter.scala`
- class RuleSetGrpcAdapter
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...5 more_
- function canUndo
- function canRedo
- _...11 more_
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
- function context
- class Observer
@@ -443,52 +203,28 @@
- function subscribe
- function unsubscribe
- _...1 more_
- `modules/core/src/main/scala/de/nowchess/chess/redis/C2sMessage.scala` — class C2sMessage
- `modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala` — class GameRedisPublisher, function onGameEvent
- `modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala`
- class GameRedisSubscriberManager
- function subscribeGame
- function onMessage
- function unsubscribeGame
- function batchResubscribeGames
- function unsubscribeGames
- _...3 more_
- `modules/core/src/main/scala/de/nowchess/chess/registry/GameRegistry.scala`
- class GameRegistry
- function store
- function get
- function update
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala`
- class RedisGameRegistry
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala`
- class GameRegistryImpl
- function store
- function get
- function update
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameDtoMapper.scala`
- class GameDtoMapper
- function statusOf
- function moveToUci
- function toPlayerDto
- function toClockDto
- function toGameStateDto
- _...1 more_
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`
- class GameResource
- function onGameEvent
- function createGame
- function getGame
- function streamGame
- function onGameEvent
- function resignGame
- function makeMove
- _...9 more_
- `modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala`
- class InstanceHeartbeatService
- function onStart
- function onShutdown
- function setRedisPrefix
- function setSubscriptionCount
- function setLocalCacheSize
- _...2 more_
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
- class GameFileService
- function saveGameToFile
@@ -517,15 +253,6 @@
- function parseBoard
- function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
- `modules/io/src/main/scala/de/nowchess/io/grpc/IoGrpcService.scala` — class IoGrpcService
- `modules/io/src/main/scala/de/nowchess/io/grpc/IoProtoMapper.scala`
- class IoProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
@@ -538,76 +265,20 @@
- function importGameContext
- function parsePgn
- function parseAlgebraicMove
- `modules/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala`
- class IoResource
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- function exportCombined
- `modules/json/src/main/scala/de/nowchess/json/ChessJacksonModule.scala` — class ChessJacksonModule
- `modules/json/src/main/scala/de/nowchess/json/GameResultDeserializer.scala` — class GameResultDeserializer
- `modules/json/src/main/scala/de/nowchess/json/GameResultSerializer.scala` — class GameResultSerializer
- `modules/json/src/main/scala/de/nowchess/json/MoveTypeDeserializer.scala` — class MoveTypeDeserializer
- `modules/json/src/main/scala/de/nowchess/json/MoveTypeSerializer.scala` — class MoveTypeSerializer
- `modules/json/src/main/scala/de/nowchess/json/SquareDeserializer.scala` — class SquareDeserializer
- `modules/json/src/main/scala/de/nowchess/json/SquareKeyDeserializer.scala` — class SquareKeyDeserializer
- `modules/json/src/main/scala/de/nowchess/json/SquareKeySerializer.scala` — class SquareKeySerializer
- `modules/json/src/main/scala/de/nowchess/json/SquareSerializer.scala` — class SquareSerializer
- `modules/rule/src/main/scala/de/nowchess/rules/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/rule/src/main/scala/de/nowchess/rules/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/rule/src/main/scala/de/nowchess/rules/grpc/ProtoMapper.scala`
- class ProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/rule/src/main/scala/de/nowchess/rules/grpc/RuleGrpcService.scala` — class RuleGrpcService
- `modules/rule/src/main/scala/de/nowchess/rules/resource/RuleSetResource.scala`
- class RuleSetResource
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
- class RuleSet
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — class DefaultRules, function positionOf
- `modules/store/src/main/scala/de/nowchess/store/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/store/src/main/scala/de/nowchess/store/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala` — class RedisConfig
- `modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala`
- class RedissonProducer
- function redissonClient
- function close
- `modules/store/src/main/scala/de/nowchess/store/domain/GameRecord.scala` — class GameRecord
- `modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala`
- class GameWritebackStreamListener
- function startListening
- function onMessage
- `modules/store/src/main/scala/de/nowchess/store/repository/GameRecordRepository.scala`
- class GameRecordRepository
- function findByGameId
- function persist
- function merge
- `modules/store/src/main/scala/de/nowchess/store/resource/StoreGameResource.scala` — class StoreGameResource, function getGame
- `modules/store/src/main/scala/de/nowchess/store/service/GameWritebackService.scala` — class GameWritebackService, function writeBack
- `modules/ws/src/main/scala/de/nowchess/ws/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala` — class RedisConfig
- `modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala`
- class RedissonProducer
- function produceRedissonClient
- function shutdown
- `modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala`
- class GameWebSocketResource
- function onOpen
- function onMessage
- function onTextMessage
- function onClose
- _...5 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
- class DefaultRules
- function positionOf
- function loop
- function toMoves
- function loop
---
@@ -630,39 +301,39 @@
## Most Imported Files (change these carefully)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **76** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **57** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **55** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **47** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **28** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **13** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **11** files
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/error/GameError.scala` — imported by **9** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala` — imported by **8** files
- `modules/core/src/main/scala/de/nowchess/chess/grpc/IoGrpcClientWrapper.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameMode.scala` — imported by **6** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **64** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **44** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **40** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **35** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **19** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **17** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **11** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **7** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala` — imported by **6** files
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala` — imported by **6** files
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — imported by **5** files
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — imported by **4** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala`, `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +71 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala` +52 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala` +50 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/ClockState.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/ClockStateTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala` +42 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +23 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala``modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala``modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`, `modules/core/src/main/scala/de/nowchess/chess/resource/GameDtoMapper.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala` +59 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +39 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +35 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala` +30 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +14 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +12 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +6 more
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala``modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala` +5 more
---
+29 -29
View File
@@ -2,36 +2,36 @@
## Most Imported Files (change these carefully)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **76** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **57** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **55** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **47** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **28** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **20** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **13** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **11** files
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/error/GameError.scala` — imported by **9** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala` — imported by **8** files
- `modules/core/src/main/scala/de/nowchess/chess/grpc/IoGrpcClientWrapper.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameMode.scala` — imported by **6** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **64** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **44** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **40** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **35** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **19** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **18** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **17** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **11** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **7** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala` — imported by **6** files
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala` — imported by **6** files
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — imported by **5** files
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — imported by **4** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala`, `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +71 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala` +52 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala` +50 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/ClockState.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/ClockStateTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala` +42 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +23 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +15 more
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala``modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala``modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`, `modules/core/src/main/scala/de/nowchess/chess/resource/GameDtoMapper.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala` +59 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +39 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +35 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala` +30 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +14 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +13 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +12 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala``modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +6 more
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala``modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala` +5 more
+33 -315
View File
@@ -16,74 +16,6 @@
- function main: () -> None
- class TestCase
- _...2 more_
- `modules/account/src/main/scala/de/nowchess/account/client/CoreGameClient.scala` — class CoreGameClient, function createGame
- `modules/account/src/main/scala/de/nowchess/account/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/account/src/main/scala/de/nowchess/account/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/account/src/main/scala/de/nowchess/account/domain/Challenge.scala`
- class Challenge
- function gameIdOpt
- function declineReasonOpt
- function timeControlLimitOpt
- function timeControlIncrementOpt
- `modules/account/src/main/scala/de/nowchess/account/domain/ChallengeColorConverter.scala` — class ChallengeColorConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/ChallengeStatusConverter.scala` — class ChallengeStatusConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/DeclineReasonConverter.scala` — class DeclineReasonConverter
- `modules/account/src/main/scala/de/nowchess/account/domain/TimeControl.scala` — class TimeControl
- `modules/account/src/main/scala/de/nowchess/account/domain/UserAccount.scala`
- class UserAccount
- function getBotAccounts
- class BotAccount
- class OfficialBotAccount
- `modules/account/src/main/scala/de/nowchess/account/error/AccountError.scala` — function message
- `modules/account/src/main/scala/de/nowchess/account/error/ChallengeError.scala` — function message
- `modules/account/src/main/scala/de/nowchess/account/filter/AlreadyLoggedInFilter.scala` — class AlreadyLoggedInFilter
- `modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala`
- class UserAccountRepository
- function findByUsername
- function findById
- function persist
- function findByEmail
- function findAll
- _...12 more_
- `modules/account/src/main/scala/de/nowchess/account/repository/ChallengeRepository.scala`
- class ChallengeRepository
- function findActiveByChallengerId
- function findActiveByDestUserId
- function findDuplicateChallenge
- function findById
- function persist
- _...1 more_
- `modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala`
- class AccountResource
- function register
- function login
- function me
- function publicProfile
- function banUser
- _...10 more_
- `modules/account/src/main/scala/de/nowchess/account/resource/ChallengeResource.scala`
- class ChallengeResource
- function create
- function list
- function accept
- function decline
- function cancel
- `modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala`
- class AccountService
- function register
- function login
- function findByUsername
- function findById
- function createBotAccount
- _...11 more_
- `modules/account/src/main/scala/de/nowchess/account/service/ChallengeService.scala`
- class ChallengeService
- function create
- function accept
- function decline
- function cancel
- function listForUser
- _...1 more_
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala`
- class Board
- function apply
@@ -113,15 +45,6 @@
- `modules/api/src/main/scala/de/nowchess/api/dto/ErrorEventDto.scala` — class ErrorEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/dto/GameFullEventDto.scala` — class GameFullEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/dto/GameStateEventDto.scala` — class GameStateEventDto, function apply
- `modules/api/src/main/scala/de/nowchess/api/error/GameError.scala` — function message
- `modules/api/src/main/scala/de/nowchess/api/game/ClockState.scala`
- function activeColor
- function afterMove
- function remainingMs
- function remainingMs
- function afterMove
- function remainingMs
- _...3 more_
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`
- function kingSquare
- function withBoard
@@ -130,21 +53,11 @@
- function withEnPassantSquare
- function withHalfMoveClock
- _...4 more_
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
- class ApiResponse
- function error
- function totalPages
- `modules/api/src/main/scala/de/nowchess/api/rules/RuleSet.scala`
- class RuleSet
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/bot/python/nnue.py`
- function get_weights_dir: ()
- function get_data_dir: ()
@@ -249,136 +162,30 @@
- class ZobristHash
- function hash
- function nextHash
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/CoordinatorApp.scala` — class CoordinatorApp
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/BeansProducer.scala`
- class BeansProducer
- function redissonClient
- function kubernetesClient
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/CoordinatorConfig.scala`
- class CoordinatorConfig
- function maxGamesPerCore
- function maxDeviationPercent
- function rebalanceInterval
- function rebalanceMinInterval
- function heartbeatTtl
- _...11 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoordinatorGrpcServer.scala` — class CoordinatorGrpcServer
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/grpc/CoreGrpcClient.scala`
- class CoreGrpcClient
- function shutdown
- function batchResubscribeGames
- function unsubscribeGames
- function evictGames
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/resource/CoordinatorResource.scala`
- class CoordinatorResource
- function listInstances
- function getMetrics
- function triggerRebalance
- function triggerFailover
- function triggerScaleUp
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/AutoScaler.scala`
- class AutoScaler
- function checkAndScale
- function scaleUp
- function scaleDown
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/CacheEvictionManager.scala`
- class CacheEvictionManager
- function setRedisPrefix
- function evictStaleGames
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/FailoverService.scala`
- class FailoverService
- function setRedisPrefix
- function onInstanceStreamDropped
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/HealthMonitor.scala`
- class HealthMonitor
- function setRedisPrefix
- function checkInstanceHealth
- function watchK8sPods
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/InstanceRegistry.scala`
- class InstanceRegistry
- function setRedisPrefix
- function getInstance
- function getAllInstances
- function updateInstanceFromRedis
- function markInstanceDead
- _...1 more_
- `modules/coordinator/src/main/scala/de/nowchess/coordinator/service/LoadBalancer.scala`
- class LoadBalancer
- function setRedisPrefix
- function shouldRebalance
- function rebalance
- `modules/core/src/main/scala/de/nowchess/chess/adapter/RuleSetRestAdapter.scala`
- class RuleSetRestAdapter
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...5 more_
- `modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala`
- class IoServiceClient
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- function exportCombined
- `modules/core/src/main/scala/de/nowchess/chess/client/RuleServiceClient.scala`
- class RuleServiceClient
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/core/src/main/scala/de/nowchess/chess/client/StoreServiceClient.scala` — class StoreServiceClient, function getGame
- `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`
- class Command
- function execute
- function undo
- function description
- class MoveResult
- `modules/core/src/main/scala/de/nowchess/chess/command/CommandInvoker.scala`
- class CommandInvoker
- function execute
- function undo
- function redo
- function history
- function getCurrentIndex
- _...3 more_
- `modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/core/src/main/scala/de/nowchess/chess/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/core/src/main/scala/de/nowchess/chess/config/RedisConfig.scala` — class RedisConfig
- `modules/core/src/main/scala/de/nowchess/chess/config/RedissonProducer.scala`
- class RedissonProducer
- function produceRedissonClient
- function shutdown
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — class Parser, function parseMove
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`
- class GameEngine
- function board
- function turn
- function context
- function pendingDrawOfferBy
- function currentClockState
- _...22 more_
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiException.scala`
- class ApiException
- class GameNotFoundException
- class BadRequestException
- `modules/core/src/main/scala/de/nowchess/chess/exception/ApiExceptionMapper.scala` — class ApiExceptionMapper, function toResponse
- `modules/core/src/main/scala/de/nowchess/chess/grpc/CoordinatorServiceHandler.scala` — class CoordinatorServiceHandler
- `modules/core/src/main/scala/de/nowchess/chess/grpc/CoreProtoMapper.scala`
- class CoreProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/core/src/main/scala/de/nowchess/chess/grpc/IoGrpcClientWrapper.scala`
- class IoGrpcClientWrapper
- function exportCombined
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- `modules/core/src/main/scala/de/nowchess/chess/grpc/RuleSetGrpcAdapter.scala`
- class RuleSetGrpcAdapter
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...5 more_
- function canUndo
- function canRedo
- _...11 more_
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
- function context
- class Observer
@@ -387,52 +194,28 @@
- function subscribe
- function unsubscribe
- _...1 more_
- `modules/core/src/main/scala/de/nowchess/chess/redis/C2sMessage.scala` — class C2sMessage
- `modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisPublisher.scala` — class GameRedisPublisher, function onGameEvent
- `modules/core/src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala`
- class GameRedisSubscriberManager
- function subscribeGame
- function onMessage
- function unsubscribeGame
- function batchResubscribeGames
- function unsubscribeGames
- _...3 more_
- `modules/core/src/main/scala/de/nowchess/chess/registry/GameRegistry.scala`
- class GameRegistry
- function store
- function get
- function update
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/RedisGameRegistry.scala`
- class RedisGameRegistry
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala`
- class GameRegistryImpl
- function store
- function get
- function update
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameDtoMapper.scala`
- class GameDtoMapper
- function statusOf
- function moveToUci
- function toPlayerDto
- function toClockDto
- function toGameStateDto
- _...1 more_
- function generateId
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`
- class GameResource
- function onGameEvent
- function createGame
- function getGame
- function streamGame
- function onGameEvent
- function resignGame
- function makeMove
- _...9 more_
- `modules/core/src/main/scala/de/nowchess/chess/service/InstanceHeartbeatService.scala`
- class InstanceHeartbeatService
- function onStart
- function onShutdown
- function setRedisPrefix
- function setSubscriptionCount
- function setLocalCacheSize
- _...2 more_
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
- class GameFileService
- function saveGameToFile
@@ -461,15 +244,6 @@
- function parseBoard
- function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
- `modules/io/src/main/scala/de/nowchess/io/grpc/IoGrpcService.scala` — class IoGrpcService
- `modules/io/src/main/scala/de/nowchess/io/grpc/IoProtoMapper.scala`
- class IoProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
@@ -482,73 +256,17 @@
- function importGameContext
- function parsePgn
- function parseAlgebraicMove
- `modules/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/io/src/main/scala/de/nowchess/io/service/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala`
- class IoResource
- function importFen
- function importPgn
- function exportFen
- function exportPgn
- function exportCombined
- `modules/json/src/main/scala/de/nowchess/json/ChessJacksonModule.scala` — class ChessJacksonModule
- `modules/json/src/main/scala/de/nowchess/json/GameResultDeserializer.scala` — class GameResultDeserializer
- `modules/json/src/main/scala/de/nowchess/json/GameResultSerializer.scala` — class GameResultSerializer
- `modules/json/src/main/scala/de/nowchess/json/MoveTypeDeserializer.scala` — class MoveTypeDeserializer
- `modules/json/src/main/scala/de/nowchess/json/MoveTypeSerializer.scala` — class MoveTypeSerializer
- `modules/json/src/main/scala/de/nowchess/json/SquareDeserializer.scala` — class SquareDeserializer
- `modules/json/src/main/scala/de/nowchess/json/SquareKeyDeserializer.scala` — class SquareKeyDeserializer
- `modules/json/src/main/scala/de/nowchess/json/SquareKeySerializer.scala` — class SquareKeySerializer
- `modules/json/src/main/scala/de/nowchess/json/SquareSerializer.scala` — class SquareSerializer
- `modules/rule/src/main/scala/de/nowchess/rules/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/rule/src/main/scala/de/nowchess/rules/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/rule/src/main/scala/de/nowchess/rules/grpc/ProtoMapper.scala`
- class ProtoMapper
- function toProtoColor
- function fromProtoColor
- function toProtoPieceType
- function fromProtoPieceType
- function toProtoMoveKind
- _...9 more_
- `modules/rule/src/main/scala/de/nowchess/rules/grpc/RuleGrpcService.scala` — class RuleGrpcService
- `modules/rule/src/main/scala/de/nowchess/rules/resource/RuleSetResource.scala`
- class RuleSetResource
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
- class RuleSet
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...6 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — class DefaultRules, function positionOf
- `modules/store/src/main/scala/de/nowchess/store/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/store/src/main/scala/de/nowchess/store/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/store/src/main/scala/de/nowchess/store/config/RedisConfig.scala` — class RedisConfig
- `modules/store/src/main/scala/de/nowchess/store/config/RedissonProducer.scala`
- class RedissonProducer
- function redissonClient
- function close
- `modules/store/src/main/scala/de/nowchess/store/domain/GameRecord.scala` — class GameRecord
- `modules/store/src/main/scala/de/nowchess/store/redis/GameWritebackStreamListener.scala`
- class GameWritebackStreamListener
- function startListening
- function onMessage
- `modules/store/src/main/scala/de/nowchess/store/repository/GameRecordRepository.scala`
- class GameRecordRepository
- function findByGameId
- function persist
- function merge
- `modules/store/src/main/scala/de/nowchess/store/resource/StoreGameResource.scala` — class StoreGameResource, function getGame
- `modules/store/src/main/scala/de/nowchess/store/service/GameWritebackService.scala` — class GameWritebackService, function writeBack
- `modules/ws/src/main/scala/de/nowchess/ws/config/JacksonConfig.scala` — class JacksonConfig, function customize
- `modules/ws/src/main/scala/de/nowchess/ws/config/NativeReflectionConfig.scala` — class NativeReflectionConfig
- `modules/ws/src/main/scala/de/nowchess/ws/config/RedisConfig.scala` — class RedisConfig
- `modules/ws/src/main/scala/de/nowchess/ws/config/RedissonProducer.scala`
- class RedissonProducer
- function produceRedissonClient
- function shutdown
- `modules/ws/src/main/scala/de/nowchess/ws/resource/GameWebSocketResource.scala`
- class GameWebSocketResource
- function onOpen
- function onMessage
- function onTextMessage
- function onClose
- _...5 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
- class DefaultRules
- function positionOf
- function loop
- function toMoves
- function loop
-44
View File
@@ -1,44 +0,0 @@
# Routes
## gRPC
- `/CoordinatorService/BatchResubscribeGames` (BatchResubscribeRequest) → BatchResubscribeResponse
- `/CoordinatorService/UnsubscribeGames` (UnsubscribeGamesRequest) → UnsubscribeGamesResponse
- `/CoordinatorService/EvictGames` (EvictGamesRequest) → EvictGamesResponse
- `/CoordinatorService/DrainInstance` (DrainInstanceRequest) → DrainInstanceResponse
- `/CoordinatorService/BatchResubscribeGames` (BatchResubscribeRequest) → BatchResubscribeResponse
- `/CoordinatorService/UnsubscribeGames` (UnsubscribeGamesRequest) → UnsubscribeGamesResponse
- `/CoordinatorService/EvictGames` (EvictGamesRequest) → EvictGamesResponse
- `/CoordinatorService/DrainInstance` (DrainInstanceRequest) → DrainInstanceResponse
- `/IoService/ImportFen` (ProtoImportFenRequest) → ProtoGameContext
- `/IoService/ImportPgn` (ProtoImportPgnRequest) → ProtoGameContext
- `/IoService/ExportCombined` (ProtoGameContext) → ProtoCombinedExport
- `/IoService/ExportFen` (ProtoGameContext) → ProtoStringResult
- `/IoService/ExportPgn` (ProtoGameContext) → ProtoStringResult
- `/RuleService/CandidateMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/LegalMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/AllLegalMoves` (ProtoGameContext) → ProtoMoveList
- `/RuleService/IsCheck` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsCheckmate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsStalemate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsInsufficientMaterial` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsFiftyMoveRule` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsThreefoldRepetition` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/ApplyMove` (ProtoMoveRequest) → ProtoGameContext
- `/RuleService/PostMoveStatus` (ProtoGameContext) → ProtoPostMoveStatus
- `/IoService/ImportFen` (ProtoImportFenRequest) → ProtoGameContext
- `/IoService/ImportPgn` (ProtoImportPgnRequest) → ProtoGameContext
- `/IoService/ExportCombined` (ProtoGameContext) → ProtoCombinedExport
- `/IoService/ExportFen` (ProtoGameContext) → ProtoStringResult
- `/IoService/ExportPgn` (ProtoGameContext) → ProtoStringResult
- `/RuleService/CandidateMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/LegalMoves` (ProtoSquareRequest) → ProtoMoveList
- `/RuleService/AllLegalMoves` (ProtoGameContext) → ProtoMoveList
- `/RuleService/IsCheck` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsCheckmate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsStalemate` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsInsufficientMaterial` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsFiftyMoveRule` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/IsThreefoldRepetition` (ProtoGameContext) → ProtoBoolResult
- `/RuleService/ApplyMove` (ProtoMoveRequest) → ProtoGameContext
- `/RuleService/PostMoveStatus` (ProtoGameContext) → ProtoPostMoveStatus
+3 -3
View File
@@ -1,6 +1,6 @@
# NowChessSystems — Wiki
_Generated 2026-04-23 — re-run `npx codesight --wiki` if the codebase has changed._
_Generated 2026-04-12 — re-run `npx codesight --wiki` if the codebase has changed._
Structural map compiled from source code via AST. No LLM — deterministic, 200ms.
@@ -15,7 +15,7 @@ Structural map compiled from source code via AST. No LLM — deterministic, 200m
- Routes: **0**
- Models: **0**
- Components: **0**
- Env vars: **1** required, **0** with defaults
- Env vars: **0** required, **0** with defaults
## How to Use
@@ -41,4 +41,4 @@ These exist in your codebase but are **not** reflected in wiki articles:
When in doubt, search the source. The wiki is a starting point, not a complete inventory.
---
_Last compiled: 2026-04-23 · 2 articles · [codesight](https://github.com/Houseofmvps/codesight)_
_Last compiled: 2026-04-12 · 2 articles · [codesight](https://github.com/Houseofmvps/codesight)_
-2
View File
@@ -3,5 +3,3 @@
History of `npx codesight --wiki` runs. Capped at 20 entries.
## [2026-04-12 14:34:19] scan | 0 routes, 0 models, 0 components → 2 articles
## [2026-04-23 11:41:43] scan | 0 routes, 0 models, 0 components → 2 articles
+7 -15
View File
@@ -4,24 +4,16 @@
**NowChessSystems** is a scala project built with raw-http.
## Scale
1 middleware layers · 1 environment variables
## High-Impact Files
Changes to these files have the widest blast radius across the codebase:
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **74** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **66** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **52** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **42** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **27** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** files
## Required Environment Variables
- `STOCKFISH_PATH``modules/bot/python/nnue.py`
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **28** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **19** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **13** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **10** files
---
_Back to [index.md](./index.md) · Generated 2026-04-23_
_Back to [index.md](./index.md) · Generated 2026-04-12_
-12
View File
@@ -1,12 +0,0 @@
FROM mcr.microsoft.com/devcontainers/java:21-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
redis-tools \
stockfish \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /home/vscode/.gradle/wrapper/dists \
&& chown -R vscode:vscode /home/vscode/.gradle
-18
View File
@@ -1,18 +0,0 @@
# Devcontainer
Dieses Setup startet den NowChess-Workspace zusammen mit Redis und PostgreSQL.
## Enthaltene Services
- `workspace` Scala/Gradle-Entwicklungscontainer
- `redis` Redis 7.4
- `postgres` PostgreSQL 16
## Wichtige Ports
- App-Services: `8080`, `8081`, `8082`, `8083`, `8084`, `8085`, `8086`, `9086`
- Redis auf dem Host: `16379`
- PostgreSQL auf dem Host: `15432`
## Einstieg
- VS Code: Ordner in einem Dev Container öffnen
- IntelliJ: Dev Container / Docker-Compose-Workspace öffnen und den `workspace`-Dienst nutzen
-39
View File
@@ -1,39 +0,0 @@
{
"name": "NowChessSystems",
"dockerComposeFile": ["docker-compose.yml"],
"service": "workspace",
"workspaceFolder": "/workspaces/NowChessSystems",
"shutdownAction": "stopCompose",
"overrideCommand": false,
"remoteUser": "vscode",
"forwardPorts": [8080, 8081, 8082, 8083, 8084, 8085, 8086, 9086],
"portsAttributes": {
"8080": {"label": "NowChess Core", "onAutoForward": "notify"},
"8081": {"label": "NowChess Io", "onAutoForward": "notify"},
"8082": {"label": "NowChess Rule", "onAutoForward": "notify"},
"8083": {"label": "NowChess Account", "onAutoForward": "notify"},
"8084": {"label": "NowChess WebSocket", "onAutoForward": "notify"},
"8085": {"label": "NowChess Store", "onAutoForward": "notify"},
"8086": {"label": "NowChess Coordinator HTTP", "onAutoForward": "notify"},
"9086": {"label": "NowChess Coordinator gRPC", "onAutoForward": "notify"}
},
"customizations": {
"vscode": {
"settings": {
"java.configuration.updateBuildConfiguration": "automatic",
"java.import.gradle.wrapper.enabled": true,
"files.watcherExclude": {
"**/build/**": true,
"**/.gradle/**": true
}
},
"extensions": [
"scala-lang.scala",
"redhat.java",
"vscjava.vscode-java-pack"
]
}
},
"postCreateCommand": "bash -lc './gradlew --no-daemon help >/dev/null'"
}
-65
View File
@@ -1,65 +0,0 @@
services:
workspace:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
command: sleep infinity
init: true
volumes:
- gradle-cache:/home/vscode/.gradle
environment:
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PREFIX: nowchess
DB_URL: jdbc:postgresql://postgres:5432/nowchess
DB_USER: nowchess
DB_PASSWORD: nowchess
CORE_SERVICE_URL: http://localhost:8080
INTERNAL_SECRET: dev-internal-secret
STOCKFISH_PATH: /usr/games/stockfish
NOWCHESS_COORDINATOR_ENABLED: "true"
dns:
- 8.8.8.8
- 1.1.1.1
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
redis:
image: docker.io/redis:7.4-alpine
command: ["redis-server", "--appendonly", "yes", "--save", "60", "1"]
ports:
- "16379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
start_period: 5s
postgres:
image: docker.io/postgres:16-alpine
environment:
POSTGRES_DB: nowchess
POSTGRES_USER: nowchess
POSTGRES_PASSWORD: nowchess
ports:
- "15432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
volumes:
gradle-cache:
redis-data:
postgres-data:
-114
View File
@@ -1,114 +0,0 @@
name: Build & Push Native Image
on:
push:
branches:
- main
workflow_dispatch:
jobs:
check-actor:
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.check.outputs.allowed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- id: check
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "allowed=true" >> "$GITHUB_OUTPUT"
else
COMMIT_AUTHOR=$(git log -1 --format='%an')
if [[ "$COMMIT_AUTHOR" == "TeamCity" ]]; then
echo "allowed=true" >> "$GITHUB_OUTPUT"
else
echo "allowed=false" >> "$GITHUB_OUTPUT"
fi
fi
build-and-push:
needs: check-actor
if: needs.check-actor.outputs.allowed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
module:
- account
- bot-platform
- coordinator
- core
- io
- official-bots
- rule
- store
- ws
steps:
- uses: actions/checkout@v4
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm-community'
native-image-job-reports: 'true'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Read version from versions.env
id: version
run: |
if [ -f "modules/${{ matrix.module }}/versions.env" ]; then
source modules/${{ matrix.module }}/versions.env
VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
else
echo "version=latest" >> "$GITHUB_OUTPUT"
fi
- name: Build native binary
run: ./gradlew :modules:${{ matrix.module }}:build -x test -Dquarkus.native.enabled=true -Dquarkus.package.jar.enabled=false -Dquarkus.profile=deployed --no-daemon
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/now-chess/now-chess-systems/${{ matrix.module }}
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: modules/${{ matrix.module }}/src/main/docker/Dockerfile.native
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.module }}
cache-to: type=gha,mode=max,scope=${{ matrix.module }}
-3
View File
@@ -41,9 +41,6 @@ bin/
graphify-out/
.graphify_*.json
### Keys ###
**/keys/private.pem
### Mac OS ###
.DS_Store
/jacoco-reporter/.venv/
-18
View File
File diff suppressed because one or more lines are too long
+2 -9
View File
@@ -5,23 +5,16 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="ms-21" />
<option name="gradleJvm" value="corretto-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/modules" />
<option value="$PROJECT_DIR$/modules/account" />
<option value="$PROJECT_DIR$/modules/api" />
<option value="$PROJECT_DIR$/modules/bot-platform" />
<option value="$PROJECT_DIR$/modules/coordinator" />
<option value="$PROJECT_DIR$/modules/bot" />
<option value="$PROJECT_DIR$/modules/core" />
<option value="$PROJECT_DIR$/modules/io" />
<option value="$PROJECT_DIR$/modules/json" />
<option value="$PROJECT_DIR$/modules/official-bots" />
<option value="$PROJECT_DIR$/modules/rule" />
<option value="$PROJECT_DIR$/modules/security" />
<option value="$PROJECT_DIR$/modules/store" />
<option value="$PROJECT_DIR$/modules/ws" />
</set>
</option>
</GradleProjectSettings>
+1 -2
View File
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+1 -1
View File
@@ -5,7 +5,7 @@
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
</profile>
<profile name="Gradle 2" modules="NowChessSystems.modules.account.integrationTest,NowChessSystems.modules.account.main,NowChessSystems.modules.account.native-test,NowChessSystems.modules.account.quarkus-generated-sources,NowChessSystems.modules.account.quarkus-test-generated-sources,NowChessSystems.modules.account.scoverage,NowChessSystems.modules.account.test,NowChessSystems.modules.bot-platform.integrationTest,NowChessSystems.modules.bot-platform.main,NowChessSystems.modules.bot-platform.native-test,NowChessSystems.modules.bot-platform.quarkus-generated-sources,NowChessSystems.modules.bot-platform.quarkus-test-generated-sources,NowChessSystems.modules.bot-platform.scoverage,NowChessSystems.modules.bot-platform.test,NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.coordinator.integrationTest,NowChessSystems.modules.coordinator.main,NowChessSystems.modules.coordinator.native-test,NowChessSystems.modules.coordinator.quarkus-generated-sources,NowChessSystems.modules.coordinator.quarkus-test-generated-sources,NowChessSystems.modules.coordinator.scoverage,NowChessSystems.modules.coordinator.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.integrationTest,NowChessSystems.modules.io.main,NowChessSystems.modules.io.native-test,NowChessSystems.modules.io.quarkus-generated-sources,NowChessSystems.modules.io.quarkus-test-generated-sources,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.json.main,NowChessSystems.modules.json.scoverage,NowChessSystems.modules.json.test,NowChessSystems.modules.official-bots.integrationTest,NowChessSystems.modules.official-bots.main,NowChessSystems.modules.official-bots.native-test,NowChessSystems.modules.official-bots.quarkus-generated-sources,NowChessSystems.modules.official-bots.quarkus-test-generated-sources,NowChessSystems.modules.official-bots.scoverage,NowChessSystems.modules.official-bots.test,NowChessSystems.modules.rule.integrationTest,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.native-test,NowChessSystems.modules.rule.quarkus-generated-sources,NowChessSystems.modules.rule.quarkus-test-generated-sources,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.security.main,NowChessSystems.modules.security.scoverage,NowChessSystems.modules.security.test,NowChessSystems.modules.store.integrationTest,NowChessSystems.modules.store.main,NowChessSystems.modules.store.native-test,NowChessSystems.modules.store.quarkus-generated-sources,NowChessSystems.modules.store.quarkus-test-generated-sources,NowChessSystems.modules.store.scoverage,NowChessSystems.modules.store.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test,NowChessSystems.modules.ws.integrationTest,NowChessSystems.modules.ws.main,NowChessSystems.modules.ws.native-test,NowChessSystems.modules.ws.quarkus-generated-sources,NowChessSystems.modules.ws.quarkus-test-generated-sources,NowChessSystems.modules.ws.scoverage,NowChessSystems.modules.ws.test">
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.integrationTest,NowChessSystems.modules.io.main,NowChessSystems.modules.io.native-test,NowChessSystems.modules.io.quarkus-generated-sources,NowChessSystems.modules.io.quarkus-test-generated-sources,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
<parameters>
+1
View File
@@ -22,6 +22,7 @@ Use consistently.
| `rule` | Game rules | api |
| `bot` | Bots and AI | api,rule,io |
| `io` | Export formats | api, core |
| `ui` | Entrypoint & UI | core, io |
## Style
+1
View File
@@ -22,6 +22,7 @@ Try to stick to these commands for consistency.
| `rule` | Game rules | api |
| `bot` | Bots and AI | api,rule,io |
| `io` | Export formats | api, core |
| `ui` | Entrypoint & UI | core, io |
## Style
@@ -1,334 +0,0 @@
# Plan: Add Coordinator Microservice
## Context
NowChess scales `core` horizontally via shared Redis but lacks:
- **Instance visibility**: no way to list running cores or their load
- **Load balancing**: games land randomly on cores; no rebalancing
- **Failover**: dead cores orphan subscriptions; bullet chess requires <1s recovery
- **Auto-scaling**: manual ops to add/remove cores
- **Cache management**: no eviction of stale games from core memory
Bullet chess games run on move timings of <3s. 30s failover = game lost on clock. Target: **<300ms failover**.
---
## Architecture: Sub-1s Failover
### Why Not Polling/TTL
- TTL expiry: minimum 10-30s detection
- HTTP polling 3x failure: 30s minimum
- **gRPC streaming TCP drop: 50-200ms** — use this as primary
### Primary: gRPC Bidirectional Streaming
- Core opens a **persistent bidirectional stream** (`CoreHeartbeatStream`) to coordinator on startup
- Core sends heartbeat frames every **200ms**
- Core crash = TCP RST/FIN → coordinator stream error in **~50-200ms**
- Stream also carries metadata updates (subscription count changes) in real-time
### Fallback: Redis Heartbeat + K8s Watch
- Redis heartbeat key `{prefix}:instances:{instanceId}` with **5s TTL**, refreshed every **2s**
- K8s pod watch via Kubernetes Java client (event-driven; handles pod eviction/OOMKill)
- Fallback covers: network partition (TCP stays up but core is zombie), coordinator restart gap
---
## Design
### 1. Module: `modules/coordinator`
**Language**: Scala 3.5.1, Quarkus REST + gRPC
**Ports**: HTTP 8086, gRPC 9086
**Dependencies**: Redisson, Kubernetes Java client, Quarkus gRPC
**Persistence**: None (all state in Redis)
---
### 2. Instance Registry
**Redis schema**:
```
{prefix}:instances:{instanceId}
- TTL: 5s (refreshed by core every 2s via background task)
- Value: JSON
{
"instanceId": "core-abc123",
"hostname": "core-pod-3",
"httpPort": 8080,
"grpcPort": 9080,
"subscriptionCount": 147,
"localCacheSize": 147,
"lastHeartbeat": "2026-04-26T10:15:30.123Z"
}
{prefix}:instance:{instanceId}:games
- Type: Redis Set (no TTL — managed explicitly)
- Members: all gameIds currently subscribed on this instance
```
**Core changes** (new `InstanceHeartbeatService` bean in `modules/core`):
- `@PostConstruct`: generate stable `instanceId` (hostname + random suffix); open gRPC stream to coordinator; publish Redis heartbeat; register in `{prefix}:instances:{instanceId}`
- Every 200ms: send heartbeat frame on gRPC stream (carries `subscriptionCount`)
- Every 2s: refresh Redis heartbeat bucket TTL
- `subscribeGame(gameId)`: `SADD {prefix}:instance:{instanceId}:games gameId`
- `unsubscribeGame(gameId)` / `evictGame(gameId)`: `SREM {prefix}:instance:{instanceId}:games gameId`
- `@PreDestroy`: delete Redis key + games set; close gRPC stream (clean shutdown)
---
### 3. Health Monitoring (3 signals, primary fast)
| Signal | Mechanism | Detection time | Role |
|--------|-----------|---------------|------|
| **gRPC stream drop** | TCP RST/FIN on bidirectional stream | 50200ms | Primary |
| **Redis heartbeat expiry** | `{prefix}:instances:{instanceId}` TTL=5s | 57s | Fallback |
| **K8s pod watch** | `CoreV1Api.listNamespacedPod` watch stream | ~instant (pod events) | Fallback |
**Dead decision**:
- gRPC stream drops → **immediate failover** (no confirmation needed; games must recover fast)
- Redis heartbeat expires (gRPC still up) → verify with single HTTP `/q/health` call → if fail: failover
- K8s pod NotReady (gRPC still up) → failover
---
### 4. Failover Protocol (<300ms target)
```
T+0ms Core JVM crashes / network drops
T+50ms Coordinator: gRPC stream error received
T+52ms SMEMBERS {prefix}:instance:{instanceId}:games → list of orphaned gameIds
T+55ms Distribute gameIds across healthy cores (least-loaded first)
T+60ms BatchResubscribeGames gRPC call(s) fire to healthy core(s)
T+150ms Healthy cores resubscribed; Redis s2c topics live again
T+200ms WebSocket clients reconnect; receive GameFullEventDto on CONNECTED
```
**Failover steps** (coordinator `FailoverService`):
1. On stream drop for `instanceId`:
a. Mark instance DEAD in local map
b. `SMEMBERS {prefix}:instance:{instanceId}:games`
c. Group gameIds into batches per target core (round-robin by load)
d. For each target core: call `BatchResubscribeGames(gameIds)`
e. Each target core: calls `subscribeGame(gameId)` for each (loads from Redis if not in local cache)
f. `DEL {prefix}:instance:{instanceId}:games` (cleanup)
2. Log failover event with count of games migrated + latency
---
### 5. Load Rebalancing
**Thresholds** (both must be evaluated):
1. **Absolute**: any core > 500 games → rebalance
2. **Relative**: max load > mean × 1.2 AND max - min > 50 games → rebalance
**Algorithm** (runs every 30s, min 60s between actual rebalances):
1. Read all `{prefix}:instances:*` keys → load map
2. Identify overloaded cores (exceed either threshold)
3. For each overloaded core: pick `excess = load - targetLoad` games
4. Assign excess games to underloaded cores
5. Call `UnsubscribeGames(gameIds)` on overloaded core
6. Call `BatchResubscribeGames(gameIds)` on target core
7. Overloaded core: `SREM` each game from its set
8. Target core: `SADD` each game to its set on subscribe
---
### 6. Auto-Scaling
**Metric**: avg `subscriptionCount` across all cores
**Actions**:
- avg > `scale-up-threshold` (80% of max): patch `nowchess-core` Argo Rollout `spec.replicas += 1`
- avg < `scale-down-threshold` (30% of max) AND `replicas > min-replicas`: drain one core then scale down
- Backoff: min 2-minute interval between scale events
**Argo Rollouts API**:
- CRD: `argoproj.io/v1alpha1`, Kind: `Rollout`, resource: `rollouts`
- Scale via Fabric8 `GenericKubernetesResource` patch on `spec.replicas`
- No StatefulSet — Argo Rollout owns pod lifecycle (canary/blue-green strategies respected)
- Pod watch filter: label selector `app=nowchess-core` (Rollout sets this; `rollouts-pod-template-hash` is Argo's equivalent of `pod-template-hash`)
**Drain before scale-down**:
1. Pick least-loaded core
2. Migrate all its games to other cores via `BatchResubscribeGames`
3. Call `DrainInstance(instanceId)` on that core (sets it to reject new subscriptions)
4. After drain confirmed: patch Rollout `spec.replicas -= 1`
---
### 7. Cache Eviction
**Trigger**: coordinator scans `{prefix}:game:entry:*` every 10 minutes
**Policy**: if `now - lastUpdated > 45min` AND `gameId` in any instance's games set → call `EvictGame`
**Effect**: core removes game from `localEngines` and `unsubscribeGame`, `SREM` from instance set
---
### 8. Proto: `coordinator_service.proto`
```proto
syntax = "proto3";
package de.nowchess.coordinator;
service CoordinatorService {
// Core → Coordinator: bidirectional stream for liveness
rpc HeartbeatStream(stream HeartbeatFrame) returns (stream CoordinatorCommand);
// Coordinator → Core: batch resubscribe after failover or rebalance
rpc BatchResubscribeGames(BatchResubscribeRequest) returns (BatchResubscribeResponse);
// Coordinator → Core: unsubscribe games (rebalance source)
rpc UnsubscribeGames(UnsubscribeGamesRequest) returns (UnsubscribeGamesResponse);
// Coordinator → Core: evict idle games from local cache
rpc EvictGames(EvictGamesRequest) returns (EvictGamesResponse);
// Coordinator → Core: drain instance before scale-down
rpc DrainInstance(DrainInstanceRequest) returns (DrainInstanceResponse);
}
message HeartbeatFrame {
string instanceId = 1;
string hostname = 2;
int32 httpPort = 3;
int32 grpcPort = 4;
int32 subscriptionCount = 5;
int32 localCacheSize = 6;
int64 timestampMillis = 7;
}
message CoordinatorCommand {
// Future: coordinator can push commands back (e.g., "start draining")
string type = 1;
string payload = 2;
}
message BatchResubscribeRequest {
repeated string gameIds = 1;
}
message BatchResubscribeResponse {
int32 subscribedCount = 1;
repeated string failedGameIds = 2;
}
message UnsubscribeGamesRequest {
repeated string gameIds = 1;
}
message UnsubscribeGamesResponse {
int32 unsubscribedCount = 1;
}
message EvictGamesRequest {
repeated string gameIds = 1;
}
message EvictGamesResponse {
int32 evictedCount = 1;
}
message DrainInstanceRequest {}
message DrainInstanceResponse {
int32 gamesMigrated = 0;
}
```
---
### 9. Coordinator REST API (internal)
- `GET /api/coordinator/instances` — all cores with load, health state
- `GET /api/coordinator/metrics` — load distribution, rebalance history
- `POST /api/coordinator/rebalance` — manual rebalance trigger
- `POST /api/coordinator/failover/{instanceId}` — manual failover
- `POST /api/coordinator/scale-up` / `scale-down` — manual scaling
---
### 10. Configuration
**`modules/coordinator/src/main/resources/application.yml`**:
```yaml
quarkus.application.name: nowchess-coordinator
quarkus.http.port: 8086
quarkus.grpc.server.port: 9086
nowchess.coordinator.max-games-per-core: 500
nowchess.coordinator.max-deviation-percent: 20
nowchess.coordinator.rebalance-interval: 30s
nowchess.coordinator.rebalance-min-interval: 60s
nowchess.coordinator.heartbeat-ttl: 5s
nowchess.coordinator.stream-heartbeat-interval: 200ms
nowchess.coordinator.cache-eviction-interval: 10m
nowchess.coordinator.game-idle-threshold: 45m
nowchess.coordinator.auto-scale-enabled: false
nowchess.coordinator.scale-up-threshold: 0.8
nowchess.coordinator.scale-down-threshold: 0.3
nowchess.coordinator.scale-min-replicas: 2
nowchess.coordinator.scale-max-replicas: 10
nowchess.coordinator.k8s-namespace: default
nowchess.coordinator.k8s-rollout-name: nowchess-core
nowchess.coordinator.k8s-rollout-label-selector: app=nowchess-core
quarkus.kubernetes-client.trust-certs: true
```
**Core `application.yml` additions**:
```yaml
nowchess.coordinator.host: localhost
nowchess.coordinator.grpc-port: 9086
nowchess.coordinator.stream-heartbeat-interval: 200ms
nowchess.coordinator.redis-heartbeat-interval: 2s
nowchess.coordinator.instance-id: ${HOSTNAME:local}-${quarkus.uuid}
```
---
### 11. Files to Create / Modify
**New — `modules/coordinator/`**:
```
build.gradle.kts
src/main/proto/coordinator_service.proto
src/main/resources/application.yml
src/main/scala/de/nowchess/coordinator/
resource/CoordinatorResource.scala # REST endpoints
service/InstanceRegistry.scala # Redis instance list + in-memory map
service/HealthMonitor.scala # gRPC stream watcher + Redis TTL + k8s watch
service/FailoverService.scala # dead core → BatchResubscribe
service/LoadBalancer.scala # rebalance logic
service/AutoScaler.scala # k8s StatefulSet scaling
service/CacheEvictionManager.scala # idle game eviction
grpc/CoordinatorGrpcServer.scala # CoordinatorService gRPC impl (for HeartbeatStream)
```
**Modify — `modules/core/`**:
- `build.gradle.kts` — add `coordinator_service.proto` stub, keep grpc dep
- `src/main/proto/coordinator_service.proto` — copy (or symlink) proto for stub generation
- `src/main/scala/de/nowchess/chess/redis/GameRedisSubscriberManager.scala``SADD`/`SREM` on subscribe/unsubscribe + implement `BatchResubscribeGames`, `UnsubscribeGames`, `EvictGames`, `DrainInstance` gRPC handlers
- `src/main/scala/de/nowchess/chess/` — new `InstanceHeartbeatService.scala` (startup, gRPC stream, Redis TTL refresh)
- `src/main/resources/application.yml` — coordinator connection config
**Modify — root**:
- `settings.gradle.kts` — add `include("modules/coordinator")`
---
## Verification
1. `./compile` — coordinator and core compile cleanly
2. **Stream detection**: start core + coordinator; kill core JVM (`kill -9`); coordinator logs failover within 300ms
3. **Game continuity**: active game on killed core; WebSocket client reconnects and receives game state
4. **Rebalance**: create 600 games on core-1 (2-core setup); coordinator rebalances ~100 to core-2
5. **Fallback**: disconnect gRPC stream manually but keep core alive; Redis TTL fallback triggers within 7s
6. **Cache eviction**: create idle game; coordinator calls `EvictGames` after 45min idle
7. **REST metrics**: `curl localhost:8086/api/coordinator/metrics` returns per-core load + health
8. **Restart recovery**: restart coordinator; gRPC streams re-establish from cores; state rebuilt from Redis
---
## Dependencies (new)
- `io.fabric8:kubernetes-client:6.13.0` (Fabric8 k8s client — handles Argo `Rollout` CRD via `GenericKubernetesResource`; no Argo Java SDK needed)
- Redisson — already in core, reuse via shared config
- Quarkus gRPC — already in core, reuse
+1 -5
View File
@@ -18,10 +18,6 @@ headers {
body:json {
{
"white": {"id": "p1", "displayName": "Alice"},
"black": {"id": "p2", "displayName": "Bob"},
"timeControl": {
"limitSeconds": 300,
"incrementSeconds": 3
}
"black": {"id": "p2", "displayName": "Bob"}
}
}
+2 -5
View File
@@ -4,12 +4,9 @@ meta {
seq: 2
}
get {
http {
method: GET
url: {{baseUrl}}/api/board/game/{{gameId}}
body: none
auth: none
}
vars:pre-request {
gameId: j0nPtcjl
}
+7 -22
View File
@@ -1,34 +1,19 @@
meta {
name: Stream Game
type: ws
type: http
seq: 3
}
ws {
url: {{wsBaseUrl}}/api/board/game/{{gameId}}/ws
body: ws
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/stream
body: none
auth: none
}
body:ws {
name: move
content: '''
{
"type": "MOVE",
"uci": "b1c3"
}
'''
}
body:ws {
name: ping
content: '''
{
"type": "PING"
}
'''
headers {
Accept: application/x-ndjson
}
vars:pre-request {
gameId: j0nPtcjl
gameId: tjOgyEcS
}
+1 -5
View File
@@ -19,10 +19,6 @@ body:json {
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"white": {"id": "p1", "displayName": "Alice"},
"black": {"id": "p2", "displayName": "Bob"},
"timeControl": {
"limitSeconds": 300,
"incrementSeconds": 3
}
"black": {"id": "p2", "displayName": "Bob"}
}
}
+1 -1
View File
@@ -11,5 +11,5 @@ post {
}
vars:pre-request {
gameId: Yg200tOF
gameId: tjOgyEcS
}
+1 -1
View File
@@ -15,5 +15,5 @@ params:query {
}
vars:pre-request {
gameId: COy3oigz
gameId: tjOgyEcS
}
-1
View File
@@ -1,5 +1,4 @@
vars {
baseUrl: http://localhost:8080
wsBaseUrl: ws://localhost:8084
ioBaseUrl: http://localhost:8081
}
+2 -18
View File
@@ -12,7 +12,7 @@ version = "1.0-SNAPSHOT"
// converted to scoverage regexes via globToScoverageRegex for instrumentation-time exclusion.
val coverageExclusions = listOf(
// UI renders JavaFX components; headless test environments cannot exercise rendering paths
"modules/api/**",
"modules/ui/**",
// FastParse macro-generated combinators produce synthetic branches that scoverage marks as uncovered
"modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse*",
// NNUE inference pipeline — coverage requires a trained model file not present in CI
@@ -36,23 +36,7 @@ val coverageExclusions = listOf(
"**/core/src/main/scala/de/nowchess/chess/registry/GameEntry.scala",
"**/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala",
// GameResource — REST integration layer with @Inject var fields; mocking dependencies for unit tests is infeasible with Quarkus DI; integration tests would require @QuarkusTest which Scoverage doesn't instrument
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala",
// IoResource — same rationale as GameResource; @QuarkusTest not instrumented by Scoverage
"**/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala",
// JacksonConfig — Quarkus lifecycle hook, no testable logic beyond ObjectMapper registration
"**/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala",
//RuleSetRestAdapter - Quarkus integration of rule into core, only testable with Quarkus tests
"**/core/src/main/de/nowchess/chess/adapter/RuleSetRestAdapter.scala",
// AccountResource / ChallengeResource — REST integration layer; @QuarkusTest not instrumented by Scoverage
"**/account/src/main/scala/de/nowchess/account/resource/**",
// JacksonConfig / NativeReflectionConfig — Quarkus lifecycle hooks, no testable logic
"**/account/src/main/scala/de/nowchess/account/config/**",
// WebSocket service — infrastructure CDI beans (RedisConfig)
"**/ws/src/main/scala/de/nowchess/ws/config/**",
// GameWebSocketResource in core — replaced by ws module
"**/core/src/main/scala/de/nowchess/chess/resource/GameWebSocketResource.scala",
// Coordinator infrastructure — gRPC, microservice orchestration
"**/coordinator/src/main/scala/**",
"**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala"
)
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
+771
View File
@@ -0,0 +1,771 @@
openapi: 3.0.3
info:
title: NowChess Board API
description: |
REST API for the NowChess application. Designed to feel familiar to users
of the [lichess API](https://lichess.org/api).
## Authentication
Most endpoints require a Bearer token:
```
Authorization: Bearer <token>
```
Authentication is reserved for future implementation — endpoints are currently
open unless noted otherwise.
## Move notation
Moves are expressed in **UCI notation**: `{from}{to}[promotion]`
- Normal move: `e2e4`
- Capture: `d5e6`
- Promotion: `e7e8q` (q=queen, r=rook, b=bishop, n=knight)
- Castling: `e1g1` (kingside white), `e1c1` (queenside white)
## Streaming
Endpoints that support streaming return **NDJSON** (newline-delimited JSON).
Request them with:
```
Accept: application/x-ndjson
```
Each line of the response is a complete JSON object. Empty lines are
keep-alive heartbeats.
## Rate limiting
Requests that exceed the rate limit receive `429 Too Many Requests`.
Honour the `Retry-After` response header and wait before retrying.
version: 1.0.0
contact:
name: NowChess
license:
name: MIT
servers:
- url: http://localhost:8080
description: Local development server
tags:
- name: game
description: Create and manage chess games
- name: move
description: Make moves and navigate game history
- name: draw
description: Draw offers and claims
- name: import
description: Load a game from FEN or PGN
- name: export
description: Export a game as FEN or PGN
paths:
# ---------------------------------------------------------------------------
# Game lifecycle
# ---------------------------------------------------------------------------
/api/board/game:
post:
operationId: createGame
tags: [game]
summary: Create a new game
description: |
Creates a new chess game starting from the initial position.
Returns the full game state including the generated `gameId`.
security:
- bearerAuth: []
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/CreateGameRequest'
responses:
'201':
description: Game created
content:
application/json:
schema:
$ref: '#/components/schemas/GameFull'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}:
get:
operationId: getGame
tags: [game]
summary: Get game state
description: Returns the full current state of a game.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: Current game state
content:
application/json:
schema:
$ref: '#/components/schemas/GameFull'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/stream:
get:
operationId: streamGame
tags: [game]
summary: Stream game events
description: |
Opens a persistent NDJSON stream for a game. The first object sent is
a `gameFull` event containing the complete game state. Subsequent
objects are `gameState` events sent whenever the game changes (move
made, draw offered, game over, etc.).
Empty lines are heartbeats to keep the connection alive.
Connect with:
```
Accept: application/x-ndjson
```
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: NDJSON event stream
content:
application/x-ndjson:
schema:
oneOf:
- $ref: '#/components/schemas/GameFullEvent'
- $ref: '#/components/schemas/GameStateEvent'
- $ref: '#/components/schemas/ErrorEvent'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/resign:
post:
operationId: resignGame
tags: [game]
summary: Resign the game
description: The active player resigns. The game ends immediately.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: Resignation accepted
content:
application/json:
schema:
$ref: '#/components/schemas/OkResponse'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
# ---------------------------------------------------------------------------
# Move-making
# ---------------------------------------------------------------------------
/api/board/game/{gameId}/move/{uci}:
post:
operationId: makeMove
tags: [move]
summary: Make a move
description: |
Submit a move in UCI notation. The move must be legal for the side
currently to move.
For promotion moves include the target piece as the fifth character:
`e7e8q`, `a2a1r`, etc. Promotion moves without the fifth character
are rejected with `400 INVALID_MOVE`.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
- name: uci
in: path
required: true
description: Move in UCI notation (e.g. `e2e4`, `e7e8q`)
schema:
type: string
pattern: '^[a-h][1-8][a-h][1-8][qrbn]?$'
example: e2e4
responses:
'200':
description: Move applied — returns updated game state
content:
application/json:
schema:
$ref: '#/components/schemas/GameState'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/moves:
get:
operationId: getLegalMoves
tags: [move]
summary: Get legal moves
description: |
Returns all legal moves for the side currently to move.
Optionally filter to moves originating from a single square.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
- name: square
in: query
required: false
description: Filter to moves from this square (e.g. `e2`)
schema:
type: string
pattern: '^[a-h][1-8]$'
example: e2
responses:
'200':
description: List of legal moves
content:
application/json:
schema:
$ref: '#/components/schemas/LegalMovesResponse'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/undo:
post:
operationId: undoMove
tags: [move]
summary: Undo the last move
description: Reverts the most recent move. Returns the updated game state.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: Move undone
content:
application/json:
schema:
$ref: '#/components/schemas/GameState'
'400':
description: No moves to undo
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/redo:
post:
operationId: redoMove
tags: [move]
summary: Redo a previously undone move
description: Re-applies the next move in the undo stack. Returns the updated game state.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: Move redone
content:
application/json:
schema:
$ref: '#/components/schemas/GameState'
'400':
description: No moves to redo
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
# ---------------------------------------------------------------------------
# Draw handling
# ---------------------------------------------------------------------------
/api/board/game/{gameId}/draw/{action}:
post:
operationId: drawAction
tags: [draw]
summary: Offer, accept, decline, or claim a draw
description: |
Perform a draw-related action:
| Action | Description |
|-----------|-------------|
| `offer` | Offer a draw to the opponent |
| `accept` | Accept the opponent's draw offer |
| `decline` | Decline the opponent's draw offer |
| `claim` | Claim a draw under the fifty-move rule (only valid when `status` is `fiftyMoveAvailable`) |
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
- name: action
in: path
required: true
schema:
type: string
enum: [offer, accept, decline, claim]
responses:
'200':
description: Action accepted
content:
application/json:
schema:
$ref: '#/components/schemas/OkResponse'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
# ---------------------------------------------------------------------------
# Import
# ---------------------------------------------------------------------------
/api/board/game/import/fen:
post:
operationId: importFen
tags: [import]
summary: Load a position from FEN
description: |
Creates a new game from a FEN string. The game starts at the position
described by the FEN; move history prior to that position is not
available.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ImportFenRequest'
responses:
'201':
description: Game created from FEN
content:
application/json:
schema:
$ref: '#/components/schemas/GameFull'
'400':
$ref: '#/components/responses/BadRequest'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/import/pgn:
post:
operationId: importPgn
tags: [import]
summary: Load a game from PGN
description: |
Creates a new game by replaying all moves in a PGN string. The game
starts at the position after the final move in the PGN; undo is
available for every replayed move.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ImportPgnRequest'
responses:
'201':
description: Game created from PGN
content:
application/json:
schema:
$ref: '#/components/schemas/GameFull'
'400':
$ref: '#/components/responses/BadRequest'
'429':
$ref: '#/components/responses/TooManyRequests'
# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------
/api/board/game/{gameId}/export/fen:
get:
operationId: exportFen
tags: [export]
summary: Export current position as FEN
description: Returns the FEN string representing the current board position.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: FEN string
content:
text/plain:
schema:
type: string
example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
/api/board/game/{gameId}/export/pgn:
get:
operationId: exportPgn
tags: [export]
summary: Export game as PGN
description: Returns the full PGN for the game including headers and move text.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/gameId'
responses:
'200':
description: PGN text
content:
application/x-chess-pgn:
schema:
type: string
example: |
[Event "NowChess game"]
[White "Player1"]
[Black "Player2"]
[Result "*"]
1. e4 e5 2. Nf3 *
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/TooManyRequests'
# =============================================================================
# Components
# =============================================================================
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'Personal access token — `Authorization: Bearer <token>`'
parameters:
gameId:
name: gameId
in: path
required: true
description: 8-character alphanumeric game ID (e.g. `Qa7FJNk2`)
schema:
type: string
pattern: '^[A-Za-z0-9]{8}$'
example: Qa7FJNk2
responses:
BadRequest:
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Unauthorized:
description: Missing or invalid authentication token
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
NotFound:
description: Game not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
TooManyRequests:
description: Rate limit exceeded — see `Retry-After` header
headers:
Retry-After:
description: Seconds to wait before retrying
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
schemas:
# -------------------------------------------------------------------------
# Requests
# -------------------------------------------------------------------------
CreateGameRequest:
type: object
description: Parameters for creating a new game. All fields are optional.
properties:
white:
$ref: '#/components/schemas/PlayerInfo'
black:
$ref: '#/components/schemas/PlayerInfo'
ImportFenRequest:
type: object
required: [fen]
properties:
fen:
type: string
description: Complete FEN string (6 fields)
example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
white:
$ref: '#/components/schemas/PlayerInfo'
black:
$ref: '#/components/schemas/PlayerInfo'
ImportPgnRequest:
type: object
required: [pgn]
properties:
pgn:
type: string
description: PGN text (headers and move list)
example: "1. e4 e5 2. Nf3 Nc6 *"
# -------------------------------------------------------------------------
# Game state
# -------------------------------------------------------------------------
GameFull:
type: object
description: Complete game information including players and current state.
required: [gameId, white, black, state]
properties:
gameId:
type: string
description: Unique 8-character game identifier
example: Qa7FJNk2
white:
$ref: '#/components/schemas/PlayerInfo'
black:
$ref: '#/components/schemas/PlayerInfo'
state:
$ref: '#/components/schemas/GameState'
GameState:
type: object
description: |
The current game state. Included in `GameFull` and returned by move
endpoints and stream events.
required: [fen, pgn, turn, status, moves, undoAvailable, redoAvailable]
properties:
fen:
type: string
description: FEN string for the current position
example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
pgn:
type: string
description: PGN move text for the full game so far
example: "1. e4"
turn:
type: string
enum: [white, black]
description: The side to move
status:
$ref: '#/components/schemas/GameStatus'
winner:
type: string
enum: [white, black]
description: Set when `status` is `checkmate` or `resign`
nullable: true
moves:
type: array
description: All moves played so far, in UCI notation
items:
type: string
example: [e2e4, e7e5, g1f3]
undoAvailable:
type: boolean
description: Whether `POST /undo` is currently valid
redoAvailable:
type: boolean
description: Whether `POST /redo` is currently valid
GameStatus:
type: string
description: |
Current game status:
| Value | Meaning |
|-------|---------|
| `started` | Game in progress, no special condition |
| `check` | Side to move is in check |
| `checkmate` | Side to move is checkmated — game over |
| `stalemate` | Side to move has no legal moves, not in check — game over (draw) |
| `resign` | A player resigned — game over |
| `draw` | Draw agreed or claimed — game over |
| `drawOffered` | Waiting for the opponent to accept or decline a draw offer |
| `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw |
| `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
enum:
- started
- check
- checkmate
- stalemate
- resign
- draw
- drawOffered
- fiftyMoveAvailable
- insufficientMaterial
# -------------------------------------------------------------------------
# Moves
# -------------------------------------------------------------------------
LegalMovesResponse:
type: object
required: [moves]
properties:
moves:
type: array
items:
$ref: '#/components/schemas/LegalMove'
LegalMove:
type: object
required: [from, to, uci, moveType]
properties:
from:
type: string
description: Origin square in algebraic notation
example: e2
to:
type: string
description: Destination square in algebraic notation
example: e4
uci:
type: string
description: Full move in UCI notation
example: e2e4
moveType:
$ref: '#/components/schemas/MoveType'
promotion:
type: string
enum: [queen, rook, bishop, knight]
description: Target piece for promotion moves
nullable: true
MoveType:
type: string
description: Classification of the move
enum:
- normal
- capture
- castleKingside
- castleQueenside
- enPassant
- promotion
# -------------------------------------------------------------------------
# Streaming events
# -------------------------------------------------------------------------
GameFullEvent:
type: object
description: |
First event on a game stream. Contains the complete game snapshot.
required: [type, game]
properties:
type:
type: string
enum: [gameFull]
game:
$ref: '#/components/schemas/GameFull'
GameStateEvent:
type: object
description: |
Emitted on a game stream whenever the game state changes (move played,
draw offered, game over, etc.).
required: [type, state]
properties:
type:
type: string
enum: [gameState]
state:
$ref: '#/components/schemas/GameState'
ErrorEvent:
type: object
description: Emitted on a game stream when an error occurs.
required: [type, error]
properties:
type:
type: string
enum: [error]
error:
$ref: '#/components/schemas/ApiError'
# -------------------------------------------------------------------------
# Shared types
# -------------------------------------------------------------------------
PlayerInfo:
type: object
required: [id, displayName]
properties:
id:
type: string
description: Unique player identifier
example: player1
displayName:
type: string
description: Human-readable display name
example: Alice
OkResponse:
type: object
required: [ok]
properties:
ok:
type: boolean
enum: [true]
ApiError:
type: object
required: [code, message]
properties:
code:
type: string
description: Machine-readable error code
example: INVALID_MOVE
message:
type: string
description: Human-readable error description
example: e2e5 is not a legal move
field:
type: string
description: Request field that caused the error, if applicable
example: uci
nullable: true
+1 -1
View File
@@ -1,6 +1,6 @@
#Sat Mar 21 14:37:06 CET 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
View File
Executable → Regular
View File
-5
View File
@@ -1,5 +0,0 @@
.gitignore
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*
-41
View File
@@ -1,41 +0,0 @@
# Gradle
.gradle/
build/
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/
# TLS Certificates
.certs/
-9
View File
@@ -1,9 +0,0 @@
## (2026-04-30)
### Features
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
-117
View File
@@ -1,117 +0,0 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
repositories {
mavenCentral()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
}
tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
runtimeOnly("io.quarkus:quarkus-jdbc-h2")
compileOnly("org.scala-lang:scala3-compiler_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation("org.scala-lang:scala3-library_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation(project(":modules:security"))
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-rest-client-jackson")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-hibernate-orm-panache")
implementation("io.quarkus:quarkus-jdbc-postgresql")
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-jwt-build")
implementation("io.quarkus:quarkus-elytron-security-common")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-micrometer")
implementation("io.quarkus:quarkus-smallrye-openapi")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
implementation("io.quarkus:quarkus-redis-client")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit")
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-jdbc-h2")
testImplementation("io.quarkus:quarkus-test-security")
testImplementation("io.quarkus:quarkus-junit5-mockito")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest", "junit-jupiter")
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
finalizedBy(tasks.reportScoverage)
}
tasks.reportScoverage {
dependsOn(tasks.test)
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
@@ -1,100 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/backcore-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backcore-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/backcore-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
# You can find more information about the UBI base runtime images and their configuration here:
# https://rh-openjdk.github.io/redhat-openjdk-containers/
###
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
ENV LANGUAGE='en_US:en'
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 build/quarkus-app/*.jar /deployments/
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -1,96 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.package.jar.type=legacy-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/backcore-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backcore-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
# when running the container
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/backcore-legacy-jar
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
# includes memory/GC tuning.
# You can configure the behavior using the following environment properties:
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
# in JAVA_OPTS (example: "-Dsome.property=foo")
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
# used to calculate a default maximal heap memory based on a containers restriction.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
# of the container available memory as set here. The default is `50` which means 50%
# of the available memory is used as an upper boundary. You can skip this mechanism by
# setting this value to `0` in which case no `-Xmx` option is added.
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
# is used to calculate a default initial heap memory based on the maximum heap memory.
# If used in a container without any memory constraints for the container then this
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
# is used as the initial heap size. You can skip this mechanism by setting this value
# to `0` in which case no `-Xms` option is added (example: "25")
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
# This is used to calculate the maximum value of the initial heap memory. If used in
# a container without any memory constraints for the container then this option has
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
# here. The default is 4096MB which means the calculated value of `-Xms` never will
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
# when things are happening. This option, if set to true, will set
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
# true").
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
# (example: "20")
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
# (example: "40")
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
# (example: "4")
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
# previous GC times. (example: "90")
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
# contain the necessary JRE command-line options to specify the required GC, which
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
# accessed directly. (example: "foo.example.com,bar.example.com")
#
# You can find more information about the UBI base runtime images and their configuration here:
# https://rh-openjdk.github.io/redhat-openjdk-containers/
###
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24
ENV LANGUAGE='en_US:en'
COPY build/lib/* /deployments/lib/
COPY build/*-runner.jar /deployments/quarkus-run.jar
EXPOSE 8080
USER 185
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
@@ -1,29 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
#
# Before building the container image run:
#
# ./gradlew :modules:account:build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backcore .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backcore
#
# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
###
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 modules/account/build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -1,32 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
# It uses a micro base image, tuned for Quarkus native executables.
# It reduces the size of the resulting container image.
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
#
# Before building the container image run:
#
# ./gradlew build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/backcore .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/backcore
#
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
###
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root --chmod=0755 build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -1,27 +0,0 @@
{
"reflection": [
{ "type": "scala.Tuple1[]" },
{ "type": "scala.Tuple2[]" },
{ "type": "scala.Tuple3[]" },
{ "type": "scala.Tuple4[]" },
{ "type": "scala.Tuple5[]" },
{ "type": "scala.Tuple6[]" },
{ "type": "scala.Tuple7[]" },
{ "type": "scala.Tuple8[]" },
{ "type": "scala.Tuple9[]" },
{ "type": "scala.Tuple10[]" },
{ "type": "scala.Tuple11[]" },
{ "type": "scala.Tuple12[]" },
{ "type": "scala.Tuple13[]" },
{ "type": "scala.Tuple14[]" },
{ "type": "scala.Tuple15[]" },
{ "type": "scala.Tuple16[]" },
{ "type": "scala.Tuple17[]" },
{ "type": "scala.Tuple18[]" },
{ "type": "scala.Tuple19[]" },
{ "type": "scala.Tuple20[]" },
{ "type": "scala.Tuple21[]" },
{ "type": "scala.Tuple22[]" },
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
]
}
@@ -1,89 +0,0 @@
quarkus:
http:
port: 8083
application:
name: nowchess-account
redis:
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
rest-client:
core-service:
url: http://localhost:8080
smallrye-openapi:
info-title: NowChess Account Service
path: /openapi
swagger-ui:
path: /swagger-ui
datasource:
db-kind: postgresql
username: ${DB_USER:nowchess}
password: ${DB_PASSWORD:nowchess}
jdbc:
url: ${DB_URL:jdbc:postgresql://localhost:5432/nowchess}
hibernate-orm:
schema-management:
strategy: update
nowchess:
redis:
host: localhost
port: 6379
prefix: nowchess
internal:
secret: 123abc
"%test":
quarkus:
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: jdbc:h2:mem:nowchess;DB_CLOSE_DELAY=-1
hibernate-orm:
schema-management:
strategy: drop-and-create
"%dev":
quarkus:
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: jdbc:h2:mem:nowchess;DB_CLOSE_DELAY=-1
hibernate-orm:
schema-management:
strategy: drop-and-create
"%deployed":
quarkus:
rest-client:
core-service:
url: ${CORE_SERVICE_URL}
datasource:
db-kind: postgresql
username: ${DB_USER}
password: ${DB_PASSWORD}
jdbc:
url: ${DB_URL}
hibernate-orm:
schema-management:
strategy: update
nowchess:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
prefix: ${REDIS_PREFIX:nowchess}
internal:
secret: ${INTERNAL_SECRET}
mp:
jwt:
verify:
publickey:
location: ${JWT_PUBLIC_KEY_PATH:keys/public.pem}
issuer: nowchess
smallrye:
jwt:
sign:
key:
location: ${JWT_PRIVATE_KEY_PATH:keys/private.pem}
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsnsCAl0vQx7Vu9CLDZ
g0SG05NgUzu9T+3DTEaHGq60T2uriO8BenwyvsF3BnDqTbKf4voohZ1DNfzdbT1J
Fj8B62FrDmxcO+sp1/b5HUCJP6y2uSRCmzOHe5k7Pk1IEi72FgBpKXSRkFibRlVf
634g7mgsPZAQ9PJEsv4Qvm05T9L6+Gmq6N3bMVLKRXs4RhDhaFbYH9GtUg1eI0yH
YjGyRfqzW/nqVMstOLHt8CuPouq4p7eMzeDH3YHkxPm4GG5foCXMOd2DZrW0SCcr
7dhFeNVWzQ2m53eOhBzNQX+v3pgjVStsePhBRt2LyGfwkNzmqDgqWsMzSHRMY+cn
WQIDAQAB
-----END PUBLIC KEY-----
@@ -1,27 +0,0 @@
package de.nowchess.account.client
import de.nowchess.security.InternalSecretClientFilter
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
case class CorePlayerInfo(id: String, displayName: String)
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
case class CoreCreateGameRequest(
white: Option[CorePlayerInfo],
black: Option[CorePlayerInfo],
timeControl: Option[CoreTimeControl],
mode: Option[String],
)
case class CoreGameResponse(gameId: String)
@Path("/api/board/game")
@RegisterRestClient(configKey = "core-service")
@RegisterProvider(classOf[InternalSecretClientFilter])
trait CoreGameClient:
@POST
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def createGame(req: CoreCreateGameRequest): CoreGameResponse
@@ -1,17 +0,0 @@
package de.nowchess.account.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(new DefaultScalaModule() {
override def version(): Version =
// scalafix:off DisableSyntax.null
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
// scalafix:on DisableSyntax.null
})
@@ -1,50 +0,0 @@
package de.nowchess.account.config
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
import de.nowchess.account.domain.{
BotAccount,
Challenge,
ChallengeColor,
ChallengeStatus,
DeclineReason,
OfficialBotAccount,
TimeControl,
UserAccount,
}
import de.nowchess.account.dto.*
import io.quarkus.runtime.annotations.RegisterForReflection
@RegisterForReflection(
targets = Array(
classOf[UserAccount],
classOf[BotAccount],
classOf[OfficialBotAccount],
classOf[Challenge],
classOf[ChallengeColor],
classOf[ChallengeStatus],
classOf[DeclineReason],
classOf[TimeControl],
classOf[LoginRequest],
classOf[TokenResponse],
classOf[PlayerInfo],
classOf[PublicAccountDto],
classOf[BotAccountDto],
classOf[BotAccountWithTokenDto],
classOf[OfficialBotAccountDto],
classOf[CreateBotAccountRequest],
classOf[UpdateBotNameRequest],
classOf[RotatedTokenDto],
classOf[TimeControlDto],
classOf[ChallengeRequest],
classOf[ChallengeDto],
classOf[DeclineRequest],
classOf[ChallengeListDto],
classOf[ErrorDto],
classOf[CorePlayerInfo],
classOf[CoreTimeControl],
classOf[CoreCreateGameRequest],
classOf[CoreGameResponse],
classOf[OfficialChallengeResponse],
),
)
class NativeReflectionConfig
@@ -1,12 +0,0 @@
package de.nowchess.account.config
import jakarta.enterprise.context.ApplicationScoped
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@ApplicationScoped
class RedisConfig:
// scalafix:off DisableSyntax.var
@ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess")
var prefix: String = uninitialized
// scalafix:on DisableSyntax.var
@@ -1,56 +0,0 @@
package de.nowchess.account.domain
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
import jakarta.persistence.*
import scala.compiletime.uninitialized
import java.time.Instant
import java.util.UUID
import scala.Conversion
@Entity
@Table(name = "challenges")
class Challenge extends PanacheEntityBase:
// scalafix:off DisableSyntax.var
@Id
@GeneratedValue(strategy = GenerationType.UUID)
var id: UUID = uninitialized
@ManyToOne
var challenger: UserAccount = uninitialized
@ManyToOne
var destUser: UserAccount = uninitialized
@Convert(converter = classOf[ChallengeColorConverter])
@Column(columnDefinition = "varchar(255)")
var color: ChallengeColor = uninitialized
@Convert(converter = classOf[ChallengeStatusConverter])
@Column(columnDefinition = "varchar(255)")
var status: ChallengeStatus = uninitialized
@Convert(converter = classOf[DeclineReasonConverter])
@Column(nullable = true, columnDefinition = "varchar(255)")
var declineReason: DeclineReason = uninitialized
var timeControlType: String = uninitialized
@Column(nullable = true)
var timeControlLimit: java.lang.Integer = uninitialized
@Column(nullable = true)
var timeControlIncrement: java.lang.Integer = uninitialized
var createdAt: Instant = uninitialized
var expiresAt: Instant = uninitialized
@Column(nullable = true)
var gameId: String = uninitialized
// scalafix:on
def gameIdOpt: Option[String] = Option(gameId)
def declineReasonOpt: Option[DeclineReason] = Option(declineReason)
def timeControlLimitOpt: Option[Int] = Option(timeControlLimit).map(_.intValue())
def timeControlIncrementOpt: Option[Int] = Option(timeControlIncrement).map(_.intValue())
@@ -1,4 +0,0 @@
package de.nowchess.account.domain
enum ChallengeColor:
case White, Black, Random
@@ -1,12 +0,0 @@
package de.nowchess.account.domain
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
@Converter(autoApply = true)
class ChallengeColorConverter extends AttributeConverter[ChallengeColor, String]:
override def convertToDatabaseColumn(attribute: ChallengeColor): String =
Option(attribute).map(_.toString).orNull
override def convertToEntityAttribute(dbData: String): ChallengeColor =
Option(dbData).map(ChallengeColor.valueOf).orNull
@@ -1,4 +0,0 @@
package de.nowchess.account.domain
enum ChallengeStatus:
case Created, Canceled, Declined, Accepted
@@ -1,12 +0,0 @@
package de.nowchess.account.domain
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
@Converter(autoApply = true)
class ChallengeStatusConverter extends AttributeConverter[ChallengeStatus, String]:
override def convertToDatabaseColumn(attribute: ChallengeStatus): String =
Option(attribute).map(_.toString).orNull
override def convertToEntityAttribute(dbData: String): ChallengeStatus =
Option(dbData).map(ChallengeStatus.valueOf).orNull
@@ -1,4 +0,0 @@
package de.nowchess.account.domain
enum DeclineReason:
case Generic, Later, TooFast, TooSlow, TimeControl, Rated, Casual, Standard, Variant, NoBot, OnlyBot
@@ -1,12 +0,0 @@
package de.nowchess.account.domain
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
@Converter(autoApply = true)
class DeclineReasonConverter extends AttributeConverter[DeclineReason, String]:
override def convertToDatabaseColumn(attribute: DeclineReason): String =
Option(attribute).map(_.toString).orNull
override def convertToEntityAttribute(dbData: String): DeclineReason =
Option(dbData).map(DeclineReason.valueOf).orNull
@@ -1,7 +0,0 @@
package de.nowchess.account.domain
sealed trait TimeControl
object TimeControl:
case class Clock(limit: Int, increment: Int) extends TimeControl
case object Unlimited extends TimeControl
@@ -1,78 +0,0 @@
package de.nowchess.account.domain
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
import jakarta.persistence.*
import scala.compiletime.uninitialized
import scala.jdk.CollectionConverters.*
import java.time.Instant
import java.util.UUID
@Entity
@Table(name = "user_accounts")
class UserAccount extends PanacheEntityBase:
// scalafix:off DisableSyntax.var
@Id
@GeneratedValue(strategy = GenerationType.UUID)
var id: UUID = uninitialized
@Column(unique = true, nullable = false)
var username: String = uninitialized
@Column(unique = true, nullable = false)
var email: String = uninitialized
var passwordHash: String = uninitialized
var rating: Int = 1500
var createdAt: Instant = uninitialized
var banned: Boolean = false
@OneToMany(mappedBy = "owner", cascade = Array(CascadeType.ALL), orphanRemoval = true)
var botAccounts: java.util.List[BotAccount] = uninitialized
// scalafix:on
def getBotAccounts: List[BotAccount] = Option(botAccounts).map(_.asScala.toList).getOrElse(Nil)
@Entity
@Table(name = "bot_accounts")
class BotAccount extends PanacheEntityBase:
// scalafix:off DisableSyntax.var
@Id
@GeneratedValue(strategy = GenerationType.UUID)
var id: UUID = uninitialized
@Column(nullable = false)
var name: String = uninitialized
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
var owner: UserAccount = uninitialized
@Column(unique = true, nullable = false, length = 256)
var token: String = uninitialized
var rating: Int = 1500
var createdAt: Instant = uninitialized
var banned: Boolean = false
// scalafix:on
@Entity
@Table(name = "official_bot_accounts")
class OfficialBotAccount extends PanacheEntityBase:
// scalafix:off DisableSyntax.var
@Id
@GeneratedValue(strategy = GenerationType.UUID)
var id: UUID = uninitialized
@Column(nullable = false)
var name: String = uninitialized
var rating: Int = 1500
var createdAt: Instant = uninitialized
// scalafix:on
@@ -1,49 +0,0 @@
package de.nowchess.account.dto
case class RegisterRequest(username: String, email: String, password: String)
case class LoginRequest(username: String, password: String)
case class TokenResponse(token: String)
case class PlayerInfo(id: String, name: String, rating: Int)
case class PublicAccountDto(id: String, username: String, rating: Int, createdAt: String)
case class TimeControlDto(`type`: String, limit: Option[Int], increment: Option[Int])
case class ChallengeRequest(color: String, timeControl: TimeControlDto)
case class ChallengeDto(
id: String,
challenger: PlayerInfo,
destUser: PlayerInfo,
variant: String,
color: String,
timeControl: TimeControlDto,
status: String,
declineReason: Option[String],
gameId: Option[String],
createdAt: String,
expiresAt: String,
)
case class DeclineRequest(reason: Option[String])
case class ChallengeListDto(in: List[ChallengeDto], out: List[ChallengeDto])
case class ErrorDto(error: String)
case class CreateBotAccountRequest(name: String)
case class UpdateBotNameRequest(name: String)
case class BotAccountDto(id: String, name: String, rating: Int, createdAt: String)
case class BotAccountWithTokenDto(id: String, name: String, rating: Int, token: String, createdAt: String)
case class RotatedTokenDto(token: String)
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
@@ -1,23 +0,0 @@
package de.nowchess.account.error
enum AccountError:
case UsernameTaken(username: String)
case EmailAlreadyRegistered(email: String)
case InvalidCredentials
case UserNotFound
case BotNotFound
case BotLimitExceeded
case NotAuthorized
case UserBanned
case BotBanned
def message: String = this match
case UsernameTaken(u) => s"Username '$u' is already taken"
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
case InvalidCredentials => "Invalid credentials"
case UserNotFound => "User not found"
case BotNotFound => "Bot account not found"
case BotLimitExceeded => "Maximum of 5 bot accounts per user exceeded"
case NotAuthorized => "Not authorized to perform this action"
case UserBanned => "User account is banned"
case BotBanned => "Bot account is banned"
@@ -1,25 +0,0 @@
package de.nowchess.account.error
enum ChallengeError:
case UserNotFound(username: String)
case ChallengerNotFound
case CannotChallengeSelf
case DuplicateChallenge
case InvalidColor(color: String)
case InvalidDeclineReason(reason: String)
case ChallengeNotFound
case ChallengeNotActive
case NotAuthorized
case GameCreationFailed
def message: String = this match
case UserNotFound(u) => s"User '$u' not found"
case ChallengerNotFound => "Challenger not found"
case CannotChallengeSelf => "Cannot challenge yourself"
case DuplicateChallenge => "Active challenge to this user already exists"
case InvalidColor(c) => s"Unknown color: $c"
case InvalidDeclineReason(r) => s"Unknown decline reason: $r"
case ChallengeNotFound => "Challenge not found"
case ChallengeNotActive => "Challenge is not active"
case NotAuthorized => "Not authorized"
case GameCreationFailed => "Failed to create game"
@@ -1,43 +0,0 @@
package de.nowchess.account.filter
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter}
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.ext.Provider
import org.eclipse.microprofile.jwt.JsonWebToken
import scala.compiletime.uninitialized
@Provider
@ApplicationScoped
class AlreadyLoggedInFilter extends ContainerRequestFilter:
@Inject
// scalafix:off DisableSyntax.var
var jwt: JsonWebToken = uninitialized
// scalafix:on
override def filter(context: ContainerRequestContext): Unit =
val path = context.getUriInfo.getPath
val method = context.getMethod
if isProtectedEndpoint(path, method) && isAuthenticated then
context.abortWith(
Response
.status(Response.Status.BAD_REQUEST)
.entity("""{"error":"Already logged in"}""")
.build(),
)
private def isAuthenticated: Boolean =
// scalafix:off DisableSyntax.null
try jwt.getName != null
catch
case _ => false
// scalafix:on DisableSyntax.null
private def isProtectedEndpoint(path: String, method: String): Boolean =
(path.contains("/api/account") || path.contains("/account")) &&
((path.endsWith("/api/account") && method == "POST") ||
(path.endsWith("/account") && method == "POST") ||
(path.contains("/login") && method == "POST"))
@@ -1,98 +0,0 @@
package de.nowchess.account.repository
import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.persistence.EntityManager
import java.util.UUID
import scala.jdk.CollectionConverters.*
@ApplicationScoped
class UserAccountRepository:
@Inject
// scalafix:off DisableSyntax.var
var em: EntityManager = scala.compiletime.uninitialized
// scalafix:on
def findByUsername(username: String): Option[UserAccount] =
em.createQuery("FROM UserAccount WHERE username = :username", classOf[UserAccount])
.setParameter("username", username)
.getResultList
.stream()
.findFirst()
.map(Option(_))
.orElse(None)
def findById(id: UUID): Option[UserAccount] =
Option(em.find(classOf[UserAccount], id))
def persist(account: UserAccount): UserAccount =
em.persist(account)
account
def findByEmail(email: String): Option[UserAccount] =
em.createQuery("FROM UserAccount WHERE email = :email", classOf[UserAccount])
.setParameter("email", email)
.getResultList
.asScala
.headOption
def findAll(): List[UserAccount] =
em.createQuery("FROM UserAccount", classOf[UserAccount]).getResultList.asScala.toList
@ApplicationScoped
class BotAccountRepository:
@Inject
// scalafix:off DisableSyntax.var
var em: EntityManager = scala.compiletime.uninitialized
// scalafix:on
def findById(id: UUID): Option[BotAccount] =
Option(em.find(classOf[BotAccount], id))
def findByOwner(ownerId: UUID): List[BotAccount] =
em.createQuery("FROM BotAccount WHERE owner.id = :ownerId", classOf[BotAccount])
.setParameter("ownerId", ownerId)
.getResultList
.asScala
.toList
def persist(bot: BotAccount): BotAccount =
em.persist(bot)
bot
def delete(botId: UUID): Unit =
em.find(classOf[BotAccount], botId) match
case bot: BotAccount => em.remove(bot)
def findByToken(token: String): Option[BotAccount] =
em.createQuery("FROM BotAccount WHERE token = :token", classOf[BotAccount])
.setParameter("token", token)
.getResultList
.asScala
.headOption
@ApplicationScoped
class OfficialBotAccountRepository:
@Inject
// scalafix:off DisableSyntax.var
var em: EntityManager = scala.compiletime.uninitialized
// scalafix:on
def findById(id: UUID): Option[OfficialBotAccount] =
Option(em.find(classOf[OfficialBotAccount], id))
def findAll(): List[OfficialBotAccount] =
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
def persist(bot: OfficialBotAccount): OfficialBotAccount =
em.persist(bot)
bot
def delete(botId: UUID): Unit =
em.find(classOf[OfficialBotAccount], botId) match
case bot: OfficialBotAccount => em.remove(bot)
@@ -1,62 +0,0 @@
package de.nowchess.account.repository
import de.nowchess.account.domain.{Challenge, ChallengeStatus}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.persistence.EntityManager
import java.time.Instant
import java.util.UUID
import scala.jdk.CollectionConverters.*
@ApplicationScoped
class ChallengeRepository:
@Inject
// scalafix:off DisableSyntax.var
var em: EntityManager = scala.compiletime.uninitialized
// scalafix:on
def findActiveByChallengerId(challengerId: UUID): List[Challenge] =
em.createQuery(
"FROM Challenge WHERE challenger.id = :cid AND status = :status AND expiresAt > :now",
classOf[Challenge],
).setParameter("cid", challengerId)
.setParameter("status", ChallengeStatus.Created)
.setParameter("now", Instant.now())
.getResultList
.asScala
.toList
def findActiveByDestUserId(destUserId: UUID): List[Challenge] =
em.createQuery(
"FROM Challenge WHERE destUser.id = :uid AND status = :status AND expiresAt > :now",
classOf[Challenge],
).setParameter("uid", destUserId)
.setParameter("status", ChallengeStatus.Created)
.setParameter("now", Instant.now())
.getResultList
.asScala
.toList
def findDuplicateChallenge(challengerId: UUID, destUserId: UUID): Option[Challenge] =
em.createQuery(
"FROM Challenge WHERE challenger.id = :cid AND destUser.id = :uid AND status = :status AND expiresAt > :now",
classOf[Challenge],
).setParameter("cid", challengerId)
.setParameter("uid", destUserId)
.setParameter("status", ChallengeStatus.Created)
.setParameter("now", Instant.now())
.getResultList
.asScala
.headOption
def findById(id: UUID): Option[Challenge] =
Option(em.find(classOf[Challenge], id))
def persist(challenge: Challenge): Challenge =
em.persist(challenge)
challenge
def merge(challenge: Challenge): Challenge =
em.merge(challenge)
@@ -1,202 +0,0 @@
package de.nowchess.account.resource
import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
import de.nowchess.account.dto.*
import de.nowchess.account.error.AccountError
import de.nowchess.account.service.AccountService
import jakarta.annotation.security.RolesAllowed
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.jwt.JsonWebToken
import scala.compiletime.uninitialized
import java.util.UUID
@Path("/api/account")
@ApplicationScoped
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class AccountResource:
// scalafix:off DisableSyntax.var
@Inject
var accountService: AccountService = uninitialized
@Inject
var jwt: JsonWebToken = uninitialized
// scalafix:on
@POST
def register(req: RegisterRequest): Response =
accountService.register(req) match
case Right(account) =>
Response.ok(toPublicDto(account)).build()
case Left(error) =>
Response.status(Response.Status.CONFLICT).entity(ErrorDto(error.message)).build()
@POST
@Path("/login")
def login(req: LoginRequest): Response =
accountService.login(req) match
case Right(token) =>
Response.ok(TokenResponse(token)).build()
case Left(AccountError.UserBanned) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build()
case Left(error) =>
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error.message)).build()
@GET
@Path("/me")
@RolesAllowed(Array("**"))
def me(): Response =
val id = UUID.fromString(jwt.getSubject)
accountService.findById(id) match
case Some(account) => Response.ok(toPublicDto(account)).build()
case None => Response.status(Response.Status.NOT_FOUND).build()
@GET
@Path("/{username}")
def publicProfile(@PathParam("username") username: String): Response =
accountService.findByUsername(username) match
case Some(account) => Response.ok(toPublicDto(account)).build()
case None => Response.status(Response.Status.NOT_FOUND).build()
@POST
@Path("/{userId}/ban")
@RolesAllowed(Array("Admin"))
def banUser(@PathParam("userId") userId: String): Response =
accountService.banUser(UUID.fromString(userId)) match
case Right(user) => Response.ok(toPublicDto(user)).build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
@POST
@Path("/{userId}/unban")
@RolesAllowed(Array("Admin"))
def unbanUser(@PathParam("userId") userId: String): Response =
accountService.unbanUser(UUID.fromString(userId)) match
case Right(user) => Response.ok(toPublicDto(user)).build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
@POST
@Path("/bots")
@RolesAllowed(Array("**"))
def createBotAccount(req: CreateBotAccountRequest): Response =
val ownerId = UUID.fromString(jwt.getSubject)
accountService.createBotAccount(ownerId, req.name) match
case Right(bot) =>
Response.status(Response.Status.CREATED).entity(toBotDtoWithToken(bot)).build()
case Left(error) =>
val status = error match
case AccountError.BotLimitExceeded => Response.Status.BAD_REQUEST
case _ => Response.Status.INTERNAL_SERVER_ERROR
Response.status(status).entity(ErrorDto(error.message)).build()
@GET
@Path("/bots")
@RolesAllowed(Array("**"))
def listBotAccounts(): Response =
val ownerId = UUID.fromString(jwt.getSubject)
val bots = accountService.getBotAccounts(ownerId)
Response.ok(bots.map(toBotDto)).build()
@PUT
@Path("/bots/{botId}")
@RolesAllowed(Array("**"))
def updateBotName(@PathParam("botId") botId: String, req: UpdateBotNameRequest): Response =
val ownerId = UUID.fromString(jwt.getSubject)
accountService.updateBotName(UUID.fromString(botId), ownerId, req.name) match
case Right(bot) => Response.ok(toBotDto(bot)).build()
case Left(AccountError.NotAuthorized) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
@POST
@Path("/bots/{botId}/rotate-token")
@RolesAllowed(Array("**"))
def rotateBotToken(@PathParam("botId") botId: String): Response =
val ownerId = UUID.fromString(jwt.getSubject)
accountService.rotateBotToken(UUID.fromString(botId), ownerId) match
case Right(bot) => Response.ok(RotatedTokenDto(bot.token)).build()
case Left(AccountError.NotAuthorized) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
@DELETE
@Path("/bots/{botId}")
@RolesAllowed(Array("**"))
def deleteBotAccount(@PathParam("botId") botId: String): Response =
val ownerId = UUID.fromString(jwt.getSubject)
val botUuid = UUID.fromString(botId)
accountService.getBotAccountWithOwnerCheck(botUuid, ownerId) match
case None => Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(AccountError.BotNotFound.message)).build()
case Some(None) =>
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
case Some(Some(_)) =>
accountService.deleteBotAccount(botUuid) match
case Right(_) => Response.noContent().build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
private def toPublicDto(account: UserAccount): PublicAccountDto =
PublicAccountDto(
id = account.id.toString,
username = account.username,
rating = account.rating,
createdAt = account.createdAt.toString,
)
private def toBotDto(bot: BotAccount): BotAccountDto =
BotAccountDto(
id = bot.id.toString,
name = bot.name,
rating = bot.rating,
createdAt = bot.createdAt.toString,
)
private def toBotDtoWithToken(bot: BotAccount): BotAccountWithTokenDto =
BotAccountWithTokenDto(
id = bot.id.toString,
name = bot.name,
rating = bot.rating,
token = bot.token,
createdAt = bot.createdAt.toString,
)
@GET
@Path("/official-bots")
def getOfficialBots: Response =
val bots = accountService.getOfficialBotAccounts()
Response.ok(bots.map(toOfficialBotDto)).build()
@POST
@Path("/official-bots")
@RolesAllowed(Array("Admin"))
def createOfficialBot(req: CreateBotAccountRequest): Response =
accountService.createOfficialBotAccount(req.name) match
case Right(bot) =>
Response.status(Response.Status.CREATED).entity(toOfficialBotDto(bot)).build()
case Left(error) =>
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ErrorDto(error.message)).build()
@DELETE
@Path("/official-bots/{botId}")
@RolesAllowed(Array("Admin"))
def deleteOfficialBot(@PathParam("botId") botId: String): Response =
accountService.deleteOfficialBotAccount(UUID.fromString(botId)) match
case Right(_) => Response.noContent().build()
case Left(error) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
private def toOfficialBotDto(bot: OfficialBotAccount): OfficialBotAccountDto =
OfficialBotAccountDto(
id = bot.id.toString,
name = bot.name,
rating = bot.rating,
createdAt = bot.createdAt.toString,
)
@@ -1,88 +0,0 @@
package de.nowchess.account.resource
import de.nowchess.account.dto.*
import de.nowchess.account.error.ChallengeError
import de.nowchess.account.service.ChallengeService
import jakarta.annotation.security.RolesAllowed
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.jwt.JsonWebToken
import scala.compiletime.uninitialized
import java.util.UUID
@Path("/api/challenge")
@ApplicationScoped
@RolesAllowed(Array("**"))
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class ChallengeResource:
// scalafix:off DisableSyntax.var
@Inject
var challengeService: ChallengeService = uninitialized
@Inject
var jwt: JsonWebToken = uninitialized
// scalafix:on
@POST
@Path("/{username}")
def create(@PathParam("username") username: String, req: ChallengeRequest): Response =
val userId = UUID.fromString(jwt.getSubject)
challengeService.create(userId, username, req) match
case Right(challenge) =>
Response.status(Response.Status.CREATED).entity(challengeService.toDto(challenge)).build()
case Left(error) =>
val status = error match
case ChallengeError.UserNotFound(_) | ChallengeError.ChallengerNotFound => Response.Status.NOT_FOUND
case ChallengeError.CannotChallengeSelf => Response.Status.BAD_REQUEST
case _ => Response.Status.CONFLICT
Response.status(status).entity(ErrorDto(error.message)).build()
@GET
def list(): Response =
val userId = UUID.fromString(jwt.getSubject)
Response.ok(challengeService.listForUser(userId)).build()
@GET
@Path("/{id}")
def get(@PathParam("id") id: UUID): Response =
val userId = UUID.fromString(jwt.getSubject)
challengeService.findById(id, userId) match
case Right(challenge) => Response.ok(challengeService.toDto(challenge)).build()
case Left(error) => errorResponse(error)
@POST
@Path("/{id}/accept")
def accept(@PathParam("id") id: UUID): Response =
val userId = UUID.fromString(jwt.getSubject)
challengeService.accept(id, userId) match
case Right(challenge) => Response.ok(challengeService.toDto(challenge)).build()
case Left(error) => errorResponse(error)
@POST
@Path("/{id}/decline")
def decline(@PathParam("id") id: UUID, req: DeclineRequest): Response =
val userId = UUID.fromString(jwt.getSubject)
challengeService.decline(id, userId, req) match
case Right(challenge) => Response.ok(challengeService.toDto(challenge)).build()
case Left(error) => errorResponse(error)
@POST
@Path("/{id}/cancel")
def cancel(@PathParam("id") id: UUID): Response =
val userId = UUID.fromString(jwt.getSubject)
challengeService.cancel(id, userId) match
case Right(challenge) => Response.ok(challengeService.toDto(challenge)).build()
case Left(error) => errorResponse(error)
private def errorResponse(error: ChallengeError): Response =
val status = error match
case ChallengeError.ChallengeNotFound => Response.Status.NOT_FOUND
case ChallengeError.NotAuthorized => Response.Status.FORBIDDEN
case ChallengeError.GameCreationFailed => Response.Status.INTERNAL_SERVER_ERROR
case _ => Response.Status.BAD_REQUEST
Response.status(status).entity(ErrorDto(error.message)).build()
@@ -1,91 +0,0 @@
package de.nowchess.account.resource
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo}
import de.nowchess.account.dto.{ErrorDto, OfficialChallengeResponse}
import de.nowchess.account.service.{AccountService, EventPublisher}
import jakarta.annotation.security.RolesAllowed
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import org.eclipse.microprofile.jwt.JsonWebToken
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import java.util.UUID
import java.util.concurrent.ThreadLocalRandom
@Path("/api/challenge/official")
@ApplicationScoped
@RolesAllowed(Array("**"))
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class OfficialChallengeResource:
// scalafix:off DisableSyntax.var
@Inject var accountService: AccountService = uninitialized
@Inject var jwt: JsonWebToken = uninitialized
@Inject var botEventPublisher: EventPublisher = uninitialized
@Inject
@RestClient
var coreGameClient: CoreGameClient = uninitialized
// scalafix:on
private val log = Logger.getLogger(classOf[OfficialChallengeResource])
@POST
@Path("/{botName}")
def challengeWithDifficulty(
@PathParam("botName") botName: String,
@QueryParam("difficulty") difficulty: Int,
@QueryParam("color") color: String,
): Response =
if difficulty < 1000 || difficulty > 2800 then
Response
.status(Response.Status.BAD_REQUEST)
.entity(ErrorDto("difficulty must be between 1000 and 2800"))
.build()
else
val normalizedColor = Option(color).map(_.toLowerCase).getOrElse("random")
normalizedColor match
case "white" | "black" | "random" =>
val userId = UUID.fromString(jwt.getSubject)
val botOpt = accountService.getOfficialBotAccounts().find(_.name == botName)
val userOpt = accountService.findById(userId)
(botOpt, userOpt) match
case (None, _) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Official bot '$botName' not found")).build()
case (_, None) =>
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto("User not found")).build()
case (Some(bot), Some(user)) =>
val userIsWhite = normalizedColor match
case "white" => true
case "black" => false
case _ => ThreadLocalRandom.current().nextBoolean()
val (white, black, botColor) =
if userIsWhite then
(CorePlayerInfo(user.id.toString, user.username), CorePlayerInfo(bot.id.toString, bot.name), "black")
else
(CorePlayerInfo(bot.id.toString, bot.name), CorePlayerInfo(user.id.toString, user.username), "white")
val req = CoreCreateGameRequest(Some(white), Some(black), None, Some("Authenticated"))
val gameId =
try Right(coreGameClient.createGame(req).gameId)
catch case _ => Left("Failed to create game")
gameId match
case Left(err) =>
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ErrorDto(err)).build()
case Right(id) =>
try botEventPublisher.publishGameStart(bot.name, id, botColor, difficulty, bot.id.toString)
catch case ex: Exception => log.warnf(ex, "Failed to notify bot for game %s", id)
Response
.status(Response.Status.CREATED)
.entity(OfficialChallengeResponse(id, botName, difficulty))
.build()
case other =>
Response
.status(Response.Status.BAD_REQUEST)
.entity(ErrorDto(s"Invalid color: $other. Must be white, black or random"))
.build()
@@ -1,165 +0,0 @@
package de.nowchess.account.service
import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
import de.nowchess.account.dto.{LoginRequest, RegisterRequest}
import de.nowchess.account.error.AccountError
import de.nowchess.account.repository.{BotAccountRepository, OfficialBotAccountRepository, UserAccountRepository}
import io.quarkus.elytron.security.common.BcryptUtil
import io.smallrye.jwt.build.Jwt
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.transaction.Transactional
import scala.compiletime.uninitialized
import java.time.Instant
import java.util.UUID
@ApplicationScoped
class AccountService:
// scalafix:off DisableSyntax.var
@Inject
var userAccountRepository: UserAccountRepository = uninitialized
@Inject
var botAccountRepository: BotAccountRepository = uninitialized
@Inject
var officialBotAccountRepository: OfficialBotAccountRepository = uninitialized
// scalafix:on
@Transactional
def register(req: RegisterRequest): Either[AccountError, UserAccount] =
if userAccountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
else if userAccountRepository.findByEmail(req.email).isDefined then
Left(AccountError.EmailAlreadyRegistered(req.email))
else
val account = new UserAccount()
account.username = req.username
account.email = req.email
account.passwordHash = BcryptUtil.bcryptHash(req.password)
account.createdAt = Instant.now()
userAccountRepository.persist(account)
Right(account)
def login(req: LoginRequest): Either[AccountError, String] =
userAccountRepository.findByUsername(req.username) match
case None => Left(AccountError.InvalidCredentials)
case Some(account) =>
if !BcryptUtil.matches(req.password, account.passwordHash) then Left(AccountError.InvalidCredentials)
else if account.banned then Left(AccountError.UserBanned)
else
Right(
Jwt
.issuer("nowchess")
.subject(account.id.toString)
.claim("username", account.username)
.sign(),
)
def findByUsername(username: String): Option[UserAccount] =
userAccountRepository.findByUsername(username)
def findById(id: UUID): Option[UserAccount] =
userAccountRepository.findById(id)
@Transactional
def createBotAccount(ownerId: UUID, botName: String): Either[AccountError, BotAccount] =
userAccountRepository.findById(ownerId) match
case None => Left(AccountError.UserNotFound)
case Some(owner) =>
val botAccounts = botAccountRepository.findByOwner(ownerId)
if botAccounts.length >= 5 then Left(AccountError.BotLimitExceeded)
else
val bot = new BotAccount()
bot.name = botName
bot.owner = owner
bot.token = generateBotToken(bot.id)
bot.createdAt = Instant.now()
botAccountRepository.persist(bot)
Right(bot)
def getBotAccounts(ownerId: UUID): List[BotAccount] =
botAccountRepository.findByOwner(ownerId)
def getBotAccountWithOwnerCheck(botId: UUID, ownerId: UUID): Option[Option[BotAccount]] =
botAccountRepository.findById(botId) match
case None => Some(None)
case Some(bot) => Some(Option(bot).filter(_.owner.id == ownerId))
@Transactional
def deleteBotAccount(botId: UUID): Either[AccountError, Unit] =
botAccountRepository.findById(botId) match
case None => Left(AccountError.BotNotFound)
case Some(_) =>
botAccountRepository.delete(botId)
Right(())
@Transactional
def updateBotName(botId: UUID, ownerId: UUID, newName: String): Either[AccountError, BotAccount] =
botAccountRepository.findById(botId) match
case None => Left(AccountError.BotNotFound)
case Some(bot) =>
if bot.owner.id != ownerId then Left(AccountError.NotAuthorized)
else
bot.name = newName
botAccountRepository.persist(bot)
Right(bot)
@Transactional
def rotateBotToken(botId: UUID, ownerId: UUID): Either[AccountError, BotAccount] =
botAccountRepository.findById(botId) match
case None => Left(AccountError.BotNotFound)
case Some(bot) =>
if bot.owner.id != ownerId then Left(AccountError.NotAuthorized)
else
bot.token = generateBotToken(botId)
botAccountRepository.persist(bot)
Right(bot)
@Transactional
def createOfficialBotAccount(botName: String): Either[AccountError, OfficialBotAccount] =
val bot = new OfficialBotAccount()
bot.name = botName
bot.createdAt = Instant.now()
officialBotAccountRepository.persist(bot)
Right(bot)
def getOfficialBotAccounts(): List[OfficialBotAccount] =
officialBotAccountRepository.findAll()
@Transactional
def deleteOfficialBotAccount(botId: UUID): Either[AccountError, Unit] =
officialBotAccountRepository.findById(botId) match
case None => Left(AccountError.BotNotFound)
case Some(_) =>
officialBotAccountRepository.delete(botId)
Right(())
private def generateBotToken(botId: UUID): String =
Jwt
.issuer("nowchess")
.subject(botId.toString)
.expiresAt(Long.MaxValue)
.claim("type", "bot")
.sign()
@Transactional
def banUser(userId: UUID): Either[AccountError, UserAccount] =
userAccountRepository.findById(userId) match
case None => Left(AccountError.UserNotFound)
case Some(user) =>
user.banned = true
user.botAccounts.forEach(_.banned = true)
userAccountRepository.persist(user)
Right(user)
@Transactional
def unbanUser(userId: UUID): Either[AccountError, UserAccount] =
userAccountRepository.findById(userId) match
case None => Left(AccountError.UserNotFound)
case Some(user) =>
user.banned = false
user.botAccounts.forEach(_.banned = false)
userAccountRepository.persist(user)
Right(user)
@@ -1,197 +0,0 @@
package de.nowchess.account.service
import de.nowchess.account.client.{
CoreCreateGameRequest,
CoreGameClient,
CoreGameResponse,
CorePlayerInfo,
CoreTimeControl,
}
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
import de.nowchess.account.dto.{
ChallengeDto,
ChallengeListDto,
ChallengeRequest,
DeclineRequest,
PlayerInfo,
TimeControlDto,
}
import de.nowchess.account.error.ChallengeError
import de.nowchess.account.repository.{ChallengeRepository, UserAccountRepository}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.transaction.Transactional
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.jboss.logging.Logger
import scala.compiletime.uninitialized
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID
import java.util.concurrent.ThreadLocalRandom
@ApplicationScoped
class ChallengeService:
private val log = Logger.getLogger(classOf[ChallengeService])
// scalafix:off DisableSyntax.var
@Inject
var userAccountRepository: UserAccountRepository = uninitialized
@Inject
var challengeRepository: ChallengeRepository = uninitialized
@Inject
@RestClient
var coreGameClient: CoreGameClient = uninitialized
@Inject
var eventPublisher: EventPublisher = uninitialized
// scalafix:on
@Transactional
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
for
destUser <- userAccountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
challenger <- userAccountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
_ <- Either.cond(
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
(),
ChallengeError.DuplicateChallenge,
)
color <- parseColor(req.color)
yield
val challenge = new Challenge()
challenge.challenger = challenger
challenge.destUser = destUser
challenge.color = color
challenge.status = ChallengeStatus.Created
challenge.timeControlType = req.timeControl.`type`
challenge.timeControlLimit = req.timeControl.limit.map(java.lang.Integer.valueOf).orNull
challenge.timeControlIncrement = req.timeControl.increment.map(java.lang.Integer.valueOf).orNull
challenge.createdAt = Instant.now()
challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS)
challengeRepository.persist(challenge)
try eventPublisher.publishChallengeCreated(destUser.id.toString, challenge.id.toString, challenger.username)
catch case ex: Exception => log.warnf(ex, "Failed to notify dest user for challenge %s", challenge.id)
challenge
@Transactional
def accept(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
_ <- Either.cond(challenge.destUser.id == userId, (), ChallengeError.NotAuthorized)
gameId <- createGame(challenge)
yield
challenge.status = ChallengeStatus.Accepted
challenge.gameId = gameId
challengeRepository.merge(challenge)
notifyBotIfNeeded(challenge, gameId)
try eventPublisher.publishChallengeAccepted(challenge.challenger.id.toString, challenge.id.toString, gameId)
catch case ex: Exception => log.warnf(ex, "Failed to notify challenger for game %s", gameId)
challenge
@Transactional
def decline(challengeId: UUID, userId: UUID, req: DeclineRequest): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
_ <- Either.cond(challenge.destUser.id == userId, (), ChallengeError.NotAuthorized)
reason <- parseDeclineReason(req.reason)
yield
challenge.status = ChallengeStatus.Declined
challenge.declineReason = reason.orNull
challengeRepository.merge(challenge)
challenge
@Transactional
def cancel(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
_ <- Either.cond(challenge.challenger.id == userId, (), ChallengeError.NotAuthorized)
yield
challenge.status = ChallengeStatus.Canceled
challengeRepository.merge(challenge)
challenge
def findById(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
for
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
_ <- Either.cond(
challenge.challenger.id == userId || challenge.destUser.id == userId,
(),
ChallengeError.NotAuthorized,
)
yield challenge
def listForUser(userId: UUID): ChallengeListDto =
val incoming = challengeRepository.findActiveByDestUserId(userId).map(toDto)
val outgoing = challengeRepository.findActiveByChallengerId(userId).map(toDto)
ChallengeListDto(in = incoming, out = outgoing)
private def notifyBotIfNeeded(challenge: Challenge, gameId: String): Unit =
val (white, black) = assignColors(challenge)
List(challenge.challenger, challenge.destUser).foreach { user =>
user.getBotAccounts.headOption.foreach { bot =>
val playingAs = if white.id == user.id.toString then "white" else "black"
try eventPublisher.publishGameStart(bot.id.toString, gameId, playingAs, 1400, bot.id.toString)
catch case ex: Exception => log.warnf(ex, "Failed to notify bot for game %s", gameId)
}
}
private def createGame(challenge: Challenge): Either[ChallengeError, String] =
try
val (white, black) = assignColors(challenge)
val tc = buildTimeControl(challenge)
val req = CoreCreateGameRequest(Some(white), Some(black), tc, Some("Authenticated"))
Right(coreGameClient.createGame(req).gameId)
catch case _ => Left(ChallengeError.GameCreationFailed)
private def assignColors(challenge: Challenge): (CorePlayerInfo, CorePlayerInfo) =
val challenger = CorePlayerInfo(challenge.challenger.id.toString, challenge.challenger.username)
val destUser = CorePlayerInfo(challenge.destUser.id.toString, challenge.destUser.username)
challenge.color match
case ChallengeColor.White => (challenger, destUser)
case ChallengeColor.Black => (destUser, challenger)
case ChallengeColor.Random =>
if ThreadLocalRandom.current().nextBoolean() then (challenger, destUser) else (destUser, challenger)
private def buildTimeControl(challenge: Challenge): Option[CoreTimeControl] =
challenge.timeControlType match
case "unlimited" => None
case "correspondence" => Some(CoreTimeControl(None, None, challenge.timeControlLimitOpt))
case _ => Some(CoreTimeControl(challenge.timeControlLimitOpt, challenge.timeControlIncrementOpt, None))
private def parseColor(raw: String): Either[ChallengeError, ChallengeColor] =
raw.toLowerCase match
case "white" => Right(ChallengeColor.White)
case "black" => Right(ChallengeColor.Black)
case "random" => Right(ChallengeColor.Random)
case _ => Left(ChallengeError.InvalidColor(raw))
private def parseDeclineReason(raw: Option[String]): Either[ChallengeError, Option[DeclineReason]] =
raw match
case None => Right(None)
case Some(r) =>
DeclineReason.values.find(_.toString.equalsIgnoreCase(r)) match
case Some(reason) => Right(Some(reason))
case None => Left(ChallengeError.InvalidDeclineReason(r))
def toDto(c: Challenge): ChallengeDto =
ChallengeDto(
id = c.id.toString,
challenger = PlayerInfo(c.challenger.id.toString, c.challenger.username, c.challenger.rating),
destUser = PlayerInfo(c.destUser.id.toString, c.destUser.username, c.destUser.rating),
variant = "standard",
color = c.color.toString.toLowerCase,
timeControl = TimeControlDto(c.timeControlType, c.timeControlLimitOpt, c.timeControlIncrementOpt),
status = c.status.toString.toLowerCase,
declineReason = c.declineReasonOpt.map(_.toString.toLowerCase),
gameId = c.gameIdOpt,
createdAt = c.createdAt.toString,
expiresAt = c.expiresAt.toString,
)
@@ -1,31 +0,0 @@
package de.nowchess.account.service
import de.nowchess.account.config.RedisConfig
import io.quarkus.redis.datasource.RedisDataSource
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import scala.compiletime.uninitialized
@ApplicationScoped
class EventPublisher:
// scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on DisableSyntax.var
def publishGameStart(botId: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
val event =
s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":$difficulty,"botAccountId":"$botAccountId"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event)
()
def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit =
val event = s"""{"type":"challengeCreated","challengeId":"$challengeId","challengerName":"$challengerName"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$destUserId:events", event)
()
def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit =
val event = s"""{"type":"challengeAccepted","challengeId":"$challengeId","gameId":"$gameId"}"""
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$challengerId:events", event)
()
@@ -1,36 +0,0 @@
quarkus:
http:
port: 8083
application:
name: nowchess-account
smallrye-openapi:
info-title: NowChess Account Service
path: /openapi
swagger-ui:
always-include: true
path: /swagger-ui
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
hibernate-orm:
schema-management:
strategy: drop-and-create
mp:
jwt:
verify:
publickey:
location: keys/test-public.pem
issuer: nowchess
smallrye:
jwt:
sign:
key:
location: keys/test-private.pem
nowchess:
internal:
secret: test-secret
auth:
enabled: false
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4zBHgRLMez2b6
wfdvvTJVR8xxbr/kJUMiq4ot14KhtTaGikFW+77ezjoqabFWH7CNjDvASWCM2n7X
PxL4fhUwzvTbhRZ2XNM80lKB+OIjP3hoNLvgeSNHbS4CztOfk2JVtQFLQdYJ/gvB
oFPgBtZYO/SZVML28d5U92JrWRIC1e1Ht1oKwKJoOqtTJrs/RuOlKQ/du4kwY8m0
jPw05wFA1YRMUC78xKklCVYCufYewIUTdKxATK0ZKWBoPCJnxDg8gwgpnV1wHQrH
GcbZvhcVg3GWpDcYdnogV4rlssws57+uAhGRyQBkmmhVb+zT+LT7WXDPB46MnHkK
FIZaxEkHAgMBAAECggEAAvu4Zih1w8+RWAb9mZ4yS9Im6MXi7yny1YJzbp4GC9pD
ERT2TRMvV6V4puqh5EQKs55J8Ka+mkeEuLDZ+4z9hpYwucKCRFLnThoPHu4HqI4D
wZroVY1fFm4aygzQucjFU6DibnaXn/2r7upJsFor56zAHCGULCxnbHO58QW1Frqa
UrTndSkrxavBD9LL1ohPEy3saXlRCVAEM5l7jZbg52dPauIYAOv0e+EE3RETw/Xz
3EWukIZ7PKyoyuQm8Sv2u7lyISljDGlvrW5IjVRPMPqOKNOa/pV3qU4mbUY6GjbC
B4xt8kEKjVSkTeMXA+W0gnZddnQOtcQYSrYWWes+AQKBgQDzjmt1ZJktZG96M8+f
Ov9JznfzSLYxN7EboDhqjTVBOkb6flRSYrd9E6gReIIrq5Sjs9Z+toA/u8BmjQ/P
GTrrLVh6bLBicUGKcmQFKw/0D9lOlbxaMg8VO9rqSb/AslumJwjucU7DA+WAN52j
cyiLiw+EmWjL/DV51fHHI18SgQKBgQDCPRzpeP8Qox83/+tGR/6fSSRi5ec3ZVPy
aCCCZM6qqhLv3hJkV0djRruVVfe136PwUi20BW6aF0PXmxDIGRWqDLQGkvDNEhjw
ZLBv/dYtW2HBZhq4E0w8DiaNZCOWvpLQ3QCEtzmuhyHhNqYHzvmuerk+w4c/8fY6
DFyPyiAHhwKBgDrpO/zNNG/SV1SLq7CsKIvFsSXbdJY7Dk/MVVkQhs0cN4bnf6Xd
0twiIQj4ySOfAPkHyt4jbqn70/H6NNS3GZVBBqG2IIPvORcvzBmj7Nvv6XQkq8Z1
TUipja4V4JfPjHOIBZUHOzHYg26cBTk/5ZK7NCmyobKVcqnhofW1DI4BAoGAaRu4
8X5QSCh9VEhggH+lAX0K+5l9LTTf4GUIcocqbp/p73M0cKfqMYatK3qBuSF0DS/r
G2d1Gl1MkPeQdTddyc9l+8i4FcCdTjiuYWvy4kh49bbS7plCv5zIr+pod8JYoD13
clnUFOV7J+vynHccFZbDd3tHTQsaOv9Fd2nhOzECgYEA8SWBEmTuaBh+0vr6zS+E
wD+cwB3iaGo+7fP7TZ+v1kxoDlcDjPYM4ikiOB+OPGNkAfqc3MGsbhfgcxqD0+5r
kpCFyiyieyoT+7hkMpMsJCNwFO+29fc3DDqPX4Keqp26tMxtRzYea3GtVShiRXew
5i4ReFwm3/IWDn9kLmHT6Fg=
-----END PRIVATE KEY-----
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMwR4ESzHs9m+sH3b70y
VUfMcW6/5CVDIquKLdeCobU2hopBVvu+3s46KmmxVh+wjYw7wElgjNp+1z8S+H4V
MM7024UWdlzTPNJSgfjiIz94aDS74HkjR20uAs7Tn5NiVbUBS0HWCf4LwaBT4AbW
WDv0mVTC9vHeVPdia1kSAtXtR7daCsCiaDqrUya7P0bjpSkP3buJMGPJtIz8NOcB
QNWETFAu/MSpJQlWArn2HsCFE3SsQEytGSlgaDwiZ8Q4PIMIKZ1dcB0KxxnG2b4X
FYNxlqQ3GHZ6IFeK5bLMLOe/rgIRkckAZJpoVW/s0/i0+1lwzweOjJx5ChSGWsRJ
BwIDAQAB
-----END PUBLIC KEY-----
@@ -1,107 +0,0 @@
package de.nowchess.account.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.Test
@QuarkusTest
class AccountResourceTest:
private def givenRequest() = RestAssured.`given`().contentType(ContentType.JSON)
private def registerBody(username: String, email: String = "", password: String = "secret") =
val resolvedEmail = if email.isEmpty then s"$username@example.com" else email
s"""{"username":"$username","email":"$resolvedEmail","password":"$password"}"""
private def loginBody(username: String, password: String = "secret") =
s"""{"username":"$username","password":"$password"}"""
private def registerAndLogin(username: String): String =
givenRequest()
.body(registerBody(username))
.when()
.post("/api/account")
.`then`()
.statusCode(200)
givenRequest()
.body(loginBody(username))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.extract()
.path[String]("token")
@Test
def registerReturns200(): Unit =
givenRequest()
.body(registerBody("alice"))
.when()
.post("/api/account")
.`then`()
.statusCode(200)
.body("username", is("alice"))
.body("rating", is(1500))
@Test
def registerConflictOnDuplicateUsername(): Unit =
givenRequest().body(registerBody("bob")).when().post("/api/account")
givenRequest()
.body(registerBody("bob"))
.when()
.post("/api/account")
.`then`()
.statusCode(409)
.body("error", containsString("bob"))
@Test
def loginReturns200WithToken(): Unit =
givenRequest().body(registerBody("charlie")).when().post("/api/account")
givenRequest()
.body(loginBody("charlie"))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.body("token", notNullValue())
@Test
def loginUnauthorizedOnWrongPassword(): Unit =
givenRequest().body(registerBody("dave")).when().post("/api/account")
givenRequest()
.body(loginBody("dave", "wrongpassword"))
.when()
.post("/api/account/login")
.`then`()
.statusCode(401)
@Test
def getMeReturns200(): Unit =
val token = registerAndLogin("eve")
givenRequest()
.header("Authorization", s"Bearer $token")
.when()
.get("/api/account/me")
.`then`()
.statusCode(200)
.body("username", is("eve"))
@Test
def getPublicProfileReturns200(): Unit =
givenRequest().body(registerBody("frank")).when().post("/api/account")
givenRequest()
.when()
.get("/api/account/frank")
.`then`()
.statusCode(200)
.body("username", is("frank"))
@Test
def getPublicProfileNotFound(): Unit =
givenRequest()
.when()
.get("/api/account/doesnotexist")
.`then`()
.statusCode(404)
@@ -1,179 +0,0 @@
package de.nowchess.account.resource
import de.nowchess.account.client.{CoreGameClient, CoreGameResponse}
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.{BeforeEach, Test}
import org.mockito.{ArgumentMatchers, Mockito}
@QuarkusTest
class ChallengeResourceTest:
@InjectMock
@RestClient
// scalafix:off DisableSyntax.var
var coreGameClient: CoreGameClient = scala.compiletime.uninitialized
// scalafix:on
@BeforeEach
def setup(): Unit =
Mockito.when(coreGameClient.createGame(ArgumentMatchers.any())).thenReturn(CoreGameResponse("test-game-id"))
private def givenRequest() = RestAssured.`given`().contentType(ContentType.JSON)
private def registerBody(username: String, suffix: String = "") =
val email = s"$username$suffix@test.com"
s"""{"username":"$username$suffix","email":"$email","password":"secret"}"""
private def loginBody(username: String, suffix: String = "") =
s"""{"username":"$username$suffix","password":"secret"}"""
private def registerAndLogin(username: String, suffix: String = ""): String =
givenRequest().body(registerBody(username, suffix)).when().post("/api/account")
givenRequest()
.body(loginBody(username, suffix))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.extract()
.path[String]("token")
private val clockBody =
"""{"color":"random","timeControl":{"type":"clock","limit":300,"increment":5}}"""
private def authed(token: String) =
givenRequest().header("Authorization", s"Bearer $token")
@Test
def createChallengeReturns201(): Unit =
val t1 = registerAndLogin("user1c")
registerAndLogin("user2c")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/user2c")
.`then`()
.statusCode(201)
.body("status", is("created"))
.body("color", is("random"))
@Test
def createChallengeConflictOnDuplicate(): Unit =
val t1 = registerAndLogin("user1dup")
registerAndLogin("user2dup")
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/user2dup")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/user2dup")
.`then`()
.statusCode(409)
@Test
def createChallengeSelfForbidden(): Unit =
val token = registerAndLogin("selfuser")
authed(token)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/selfuser")
.`then`()
.statusCode(400)
@Test
def acceptChallengeReturns200(): Unit =
val t1 = registerAndLogin("accUser1")
val t2 = registerAndLogin("accUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/accUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t2)
.when()
.post(s"/api/challenge/$challengeId/accept")
.`then`()
.statusCode(200)
.body("status", is("accepted"))
.body("gameId", is("test-game-id"))
@Test
def declineChallengeReturns200(): Unit =
val t1 = registerAndLogin("decUser1")
val t2 = registerAndLogin("decUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/decUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t2)
.contentType(ContentType.JSON)
.body("""{"reason":"later"}""")
.when()
.post(s"/api/challenge/$challengeId/decline")
.`then`()
.statusCode(200)
.body("status", is("declined"))
.body("declineReason", is("later"))
@Test
def cancelChallengeReturns200(): Unit =
val t1 = registerAndLogin("canUser1")
registerAndLogin("canUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/canUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t1)
.when()
.post(s"/api/challenge/$challengeId/cancel")
.`then`()
.statusCode(200)
.body("status", is("canceled"))
@Test
def listChallengesReturnsInAndOut(): Unit =
val t1 = registerAndLogin("listUser1")
registerAndLogin("listUser2")
registerAndLogin("listUser3")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/listUser2")
.`then`()
.statusCode(201)
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/listUser3")
.`then`()
.statusCode(201)
authed(t1)
.when()
.get("/api/challenge")
.`then`()
.statusCode(200)
.body("out.size()", is(2))
.body("in.size()", is(0))
-3
View File
@@ -1,3 +0,0 @@
MAJOR=0
MINOR=1
PATCH=0
-39
View File
@@ -70,42 +70,3 @@
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### Features
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-22)
### Features
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
### Bug Fixes
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([fb5c61d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fb5c61de63292e5d70c06304cba2193686aa1607))
## (2026-04-22)
### Features
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([f088c4e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f088c4e9ffcc498d3d1b6f01e8f50042d5830d55))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([8744bee](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8744bee2dd20966dae90a09c21a43d5b06f59e00))
* **rule:** Rules as a microservice ([#39](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/39)) ([093134d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/093134d36c6844ba02a36a28d5d044f09291cd1d))
### Bug Fixes
* IO microservice ([#38](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/38)) ([a386f57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a386f57c21d34ead6cc6f92836c52b714597e289))
@@ -0,0 +1,11 @@
package de.nowchess.api.bot
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
trait Bot {
def name: String
def nextMove(context: GameContext): Option[Move]
}
@@ -1,9 +0,0 @@
package de.nowchess.api.dto
/** Snapshot of remaining clock time for both players in milliseconds. -1 indicates the value is not applicable (e.g.
* inactive player in correspondence chess).
*/
final case class ClockDto(
whiteRemainingMs: Long,
blackRemainingMs: Long,
)
@@ -1,10 +1,6 @@
package de.nowchess.api.dto
import de.nowchess.api.game.GameMode
final case class CreateGameRequestDto(
white: Option[PlayerInfoDto],
black: Option[PlayerInfoDto],
timeControl: Option[TimeControlDto],
mode: Option[GameMode] = None,
)
@@ -9,6 +9,4 @@ final case class GameStateDto(
moves: List[String],
undoAvailable: Boolean,
redoAvailable: Boolean,
clock: Option[ClockDto],
takebackRequestedBy: Option[String] = None,
)
@@ -1,28 +0,0 @@
package de.nowchess.api.dto
case class GameWritebackEventDto(
gameId: String,
fen: String,
pgn: String,
moveCount: Int,
whiteId: String,
whiteName: String,
blackId: String,
blackName: String,
mode: String,
resigned: Boolean,
limitSeconds: Option[Int],
incrementSeconds: Option[Int],
daysPerMove: Option[Int],
whiteRemainingMs: Option[Long],
blackRemainingMs: Option[Long],
incrementMs: Option[Long],
clockLastTickAt: Option[Long],
clockMoveDeadline: Option[Long],
clockActiveColor: Option[String],
pendingDrawOffer: Option[String],
result: Option[String] = None,
terminationReason: Option[String] = None,
redoStack: List[String] = Nil,
pendingTakebackRequest: Option[String] = None,
)
@@ -1,3 +0,0 @@
package de.nowchess.api.dto
case class ImportFenRequest(fen: String)
@@ -4,5 +4,4 @@ final case class ImportFenRequestDto(
fen: String,
white: Option[PlayerInfoDto],
black: Option[PlayerInfoDto],
timeControl: Option[TimeControlDto],
)
@@ -1,3 +0,0 @@
package de.nowchess.api.dto
case class ImportPgnRequest(pgn: String)
@@ -1,5 +1,3 @@
package de.nowchess.api.dto
import de.nowchess.api.player.PlayerType
final case class PlayerInfoDto(id: String, displayName: String, playerType: PlayerType)
final case class PlayerInfoDto(id: String, displayName: String)
@@ -1,7 +0,0 @@
package de.nowchess.api.dto
final case class TimeControlDto(
limitSeconds: Option[Int],
incrementSeconds: Option[Int],
daysPerMove: Option[Int],
)
@@ -1,13 +0,0 @@
package de.nowchess.api.error
enum GameError:
case ParseError(details: String)
case FileReadError(details: String)
case FileWriteError(details: String)
case IllegalMove
def message: String = this match
case ParseError(d) => d
case FileReadError(d) => d
case FileWriteError(d) => d
case IllegalMove => "Illegal move"
@@ -1,55 +0,0 @@
package de.nowchess.api.game
import de.nowchess.api.board.Color
import java.time.Instant
import java.time.temporal.ChronoUnit
sealed trait ClockState:
def activeColor: Color
def afterMove(movedColor: Color, at: Instant): Either[Color, ClockState]
def remainingMs(color: Color, now: Instant): Long
final case class LiveClockState(
whiteRemainingMs: Long,
blackRemainingMs: Long,
incrementMs: Long,
lastTickAt: Instant,
activeColor: Color,
) extends ClockState:
def remainingMs(color: Color, now: Instant): Long =
val stored = if color == Color.White then whiteRemainingMs else blackRemainingMs
if color == activeColor then math.max(0L, stored - (now.toEpochMilli - lastTickAt.toEpochMilli))
else stored
def afterMove(movedColor: Color, at: Instant): Either[Color, ClockState] =
val elapsed = at.toEpochMilli - lastTickAt.toEpochMilli
val newRemaining =
(if movedColor == Color.White then whiteRemainingMs else blackRemainingMs) - elapsed + incrementMs
if newRemaining <= 0 then Left(movedColor)
else
val (w, b) =
if movedColor == Color.White then (newRemaining, blackRemainingMs)
else (whiteRemainingMs, newRemaining)
Right(copy(whiteRemainingMs = w, blackRemainingMs = b, lastTickAt = at, activeColor = movedColor.opposite))
final case class CorrespondenceClockState(
moveDeadline: Instant,
daysPerMove: Int,
activeColor: Color,
) extends ClockState:
def remainingMs(color: Color, now: Instant): Long =
math.max(0L, moveDeadline.toEpochMilli - now.toEpochMilli)
def afterMove(movedColor: Color, at: Instant): Either[Color, ClockState] =
if at.isAfter(moveDeadline) then Left(movedColor)
else Right(copy(moveDeadline = at.plus(daysPerMove.toLong, ChronoUnit.DAYS), activeColor = movedColor.opposite))
object ClockState:
def fromTimeControl(tc: TimeControl, activeColor: Color, now: Instant): Option[ClockState] =
tc match
case TimeControl.Clock(limit, inc) =>
val ms = limit * 1000L
Some(LiveClockState(ms, ms, inc * 1000L, now, activeColor))
case TimeControl.Correspondence(days) =>
Some(CorrespondenceClockState(now.plus(days.toLong, ChronoUnit.DAYS), days, activeColor))
case TimeControl.Unlimited => None
@@ -1,4 +0,0 @@
package de.nowchess.api.game
enum GameMode:
case Open, Authenticated
@@ -4,10 +4,5 @@ import de.nowchess.api.board.Color
/** Outcome of a finished game. */
enum GameResult:
case Win(color: Color, winReason: WinReason)
case Win(color: Color)
case Draw(reason: DrawReason)
enum WinReason:
case Checkmate
case Resignation
case TimeControl
@@ -0,0 +1,8 @@
package de.nowchess.api.game
import de.nowchess.api.bot.Bot
import de.nowchess.api.player.PlayerInfo
sealed trait Participant
final case class Human(playerInfo: PlayerInfo) extends Participant
final case class BotParticipant(bot: Bot) extends Participant
@@ -1,6 +0,0 @@
package de.nowchess.api.game
enum TimeControl:
case Clock(limitSeconds: Int, incrementSeconds: Int)
case Correspondence(daysPerMove: Int)
case Unlimited
@@ -1,107 +0,0 @@
package de.nowchess.api.grpc
import de.nowchess.api.board.{CastlingRights as DomainCastlingRights, *}
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
import de.nowchess.api.move.{Move as DomainMove, MoveType, PromotionPiece}
import scala.jdk.CollectionConverters.*
trait ProtoMapperBase[PC, PPT, PMK, PM, PSP, PBoard, PCR, PRK, PGC]:
def toProtoColor(c: Color): PC
def fromProtoColor(c: PC): Color
def toProtoPieceType(pt: PieceType): PPT
def fromProtoPieceType(pt: PPT): PieceType
def toProtoMoveKind(mt: MoveType): PMK
def fromProtoMoveKind(k: PMK): MoveType
def toProtoMove(m: DomainMove): PM
def fromProtoMove(m: PM): Option[DomainMove]
def toProtoSquarePiece(sq: Square, piece: Piece): PSP
def fromProtoSquarePiece(sp: PSP): Option[(Square, Piece)]
def toProtoBoard(board: Board): java.util.List[PSP]
def fromProtoBoard(pieces: java.util.List[PSP]): Board
def toProtoResultKind(r: Option[GameResult]): PRK
def fromProtoResultKind(k: PRK): Option[GameResult]
def toProtoCastlingRights(cr: DomainCastlingRights): PCR
def fromProtoCastlingRights(pcr: PCR): DomainCastlingRights
def toProtoGameContext(ctx: GameContext): PGC
def fromProtoGameContext(p: PGC): GameContext
object ProtoMapperBase:
def colorConversions[PC](white: PC, black: PC): (Color => PC, PC => Color) =
(
(c: Color) =>
c match
case Color.White => white
case Color.Black => black,
(pc: PC) =>
if pc == white then Color.White
else Color.Black,
)
def pieceTypeConversions[PPT](
pawn: PPT,
knight: PPT,
bishop: PPT,
rook: PPT,
queen: PPT,
king: PPT,
): (PieceType => PPT, PPT => PieceType) =
(
(pt: PieceType) =>
pt match
case PieceType.Pawn => pawn
case PieceType.Knight => knight
case PieceType.Bishop => bishop
case PieceType.Rook => rook
case PieceType.Queen => queen
case PieceType.King => king,
(ppt: PPT) =>
if ppt == pawn then PieceType.Pawn
else if ppt == knight then PieceType.Knight
else if ppt == bishop then PieceType.Bishop
else if ppt == rook then PieceType.Rook
else if ppt == queen then PieceType.Queen
else PieceType.King,
)
def moveKindConversions[PMK](
quiet: PMK,
capture: PMK,
castleKingside: PMK,
castleQueenside: PMK,
enPassant: PMK,
promoQueen: PMK,
promoRook: PMK,
promoBishop: PMK,
promoKnight: PMK,
): (MoveType => PMK, PMK => MoveType) =
(
(mt: MoveType) =>
mt match
case MoveType.Normal(false) => quiet
case MoveType.Normal(true) => capture
case MoveType.CastleKingside => castleKingside
case MoveType.CastleQueenside => castleQueenside
case MoveType.EnPassant => enPassant
case MoveType.Promotion(PromotionPiece.Queen) => promoQueen
case MoveType.Promotion(PromotionPiece.Rook) => promoRook
case MoveType.Promotion(PromotionPiece.Bishop) => promoBishop
case MoveType.Promotion(PromotionPiece.Knight) => promoKnight,
(pmk: PMK) =>
if pmk == quiet then MoveType.Normal(false)
else if pmk == capture then MoveType.Normal(true)
else if pmk == castleKingside then MoveType.CastleKingside
else if pmk == castleQueenside then MoveType.CastleQueenside
else if pmk == enPassant then MoveType.EnPassant
else if pmk == promoQueen then MoveType.Promotion(PromotionPiece.Queen)
else if pmk == promoRook then MoveType.Promotion(PromotionPiece.Rook)
else if pmk == promoBishop then MoveType.Promotion(PromotionPiece.Bishop)
else if pmk == promoKnight then MoveType.Promotion(PromotionPiece.Knight)
else MoveType.Normal(false),
)
@@ -1,7 +0,0 @@
package de.nowchess.api.io
import de.nowchess.api.error.GameError
import de.nowchess.api.game.GameContext
trait GameContextImport:
def importGameContext(input: String): Either[GameError, GameContext]
@@ -23,10 +23,4 @@ object PlayerId:
final case class PlayerInfo(
id: PlayerId,
displayName: String,
playerType: PlayerType = PlayerType.Human,
)
enum PlayerType:
case Human
case OfficialBot
case Bot
@@ -1,9 +0,0 @@
package de.nowchess.api.rules
final case class PostMoveStatus(
isCheckmate: Boolean,
isStalemate: Boolean,
isInsufficientMaterial: Boolean,
isCheck: Boolean,
isThreefoldRepetition: Boolean,
)
@@ -1,109 +0,0 @@
package de.nowchess.api.game
import de.nowchess.api.board.Color
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import java.time.Instant
import java.time.temporal.ChronoUnit
class ClockStateTest extends AnyFunSuite with Matchers:
private val t0 = Instant.parse("2024-01-01T00:00:00Z")
private val t1s = t0.plusSeconds(1)
private val t5s = t0.plusSeconds(5)
// ── LiveClockState ────────────────────────────────────────────────────────
test("LiveClockState.afterMove deducts elapsed and adds increment on valid move"):
val cs = LiveClockState(300_000L, 300_000L, 3_000L, t0, Color.White)
cs.afterMove(Color.White, t5s) match
case Right(updated: LiveClockState) =>
updated.whiteRemainingMs shouldBe (300_000L - 5_000L + 3_000L)
updated.blackRemainingMs shouldBe 300_000L
updated.activeColor shouldBe Color.Black
updated.lastTickAt shouldBe t5s
case other => fail(s"Expected Right(LiveClockState), got $other")
test("LiveClockState.afterMove returns Left when time exhausted"):
val cs = LiveClockState(2_000L, 300_000L, 0L, t0, Color.White)
cs.afterMove(Color.White, t5s) shouldBe Left(Color.White)
test("LiveClockState.afterMove returns Left when time exactly zero"):
val cs = LiveClockState(5_000L, 300_000L, 0L, t0, Color.White)
cs.afterMove(Color.White, t5s) shouldBe Left(Color.White)
test("LiveClockState.remainingMs for active color deducts live elapsed"):
val cs = LiveClockState(300_000L, 300_000L, 0L, t0, Color.White)
val now = t5s
cs.remainingMs(Color.White, now) shouldBe (300_000L - 5_000L)
test("LiveClockState.remainingMs for inactive color returns stored value"):
val cs = LiveClockState(200_000L, 300_000L, 0L, t0, Color.White)
cs.remainingMs(Color.Black, t5s) shouldBe 300_000L
test("LiveClockState.remainingMs clamps to zero when overdue"):
val cs = LiveClockState(1_000L, 300_000L, 0L, t0, Color.White)
cs.remainingMs(Color.White, t5s) shouldBe 0L
test("LiveClockState.afterMove advances activeColor to opponent"):
val cs = LiveClockState(300_000L, 300_000L, 0L, t0, Color.Black)
cs.afterMove(Color.Black, t1s) match
case Right(updated: LiveClockState) => updated.activeColor shouldBe Color.White
case other => fail(s"Expected Right, got $other")
// ── CorrespondenceClockState ──────────────────────────────────────────────
test("CorrespondenceClockState.afterMove advances deadline on valid move"):
val deadline = t0.plus(3L, ChronoUnit.DAYS)
val cs = CorrespondenceClockState(deadline, 3, Color.White)
cs.afterMove(Color.White, t1s) match
case Right(updated: CorrespondenceClockState) =>
updated.moveDeadline shouldBe t1s.plus(3L, ChronoUnit.DAYS)
updated.activeColor shouldBe Color.Black
case other => fail(s"Expected Right(CorrespondenceClockState), got $other")
test("CorrespondenceClockState.afterMove returns Left when move is past deadline"):
val deadline = t0.plus(1L, ChronoUnit.DAYS)
val cs = CorrespondenceClockState(deadline, 3, Color.White)
val lateMove = t0.plus(2L, ChronoUnit.DAYS)
cs.afterMove(Color.White, lateMove) shouldBe Left(Color.White)
test("CorrespondenceClockState.remainingMs returns time until deadline"):
val deadline = t0.plus(3L, ChronoUnit.DAYS)
val cs = CorrespondenceClockState(deadline, 3, Color.White)
val expected = deadline.toEpochMilli - t1s.toEpochMilli
cs.remainingMs(Color.White, t1s) shouldBe expected
test("CorrespondenceClockState.remainingMs clamps to zero when overdue"):
val deadline = t0.plus(1L, ChronoUnit.DAYS)
val cs = CorrespondenceClockState(deadline, 3, Color.White)
val overdue = t0.plus(2L, ChronoUnit.DAYS)
cs.remainingMs(Color.White, overdue) shouldBe 0L
// ── ClockState.fromTimeControl ────────────────────────────────────────────
test("fromTimeControl with Clock returns LiveClockState with correct initial values"):
ClockState.fromTimeControl(TimeControl.Clock(300, 3), Color.White, t0) match
case Some(cs: LiveClockState) =>
cs.whiteRemainingMs shouldBe 300_000L
cs.blackRemainingMs shouldBe 300_000L
cs.incrementMs shouldBe 3_000L
cs.activeColor shouldBe Color.White
cs.lastTickAt shouldBe t0
case other => fail(s"Expected Some(LiveClockState), got $other")
test("fromTimeControl with Correspondence returns CorrespondenceClockState"):
ClockState.fromTimeControl(TimeControl.Correspondence(3), Color.White, t0) match
case Some(cs: CorrespondenceClockState) =>
cs.moveDeadline shouldBe t0.plus(3L, ChronoUnit.DAYS)
cs.daysPerMove shouldBe 3
cs.activeColor shouldBe Color.White
case other => fail(s"Expected Some(CorrespondenceClockState), got $other")
test("fromTimeControl with Unlimited returns None"):
ClockState.fromTimeControl(TimeControl.Unlimited, Color.White, t0) shouldBe None
test("fromTimeControl with Black as starting color sets activeColor correctly"):
ClockState.fromTimeControl(TimeControl.Clock(300, 0), Color.Black, t0) match
case Some(cs: LiveClockState) => cs.activeColor shouldBe Color.Black
case other => fail(s"Expected Some(LiveClockState), got $other")
@@ -1,7 +1,6 @@
package de.nowchess.api.game
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square}
import de.nowchess.api.game.WinReason.Checkmate
import de.nowchess.api.move.Move
import de.nowchess.api.game.{DrawReason, GameResult}
import org.scalatest.funsuite.AnyFunSuite
@@ -62,7 +61,7 @@ class GameContextTest extends AnyFunSuite with Matchers:
GameContext.initial.withMove(move).moves shouldBe List(move)
test("withResult sets Win result"):
val win = Some(GameResult.Win(Color.White, Checkmate))
val win = Some(GameResult.Win(Color.White))
GameContext.initial.withResult(win).result shouldBe win
test("withResult sets Draw result"):
@@ -70,7 +69,7 @@ class GameContextTest extends AnyFunSuite with Matchers:
GameContext.initial.withResult(draw).result shouldBe draw
test("withResult clears result"):
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black, Checkmate)))
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black)))
ctx.withResult(None).result shouldBe None
test("kingSquare returns white king position"):
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=12
MINOR=9
PATCH=0
-5
View File
@@ -1,5 +0,0 @@
.gitignore
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*
-41
View File
@@ -1,41 +0,0 @@
# Gradle
.gradle/
build/
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/
# TLS Certificates
.certs/

Some files were not shown because too many files have changed in this diff Show More