Compare commits

...

14 Commits

Author SHA1 Message Date
Janis 04a3ad721c feat: true-microservices (#40)
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #40
2026-04-29 22:06:20 +02:00
Janis 590924254e feat: true-microservices (#40)
Reviewed-on: #40
2026-04-29 22:06:01 +02:00
TeamCity 67511fc649 ci: bump version with Build-50 2026-04-22 08:21:04 +00:00
Janis 093134d36c feat(rule): Rules as a microservice (#39)
Build & Test (NowChessSystems) TeamCity build finished
Co-authored-by: LQ63 <lkhermann@web.de>
Co-authored-by: TeamCity <teamcity@service.local>
Reviewed-on: #39
2026-04-22 10:09:35 +02:00
Janis ffeb3ce338 fix: disable jar packaging in native image build
Build & Test (NowChessSystems) TeamCity build finished
2026-04-22 09:50:10 +02:00
TeamCity 52b171c7af ci: bump version with Build-46
Build & Test (NowChessSystems) TeamCity build finished
2026-04-22 06:51:59 +00:00
Janis a386f57c21 fix: IO microservice (#38)
Reviewed-on: #38
2026-04-22 08:36:54 +02:00
TeamCity 3ca2afbb4b ci: bump version with Build-45 2026-04-21 13:49:26 +00:00
Janis b5a2966ada feat: NCS-53 changed IO to MicroService for easier scaling (#37)
Reviewed-on: #37
Reviewed-by: Shahd Lala <shosho996@blackhole.local>
2026-04-21 15:38:58 +02:00
TeamCity 74a4fce0ca ci: bump version with Build-44 2026-04-21 11:13:30 +00:00
Janis 8fc97bde02 refactor: align JSON string formatting in JsonParserTest 2026-04-21 13:02:18 +02:00
shosho996 2d75b2e80e test: NCS-45 IO Test reduction (#32)
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 f088c4e9ff feat: NCS-37 Quarkus integration (#35)
Reviewed-on: #35
Reviewed-by: Leon Hermann <lq@blackhole.local>
2026-04-21 12:35:20 +02:00
TeamCity 8a1cf909d4 ci: bump version with Build-43 2026-04-19 20:53:56 +00:00
440 changed files with 15653 additions and 4325 deletions
+422 -87
View File
@@ -2,11 +2,58 @@
> **Stack:** raw-http | none | unknown | scala
> 0 routes | 0 models | 0 components | 63 lib files | 1 env vars | 1 middleware
> 0 routes + 40 rpc | 0 models | 0 components | 146 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`
@@ -25,6 +72,74 @@
- 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
@@ -47,19 +162,45 @@
- class Square
- function fromAlgebraic
- function offset
- `modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`
- class Bot
- function name
- function nextMove
- `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
- function withTurn
- function withCastlingRights
- function withEnPassantSquare
- function withHalfMoveClock
- function withMove
- _...2 more_
- _...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: ()
@@ -79,6 +220,7 @@
- `modules/bot/python/src/export.py` — function export_to_nbai: (weights_file, output_file, trained_by, train_loss)
- `modules/bot/python/src/generate.py` — function play_random_game_and_collect_positions: (output_file, total_positions, samples_per_game, min_move, max_move, num_workers)
- `modules/bot/python/src/label.py` — function normalize_evaluation: (cp_value, method, scale), function label_positions_with_stockfish: (positions_file, output_file, stockfish_path, batch_size, depth, verbose, normalize, num_workers)
- `modules/bot/python/src/lichess_importer.py` — function import_lichess_evals: (input_path, output_file, max_positions, min_depth, verbose) -> int
- `modules/bot/python/src/tactical_positions_extractor.py`
- function download_and_extract_puzzle_db: (url, output_dir)
- function extract_puzzle_positions: (puzzle_csv, max_puzzles) -> Set[str]
@@ -90,14 +232,10 @@
- function fen_to_features: (fen)
- function find_next_version: (base_name)
- function save_metadata: (weights_file, metadata)
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio)
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio)
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio, hidden_sizes)
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio, hidden_sizes)
- class NNUEDataset
- _...1 more_
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`
- class Bot
- function name
- function nextMove
- `modules/bot/src/main/scala/de/nowchess/bot/BotController.scala`
- class BotController
- function getBot
@@ -148,7 +286,6 @@
- function bestMoveWithTime
- function loop
- function loop
- _...2 more_
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
- class MoveOrdering
- class OrderingContext
@@ -158,6 +295,7 @@
- function getHistory
- _...3 more_
- `modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala`
- function advance
- function probe
- function store
- function clear
@@ -167,29 +305,136 @@
- class ZobristHash
- function hash
- function nextHash
- `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/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/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 isPendingPromotion
- function board
- function turn
- function context
- function canUndo
- _...11 more_
- 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_
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
- function context
- class Observer
@@ -198,8 +443,52 @@
- function subscribe
- function unsubscribe
- _...1 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/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
- 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_
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`
- class GameResource
- function onGameEvent
- function createGame
- function getGame
- 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/GameFileService.scala`
- class GameFileService
- function saveGameToFile
@@ -228,6 +517,15 @@
- 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`
@@ -240,39 +538,76 @@
- function importGameContext
- function parsePgn
- function parseAlgebraicMove
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
- class RuleSet
- `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
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...4 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
- class DefaultRules
- function loop
- function toMoves
- function loop
- `modules/ui/src/main/scala/de/nowchess/ui/Main.scala` — class Main, function main
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala`
- class ChessBoardView
- function updateBoard
- function updateUndoRedoButtons
- function showMessage
- function showPromotionDialog
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessGUI.scala`
- class ChessGUIApp
- class ChessGUILauncher
- function getEngine
- function launch
- `modules/ui/src/main/scala/de/nowchess/ui/gui/GUIObserver.scala` — class GUIObserver
- `modules/ui/src/main/scala/de/nowchess/ui/gui/PieceSprites.scala`
- class PieceSprites
- function loadPieceImage
- class SquareColors
- `modules/ui/src/main/scala/de/nowchess/ui/terminal/TerminalUI.scala` — class TerminalUI, function start
- `modules/ui/src/main/scala/de/nowchess/ui/utils/PieceUnicode.scala` — function unicode
- `modules/ui/src/main/scala/de/nowchess/ui/utils/Renderer.scala` — class Renderer, function render
- _...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
---
@@ -295,39 +630,39 @@
## Most Imported Files (change these carefully)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **60** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **40** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** 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 **10** 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 **8** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
- `modules/bot/src/main/scala/de/nowchess/bot/BotDifficulty.scala` — imported by **5** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** 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
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
- `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/bot/Bot.scala` — imported by **6** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/bot/src/main/scala/de/nowchess/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` +55 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.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/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +35 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` +34 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/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`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 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` +17 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.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`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 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` +16 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` +5 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/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`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
- `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
---
+30 -30
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 **60** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **40** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** 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 **10** 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 **8** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — imported by **5** files
- `modules/bot/src/main/scala/de/nowchess/bot/BotDifficulty.scala` — imported by **5** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** 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
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — imported by **4** files
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
- `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/bot/Bot.scala` — imported by **6** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/bot/src/main/scala/de/nowchess/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` +55 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.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/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/BotMoveRepetition.scala` +35 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` +34 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/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`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 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` +17 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.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`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 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` +16 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` +5 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/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`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
- `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
+344 -56
View File
@@ -16,6 +16,74 @@
- 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
@@ -38,19 +106,45 @@
- class Square
- function fromAlgebraic
- function offset
- `modules/api/src/main/scala/de/nowchess/api/bot/Bot.scala`
- class Bot
- function name
- function nextMove
- `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
- function withTurn
- function withCastlingRights
- function withEnPassantSquare
- function withHalfMoveClock
- function withMove
- _...2 more_
- _...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: ()
@@ -70,6 +164,7 @@
- `modules/bot/python/src/export.py` — function export_to_nbai: (weights_file, output_file, trained_by, train_loss)
- `modules/bot/python/src/generate.py` — function play_random_game_and_collect_positions: (output_file, total_positions, samples_per_game, min_move, max_move, num_workers)
- `modules/bot/python/src/label.py` — function normalize_evaluation: (cp_value, method, scale), function label_positions_with_stockfish: (positions_file, output_file, stockfish_path, batch_size, depth, verbose, normalize, num_workers)
- `modules/bot/python/src/lichess_importer.py` — function import_lichess_evals: (input_path, output_file, max_positions, min_depth, verbose) -> int
- `modules/bot/python/src/tactical_positions_extractor.py`
- function download_and_extract_puzzle_db: (url, output_dir)
- function extract_puzzle_positions: (puzzle_csv, max_puzzles) -> Set[str]
@@ -81,14 +176,10 @@
- function fen_to_features: (fen)
- function find_next_version: (base_name)
- function save_metadata: (weights_file, metadata)
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio)
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio)
- function train_nnue: (data_file, output_file, epochs, batch_size, lr, checkpoint, stockfish_depth, use_versioning, early_stopping_patience, weight_decay, subsample_ratio, hidden_sizes)
- function burst_train: (data_file, output_file, duration_minutes, epochs_per_season, early_stopping_patience, batch_size, lr, initial_checkpoint, stockfish_depth, use_versioning, weight_decay, subsample_ratio, hidden_sizes)
- class NNUEDataset
- _...1 more_
- `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`
- class Bot
- function name
- function nextMove
- `modules/bot/src/main/scala/de/nowchess/bot/BotController.scala`
- class BotController
- function getBot
@@ -139,7 +230,6 @@
- function bestMoveWithTime
- function loop
- function loop
- _...2 more_
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
- class MoveOrdering
- class OrderingContext
@@ -149,6 +239,7 @@
- function getHistory
- _...3 more_
- `modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala`
- function advance
- function probe
- function store
- function clear
@@ -158,29 +249,136 @@
- class ZobristHash
- function hash
- function nextHash
- `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/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/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 isPendingPromotion
- function board
- function turn
- function context
- function canUndo
- _...11 more_
- 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_
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`
- function context
- class Observer
@@ -189,8 +387,52 @@
- function subscribe
- function unsubscribe
- _...1 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/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
- 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_
- `modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala`
- class GameResource
- function onGameEvent
- function createGame
- function getGame
- 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/GameFileService.scala`
- class GameFileService
- function saveGameToFile
@@ -219,6 +461,15 @@
- 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`
@@ -231,36 +482,73 @@
- function importGameContext
- function parsePgn
- function parseAlgebraicMove
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala`
- class RuleSet
- `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
- function candidateMoves
- function legalMoves
- function allLegalMoves
- function isCheck
- function isCheckmate
- _...4 more_
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala`
- class DefaultRules
- function loop
- function toMoves
- function loop
- `modules/ui/src/main/scala/de/nowchess/ui/Main.scala` — class Main, function main
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala`
- class ChessBoardView
- function updateBoard
- function updateUndoRedoButtons
- function showMessage
- function showPromotionDialog
- `modules/ui/src/main/scala/de/nowchess/ui/gui/ChessGUI.scala`
- class ChessGUIApp
- class ChessGUILauncher
- function getEngine
- function launch
- `modules/ui/src/main/scala/de/nowchess/ui/gui/GUIObserver.scala` — class GUIObserver
- `modules/ui/src/main/scala/de/nowchess/ui/gui/PieceSprites.scala`
- class PieceSprites
- function loadPieceImage
- class SquareColors
- `modules/ui/src/main/scala/de/nowchess/ui/terminal/TerminalUI.scala` — class TerminalUI, function start
- `modules/ui/src/main/scala/de/nowchess/ui/utils/PieceUnicode.scala` — function unicode
- `modules/ui/src/main/scala/de/nowchess/ui/utils/Renderer.scala` — class Renderer, function render
- _...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
+198
View File
@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NowChessSystems — codesight report</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a0f;--card:#12121a;--border:#1e1e2e;--text:#e0e0e8;--muted:#6b6b80;--accent:#6366f1;--accent2:#22d3ee;--green:#22c55e;--orange:#f59e0b;--red:#ef4444;--pink:#ec4899}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:2rem;max-width:1400px;margin:0 auto;line-height:1.6}
h1{font-size:2.5rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:.25rem}
.subtitle{color:var(--muted);font-size:1rem;margin-bottom:2rem}
.stack-badge{display:inline-block;background:var(--card);border:1px solid var(--border);border-radius:6px;padding:2px 10px;font-size:.85rem;color:var(--accent2);margin:0 4px 4px 0}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin:2rem 0}
.stat{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;text-align:center}
.stat-value{font-size:2rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.stat-label{color:var(--muted);font-size:.85rem;margin-top:.25rem}
.token-hero{background:linear-gradient(135deg,#1a1a2e,#16213e);border:1px solid var(--accent);border-radius:16px;padding:2rem;margin:2rem 0;text-align:center}
.token-saved{font-size:3rem;font-weight:900;color:var(--green)}
.token-detail{color:var(--muted);font-size:.9rem;margin-top:.5rem}
.section{margin:2.5rem 0}
.section h2{font-size:1.4rem;font-weight:700;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid var(--border)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:1rem;transition:border-color .2s}
.card:hover{border-color:var(--accent)}
.card-title{font-weight:700;font-size:1rem;margin-bottom:.5rem}
.card-meta{color:var(--muted);font-size:.8rem}
.tag{display:inline-block;background:rgba(99,102,241,.15);color:var(--accent);border-radius:4px;padding:1px 6px;font-size:.75rem;margin:1px}
.tag-auth{background:rgba(239,68,68,.15);color:var(--red)}
.tag-db{background:rgba(34,211,238,.15);color:var(--accent2)}
.tag-ai{background:rgba(236,72,153,.15);color:var(--pink)}
.tag-payment{background:rgba(245,158,11,.15);color:var(--orange)}
.tag-email{background:rgba(34,197,94,.15);color:var(--green)}
.tag-queue{background:rgba(168,85,247,.15);color:#a855f7}
.tag-cache{background:rgba(245,158,11,.15);color:var(--orange)}
.method{font-weight:700;font-size:.8rem;padding:2px 6px;border-radius:4px;margin-right:6px}
.method-GET{background:rgba(34,197,94,.2);color:var(--green)}
.method-POST{background:rgba(99,102,241,.2);color:var(--accent)}
.method-PUT{background:rgba(245,158,11,.2);color:var(--orange)}
.method-PATCH{background:rgba(245,158,11,.2);color:var(--orange)}
.method-DELETE{background:rgba(239,68,68,.2);color:var(--red)}
.method-ALL{background:rgba(107,107,128,.2);color:var(--muted)}
.route-path{font-family:'Fira Code',monospace;font-size:.9rem}
.route-contract{color:var(--muted);font-size:.8rem;font-style:italic;margin-left:.5rem}
.field{display:flex;gap:.5rem;padding:3px 0;font-size:.9rem}
.field-name{font-family:monospace;color:var(--accent2)}
.field-type{color:var(--muted);font-family:monospace}
.field-flags{display:flex;gap:3px}
.flag{font-size:.7rem;padding:0 4px;border-radius:3px;background:rgba(99,102,241,.1);color:var(--accent)}
.flag-pk{background:rgba(245,158,11,.2);color:var(--orange)}
.flag-fk{background:rgba(34,211,238,.2);color:var(--accent2)}
.flag-unique{background:rgba(236,72,153,.2);color:var(--pink)}
.hot-bar{height:8px;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:4px;margin-top:4px}
.component-props{color:var(--muted);font-size:.85rem}
.badge-client{background:rgba(34,197,94,.15);color:var(--green);font-size:.75rem;padding:1px 6px;border-radius:4px}
.badge-server{background:rgba(99,102,241,.15);color:var(--accent);font-size:.75rem;padding:1px 6px;border-radius:4px}
.env-required{color:var(--red);font-weight:600;font-size:.8rem}
.env-default{color:var(--green);font-size:.8rem}
.footer{text-align:center;color:var(--muted);margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);font-size:.85rem}
.footer a{color:var(--accent);text-decoration:none}
table{width:100%;border-collapse:collapse}
table td,table th{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);font-size:.9rem}
table th{color:var(--muted);font-size:.8rem;font-weight:600;text-transform:uppercase}
</style>
</head>
<body>
<h1>NowChessSystems</h1>
<div class="subtitle">AI Context Map — generated by codesight</div>
<div>
<span class="stack-badge">raw-http</span>
<span class="stack-badge">unknown</span>
<span class="stack-badge">scala</span>
</div>
<div class="token-hero">
<div class="token-saved">~20,573 tokens saved</div>
<div class="token-detail">
Output: 5,297 tokens — Exploration cost without codesight: ~25,870 tokens — 149 files scanned
</div>
</div>
<div class="stats">
<div class="stat"><div class="stat-value">0</div><div class="stat-label">Routes</div></div>
<div class="stat"><div class="stat-value">0</div><div class="stat-label">Models</div></div>
<div class="stat"><div class="stat-value">0</div><div class="stat-label">Components</div></div>
<div class="stat"><div class="stat-value">63</div><div class="stat-label">Libraries</div></div>
<div class="stat"><div class="stat-value">1</div><div class="stat-label">Env Vars</div></div>
<div class="stat"><div class="stat-value">1</div><div class="stat-label">Middleware</div></div>
<div class="stat"><div class="stat-value">383</div><div class="stat-label">Import Links</div></div>
</div>
<div class="section">
<h2>Dependency Hot Files</h2>
<div class="grid">
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala</div>
<div class="card-meta">imported by 64 files</div>
<div class="hot-bar" style="width:100%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/move/Move.scala</div>
<div class="card-meta">imported by 44 files</div>
<div class="hot-bar" style="width:69%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/Square.scala</div>
<div class="card-meta">imported by 40 files</div>
<div class="hot-bar" style="width:63%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/Color.scala</div>
<div class="card-meta">imported by 35 files</div>
<div class="hot-bar" style="width:55%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/Board.scala</div>
<div class="card-meta">imported by 19 files</div>
<div class="hot-bar" style="width:30%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/Piece.scala</div>
<div class="card-meta">imported by 18 files</div>
<div class="hot-bar" style="width:28%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala</div>
<div class="card-meta">imported by 17 files</div>
<div class="hot-bar" style="width:27%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala</div>
<div class="card-meta">imported by 17 files</div>
<div class="hot-bar" style="width:27%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala</div>
<div class="card-meta">imported by 11 files</div>
<div class="hot-bar" style="width:17%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala</div>
<div class="card-meta">imported by 10 files</div>
<div class="hot-bar" style="width:16%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala</div>
<div class="card-meta">imported by 9 files</div>
<div class="hot-bar" style="width:14%"></div>
</div>
<div class="card">
<div class="card-title" style="font-size:.9rem">modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala</div>
<div class="card-meta">imported by 7 files</div>
<div class="hot-bar" style="width:11%"></div>
</div>
</div>
</div>
<div class="section">
<h2>Environment Variables</h2>
<table>
<tr><th>Variable</th><th>Status</th><th>Source</th></tr>
<tr>
<td><code>STOCKFISH_PATH</code></td>
<td><span class="env-required">required</span></td>
<td class="card-meta">modules/bot/python/nnue.py</td>
</tr>
</table>
</div>
<div class="section">
<h2>Middleware</h2>
<div class="grid">
<div class="card">
<div class="card-title">generate <span class="tag tag-custom">custom</span></div>
<div class="card-meta">modules/bot/python/src/generate.py</div>
</div>
</div>
</div>
<div class="footer">
Generated by <a href="https://github.com/Houseofmvps/codesight">codesight</a> — see your codebase clearly
</div>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
# 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-12 — re-run `npx codesight --wiki` if the codebase has changed._
_Generated 2026-04-23 — 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: **0** required, **0** with defaults
- Env vars: **1** 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-12 · 2 articles · [codesight](https://github.com/Houseofmvps/codesight)_
_Last compiled: 2026-04-23 · 2 articles · [codesight](https://github.com/Houseofmvps/codesight)_
+2
View File
@@ -3,3 +3,5 @@
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
+15 -7
View File
@@ -4,16 +4,24 @@
**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 **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
- `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`
---
_Back to [index.md](./index.md) · Generated 2026-04-12_
_Back to [index.md](./index.md) · Generated 2026-04-23_
+12
View File
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,18 @@
# 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
@@ -0,0 +1,39 @@
{
"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
@@ -0,0 +1,65 @@
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:
+94
View File
@@ -0,0 +1,94 @@
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:
- id: check
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" || "${{ github.actor }}" == "TeamCity" ]]; then
echo "allowed=true" >> "$GITHUB_OUTPUT"
else
echo "allowed=false" >> "$GITHUB_OUTPUT"
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: Build native binary
run: ./gradlew :modules:${{ matrix.module }}:build -Dquarkus.native.enabled=true -Dquarkus.package.jar.enabled=false --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=sha,prefix=,format=short
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,6 +41,9 @@ 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
+9 -3
View File
@@ -5,17 +5,23 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="corretto-21" />
<option name="gradleJvm" value="ms-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" />
<option value="$PROJECT_DIR$/modules/bot-platform" />
<option value="$PROJECT_DIR$/modules/coordinator" />
<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/ui" />
<option value="$PROJECT_DIR$/modules/security" />
<option value="$PROJECT_DIR$/modules/store" />
<option value="$PROJECT_DIR$/modules/ws" />
</set>
</option>
</GradleProjectSettings>
+2 -1
View File
@@ -1,9 +1,10 @@
<?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="corretto-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
@@ -0,0 +1,33 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="NowChessSystems.modules.core.main" type="QuarkusRunConfigurationType" factoryName="Quarkus" nameIsGenerated="true">
<module name="NowChessSystems.modules.core.main" />
<QsGradleRunConfiguration>
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/modules/core" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="quarkusDev" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<profile>dev</profile>
</QsGradleRunConfiguration>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
+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.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.main,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">
<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">
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
<parameters>
+27 -30
View File
@@ -9,8 +9,9 @@ Scala 3.5.1 · Gradle 9
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Try to stick to these commands for consistency.
Use consistently.
## Modules
@@ -21,18 +22,17 @@ 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
- Use immutable data and pure functions.
- Keep functions under 30 lines. If you need "and" to describe it, split it.
- Keep cyclomatic complexity under 15.
- Avoid comments. Let names carry intent; comment only non-obvious algorithms.
- Scan for duplicated logic before finishing. Extract it.
- Immutable data, pure functions.
- Functions under 30 lines. Need "and"? Split it.
- Cyclomatic complexity under 15.
- No comments. Names carry intent. Comment non-obvious algorithms only.
- Scan duplicated logic. Extract.
- Follow default Sonar style for Scala.
- Use `Option` or `Either` for fallible operations; avoid exceptions for control flow.
- Naming: types are PascalCase, functions/values are camelCase.
- `Option`/`Either` for fallible ops. Skip exceptions for control flow.
- Naming: types PascalCase, functions/values camelCase.
## Code Quality
@@ -40,23 +40,23 @@ Try to stick to these commands for consistency.
### Linters
- **scalafmt** — enforces formatting; run `./gradlew spotlessScalaCheck` to check and `./gradlew spotlessScalaApply` to refactor.
- **scalafix** — enforces style and detects unused imports/code; run `./gradlew scalafix` to apply rules.
- **scalafmt** — Enforces formatting. Check: `./gradlew spotlessScalaCheck`. Refactor: `./gradlew spotlessScalaApply`.
- **scalafix** — Enforces style, detects unused imports/code. Run: `./gradlew scalafix`.
## Architecture Decisions
- **Immutable state as primary model:** GameContext (api) holds board, history, player stateimmutable, passed through the system. Each move creates a new GameContext, enabling undo/redo without side effects.
- **Observer pattern for UI decoupling:** GameEngine publishes move/state events; CommandInvoker queues moves; UI listens to events, not polling. GameEngine never imports UI code.
- **RuleSet trait encapsulates rules:** Move generation, check, castling, en passant all in RuleSet impl. GameEngine calls rules as a black box; rules don't know about the rest of core.
- **Polyglot hash must follow spec index layout:** piece keys use interleaved mapping `(pieceType * 2 + colorBit)` with black=0/white=1, castling keys are `768..771`, en-passant file keys are `772..779` and are XORed only if side-to-move has a pawn that can capture en passant, side-to-move key is `780` for white.
- **Alpha-beta uses sequential PV search by default:** parallel split was disabled because fixed-window futures removed pruning effectiveness; correctness and pruning quality take priority over speculative parallelism.
- **Search hash is updated incrementally per move:** bot search now updates Zobrist keys from parent hash with move deltas instead of recomputing piece scans at every node.
- **Immutable state as primary model:** GameContext (api) holds board, history, player stateimmutable throughout. Each move new GameContext. Enables undo/redo without side effects.
- **Observer pattern for UI decoupling:** GameEngine publishes move/state events; CommandInvoker queues moves; UI listens (no polling). GameEngine never imports UI.
- **RuleSet trait encapsulates rules:** Move generation, check, castling, en passant all in RuleSet impl. GameEngine calls rules as black box; rules don't know rest of core.
- **Polyglot hash must follow spec index layout:** Piece keys use interleaved mapping `(pieceType * 2 + colorBit)` (black=0, white=1). Castling keys: `768..771`. En-passant file keys: `772..779`, XORed only if side-to-move has capturable en passant. Side-to-move key: `780` (white).
- **Alpha-beta uses sequential PV search by default:** Parallel split disabled (fixed-window futures removed pruning effectiveness). Sequential PV default. Correctness + pruning quality > speculative parallelism.
- **Search hash is updated incrementally per move:** Bot search updates Zobrist keys from parent hash with move deltas, not recomputing piece scans per node.
## Rules
- **Tests are the spec.** Never modify tests to pass; modify requirements or code. Update tests only if requirements change.
- **Tests are the spec.** Don't modify to pass. Fix requirements/code. Update only if requirements change.
- Never read build folders. Ask permission if needed.
- Keep this file up to date with any important decisions or conventions.
- Keep file current with decisions + conventions.
---
@@ -64,11 +64,9 @@ Try to stick to these commands for consistency.
### Two-Step Rule (mandatory)
**Step 1 — Orient:** Use wiki articles to find WHERE things live.
**Step 2 — Verify:** Read the actual source files listed in the wiki article BEFORE writing any code.
**Step 2 — Verify:** Read source files from wiki BEFORE coding.
Wiki articles are structural summaries extracted by AST. They show routes, models, and file locations.
They do NOT show full function logic, middleware internals, or dynamic runtime behavior.
**Never write or modify code based solely on wiki content — always read source files first.**
Wiki = structural summaries (routes, models, file locations). No function logic, middleware internals, runtime behavior. Don't code from wiki alone—read sources.
Read in order at session start:
1. `.codesight/wiki/index.md` — orientation map (~200 tokens)
@@ -76,8 +74,7 @@ Read in order at session start:
3. Domain article (e.g. `.codesight/wiki/auth.md`) → check "Source Files" section → read those files
4. `.codesight/CODESIGHT.md` — full context map for deep exploration
Routes marked `[inferred]` in wiki articles were detected via regex — verify against source before trusting.
If any source file shows ⚠ in the wiki, re-run `codesight --wiki` before proceeding.
`[inferred]` routes = regex-detected. Verify sources. ⚠ in wiki? Re-run `codesight --wiki`.
Or use the codesight MCP server for on-demand queries:
- `codesight_get_wiki_article` — read a specific wiki article by name
@@ -87,13 +84,13 @@ Or use the codesight MCP server for on-demand queries:
- `codesight_get_blast_radius --file src/lib/db.ts` — impact analysis before changes
- `codesight_get_schema --model users` — specific model details
Only open specific files after consulting codesight context. This saves ~16.893 tokens per conversation.
Consult codesight context first. Saves ~16.893 tokens/conversation.
## graphify
This project has a graphify knowledge graph at graphify-out/.
graphify knowledge graph at graphify-out/.
Rules:
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
- Architecture/codebase questions? Read graphify-out/GRAPH_REPORT.md (god nodes, communities).
- graphify-out/wiki/index.md exists? Use it (not raw files).
- Code modified? Run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to sync graph.
+99
View File
@@ -0,0 +1,99 @@
# Now-Chess
Scala 3.5.1 · Gradle 9
## Commands
```
./clean # Clear build dirs — only when necessary
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Try to stick to these commands for consistency.
## Modules
| Module | Role | Depends on |
|--------|------|-----------|
| `api` | Model / shared types | (none) |
| `core` | Primary business logic | api, rule |
| `rule` | Game rules | api |
| `bot` | Bots and AI | api,rule,io |
| `io` | Export formats | api, core |
## Style
- Use immutable data and pure functions.
- Keep functions under 30 lines. If you need "and" to describe it, split it.
- Keep cyclomatic complexity under 15.
- Avoid comments. Let names carry intent; comment only non-obvious algorithms.
- Scan for duplicated logic before finishing. Extract it.
- Follow default Sonar style for Scala.
- Use `Option` or `Either` for fallible operations; avoid exceptions for control flow.
- Naming: types are PascalCase, functions/values are camelCase.
## Code Quality
- **Coverage:** 100% condition coverage required in `api`, `core`, `rule`, `io` (mandatory); `ui` exempt.
### Linters
- **scalafmt** — enforces formatting; run `./gradlew spotlessScalaCheck` to check and `./gradlew spotlessScalaApply` to refactor.
- **scalafix** — enforces style and detects unused imports/code; run `./gradlew scalafix` to apply rules.
## Architecture Decisions
- **Immutable state as primary model:** GameContext (api) holds board, history, player state — immutable, passed through the system. Each move creates a new GameContext, enabling undo/redo without side effects.
- **Observer pattern for UI decoupling:** GameEngine publishes move/state events; CommandInvoker queues moves; UI listens to events, not polling. GameEngine never imports UI code.
- **RuleSet trait encapsulates rules:** Move generation, check, castling, en passant all in RuleSet impl. GameEngine calls rules as a black box; rules don't know about the rest of core.
- **Polyglot hash must follow spec index layout:** piece keys use interleaved mapping `(pieceType * 2 + colorBit)` with black=0/white=1, castling keys are `768..771`, en-passant file keys are `772..779` and are XORed only if side-to-move has a pawn that can capture en passant, side-to-move key is `780` for white.
- **Alpha-beta uses sequential PV search by default:** parallel split was disabled because fixed-window futures removed pruning effectiveness; correctness and pruning quality take priority over speculative parallelism.
- **Search hash is updated incrementally per move:** bot search now updates Zobrist keys from parent hash with move deltas instead of recomputing piece scans at every node.
## Rules
- **Tests are the spec.** Never modify tests to pass; modify requirements or code. Update tests only if requirements change.
- Never read build folders. Ask permission if needed.
- Keep this file up to date with any important decisions or conventions.
---
## Instructions for Claude Code
### Two-Step Rule (mandatory)
**Step 1 — Orient:** Use wiki articles to find WHERE things live.
**Step 2 — Verify:** Read the actual source files listed in the wiki article BEFORE writing any code.
Wiki articles are structural summaries extracted by AST. They show routes, models, and file locations.
They do NOT show full function logic, middleware internals, or dynamic runtime behavior.
**Never write or modify code based solely on wiki content — always read source files first.**
Read in order at session start:
1. `.codesight/wiki/index.md` — orientation map (~200 tokens)
2. `.codesight/wiki/overview.md` — architecture overview (~500 tokens)
3. Domain article (e.g. `.codesight/wiki/auth.md`) → check "Source Files" section → read those files
4. `.codesight/CODESIGHT.md` — full context map for deep exploration
Routes marked `[inferred]` in wiki articles were detected via regex — verify against source before trusting.
If any source file shows ⚠ in the wiki, re-run `codesight --wiki` before proceeding.
Or use the codesight MCP server for on-demand queries:
- `codesight_get_wiki_article` — read a specific wiki article by name
- `codesight_get_wiki_index` — get the wiki index
- `codesight_get_summary` — quick project overview
- `codesight_get_routes --prefix /api/users` — filtered routes
- `codesight_get_blast_radius --file src/lib/db.ts` — impact analysis before changes
- `codesight_get_schema --model users` — specific model details
Only open specific files after consulting codesight context. This saves ~16.893 tokens per conversation.
## graphify
This project has a graphify knowledge graph at graphify-out/.
Rules:
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
@@ -0,0 +1,334 @@
# 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
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Offer Draw
type: http
seq: 1
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/offer
body: none
auth: none
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Accept Draw
type: http
seq: 2
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/accept
body: none
auth: none
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Decline Draw
type: http
seq: 3
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/decline
body: none
auth: none
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Claim Draw
type: http
seq: 4
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/claim
body: none
auth: none
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: draw
seq: 2
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Export FEN
type: http
seq: 1
}
http {
method: GET
url: {{baseUrl}}/api/board/game/{{gameId}}/export/fen
body: none
auth: none
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Export PGN
type: http
seq: 2
}
http {
method: GET
url: {{baseUrl}}/api/board/game/{{gameId}}/export/pgn
body: none
auth: none
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: export
seq: 6
}
+27
View File
@@ -0,0 +1,27 @@
meta {
name: Create Game
type: http
seq: 1
}
http {
method: POST
url: {{baseUrl}}/api/board/game
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"white": {"id": "p1", "displayName": "Alice"},
"black": {"id": "p2", "displayName": "Bob"},
"timeControl": {
"limitSeconds": 300,
"incrementSeconds": 3
}
}
}
+15
View File
@@ -0,0 +1,15 @@
meta {
name: Get Game
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}
body: none
auth: none
}
vars:pre-request {
gameId: j0nPtcjl
}
+34
View File
@@ -0,0 +1,34 @@
meta {
name: Stream Game
type: ws
seq: 3
}
ws {
url: {{wsBaseUrl}}/api/board/game/{{gameId}}/ws
body: ws
auth: none
}
body:ws {
name: move
content: '''
{
"type": "MOVE",
"uci": "b1c3"
}
'''
}
body:ws {
name: ping
content: '''
{
"type": "PING"
}
'''
}
vars:pre-request {
gameId: j0nPtcjl
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Resign
type: http
seq: 4
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/resign
body: none
auth: none
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: game
seq: 3
}
+28
View File
@@ -0,0 +1,28 @@
meta {
name: Import FEN
type: http
seq: 1
}
http {
method: POST
url: {{baseUrl}}/api/board/game/import/fen
body: json
auth: none
}
headers {
Content-Type: application/json
}
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
}
}
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: Import PGN
type: http
seq: 2
}
http {
method: POST
url: {{baseUrl}}/api/board/game/import/pgn
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
}
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: import
seq: 5
}
+15
View File
@@ -0,0 +1,15 @@
meta {
name: Make Move
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/move/b1c3
body: none
auth: none
}
vars:pre-request {
gameId: Yg200tOF
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: Get Legal Moves
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/moves
body: none
auth: none
}
params:query {
square: e2
}
vars:pre-request {
gameId: COy3oigz
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Undo Move
type: http
seq: 3
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/undo
body: none
auth: none
}
+12
View File
@@ -0,0 +1,12 @@
meta {
name: Redo Move
type: http
seq: 4
}
http {
method: POST
url: {{baseUrl}}/api/board/game/{{gameId}}/redo
body: none
auth: none
}
+3
View File
@@ -0,0 +1,3 @@
meta {
name: move
}
+6
View File
@@ -0,0 +1,6 @@
{
"version": "1",
"name": "NowChess API",
"type": "collection",
"ignore": []
}
View File
+5
View File
@@ -0,0 +1,5 @@
vars {
baseUrl: http://localhost:8080
wsBaseUrl: ws://localhost:8084
ioBaseUrl: http://localhost:8081
}
+100
View File
@@ -0,0 +1,100 @@
meta {
name: Export FEN
type: http
seq: 1
}
http {
method: POST
url: {{ioBaseUrl}}/io/export/fen
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"board": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
},
"turn": "White",
"castlingRights": {
"whiteKingSide": true,
"whiteQueenSide": true,
"blackKingSide": true,
"blackQueenSide": true
},
"enPassantSquare": null,
"halfMoveClock": 0,
"moves": [],
"result": null,
"initialBoard": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
meta {
name: Export PGN
type: http
seq: 2
}
http {
method: POST
url: {{ioBaseUrl}}/io/export/pgn
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"board": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
},
"turn": "White",
"castlingRights": {
"whiteKingSide": true,
"whiteQueenSide": true,
"blackKingSide": true,
"blackQueenSide": true
},
"enPassantSquare": null,
"halfMoveClock": 0,
"moves": [],
"result": null,
"initialBoard": {
"a1": {"color": "White", "pieceType": "Rook"},
"b1": {"color": "White", "pieceType": "Knight"},
"c1": {"color": "White", "pieceType": "Bishop"},
"d1": {"color": "White", "pieceType": "Queen"},
"e1": {"color": "White", "pieceType": "King"},
"f1": {"color": "White", "pieceType": "Bishop"},
"g1": {"color": "White", "pieceType": "Knight"},
"h1": {"color": "White", "pieceType": "Rook"},
"a2": {"color": "White", "pieceType": "Pawn"},
"b2": {"color": "White", "pieceType": "Pawn"},
"c2": {"color": "White", "pieceType": "Pawn"},
"d2": {"color": "White", "pieceType": "Pawn"},
"e2": {"color": "White", "pieceType": "Pawn"},
"f2": {"color": "White", "pieceType": "Pawn"},
"g2": {"color": "White", "pieceType": "Pawn"},
"h2": {"color": "White", "pieceType": "Pawn"},
"a7": {"color": "Black", "pieceType": "Pawn"},
"b7": {"color": "Black", "pieceType": "Pawn"},
"c7": {"color": "Black", "pieceType": "Pawn"},
"d7": {"color": "Black", "pieceType": "Pawn"},
"e7": {"color": "Black", "pieceType": "Pawn"},
"f7": {"color": "Black", "pieceType": "Pawn"},
"g7": {"color": "Black", "pieceType": "Pawn"},
"h7": {"color": "Black", "pieceType": "Pawn"},
"a8": {"color": "Black", "pieceType": "Rook"},
"b8": {"color": "Black", "pieceType": "Knight"},
"c8": {"color": "Black", "pieceType": "Bishop"},
"d8": {"color": "Black", "pieceType": "Queen"},
"e8": {"color": "Black", "pieceType": "King"},
"f8": {"color": "Black", "pieceType": "Bishop"},
"g8": {"color": "Black", "pieceType": "Knight"},
"h8": {"color": "Black", "pieceType": "Rook"}
}
}
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: export
seq: 2
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: Import FEN
type: http
seq: 1
}
http {
method: POST
url: {{ioBaseUrl}}/io/import/fen
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
}
}
+22
View File
@@ -0,0 +1,22 @@
meta {
name: Import PGN
type: http
seq: 2
}
http {
method: POST
url: {{ioBaseUrl}}/io/import/pgn
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
}
}
+4
View File
@@ -0,0 +1,4 @@
meta {
name: import
seq: 1
}
+61 -21
View File
@@ -8,6 +8,65 @@ plugins {
group = "de.nowchess"
version = "1.0-SNAPSHOT"
// Canonical coverage exclusions — glob patterns consumed by Sonar directly;
// 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/**",
// 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
"**/bot/**/NNUE.scala",
"**/bot/**/NNUEBot.scala",
"**/bot/**/EvaluationNNUE.scala",
// NBAI binary format loader/writer — error paths require crafted corrupt files; migrator is a one-shot tool
"**/bot/**/NbaiLoader.scala",
"**/bot/**/NbaiModel.scala",
"**/bot/**/NbaiMigrator.scala",
"**/bot/**/NbaiWriter.scala",
// PolyglotBook — binary I/O and dead-code guards (bit-masked fields can never exceed valid range)
"**/bot/**/PolyglotBook.scala",
"**/bot/**/MoveOrdering.scala",
"**/bot/**/AlphaBetaSearch.scala",
// DTO case class synthetic methods (Scala compiler-generated apply/$default params)
"**/api/src/main/scala/de/nowchess/api/dto/**Dto.scala",
// Core infrastructure: exception classes, config, registry implementation, game entry
"**/core/src/main/scala/de/nowchess/chess/exception/**",
"**/core/src/main/scala/de/nowchess/chess/config/**",
"**/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/**",
)
// Converts a Sonar-style glob to a scoverage regex (matched against full source path).
// Order matters: protect ** before converting lone *, escape dots last.
fun globToScoverageRegex(glob: String): String =
glob
.replace("**", "^@")
.replace("*", "[^/]*")
.replace(".", "\\.")
.replace("^@", ".*")
.let { ".*$it" }
extra["SCOVERAGE_EXCLUDED"] = coverageExclusions.map(::globToScoverageRegex)
sonar {
properties {
property("sonar.projectKey", "Now-Chess-Systems")
@@ -22,33 +81,14 @@ sonar {
}.joinToString(",")
property("sonar.scala.coverage.reportPaths", scoverageReports)
property(
"sonar.coverage.exclusions",
// UI renders JavaFX components; headless test environments cannot exercise rendering paths
"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
"**/bot/**/NNUE.scala," +
"**/bot/**/NNUEBot.scala," +
"**/bot/**/EvaluationNNUE.scala," +
// NBAI binary format loader/writer — error paths require crafted corrupt files; migrator is a one-shot tool
"**/bot/**/NbaiLoader.scala," +
"**/bot/**/NbaiModel.scala," +
"**/bot/**/NbaiMigrator.scala," +
"**/bot/**/NbaiWriter.scala," +
// PolyglotBook — binary I/O and dead-code guards (bit-masked fields can never exceed valid range)
"**/bot/**/PolyglotBook.scala," +
"**/bot/**/MoveOrdering.scala," +
"**/bot/**/AlphaBetaSearch.scala"
)
property("sonar.coverage.exclusions", coverageExclusions.joinToString(","))
}
}
val versions = mapOf(
"QUARKUS_SCALA3" to "1.0.0",
"SCALA3" to "3.5.1",
"SCALA_LIBRARY" to "2.13.18",
"SCALA_LIBRARY" to "2.13.16",
"SCALATEST" to "3.2.19",
"SCALATEST_JUNIT" to "0.1.11",
"SCOVERAGE" to "2.1.1",
-776
View File
@@ -1,776 +0,0 @@
openapi: 3.0.3
info:
title: NowChess 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.
If the move results in a pawn reaching the back rank and no promotion
character is supplied, the game enters `promotionPending` status and
the move is not yet applied — resubmit with the promotion character.
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 |
| `promotionPending` | A pawn reached the back rank; awaiting promotion piece selection |
| `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
enum:
- started
- check
- checkmate
- stalemate
- resign
- draw
- drawOffered
- fiftyMoveAvailable
- promotionPending
- 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
+6
View File
@@ -0,0 +1,6 @@
# Gradle properties
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.32.4
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.32.4
+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.0.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
View File
Executable
+3
View File
@@ -0,0 +1,3 @@
#! /usr/bin/env bash
./gradlew scalafix spotlessCheck
+117
View File
@@ -0,0 +1,117 @@
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
}
@@ -0,0 +1,27 @@
{
"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[]" }
]
}
@@ -0,0 +1,64 @@
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:
always-include: true
path: /swagger-ui
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
nowchess:
redis:
host: localhost
port: 6379
prefix: nowchess
internal:
secret: 123abc
"%deployed":
quarkus:
rest-client:
core-service:
url: ${CORE_SERVICE_URL}
nowchess:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
prefix: ${REDIS_PREFIX:nowchess}
datasource:
db-kind: postgresql
username: ${DB_USER}
password: ${DB_PASSWORD}
jdbc:
url: ${DB_URL}
hibernate-orm:
schema-management:
strategy: update
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}
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsnsCAl0vQx7Vu9CLDZ
g0SG05NgUzu9T+3DTEaHGq60T2uriO8BenwyvsF3BnDqTbKf4voohZ1DNfzdbT1J
Fj8B62FrDmxcO+sp1/b5HUCJP6y2uSRCmzOHe5k7Pk1IEi72FgBpKXSRkFibRlVf
634g7mgsPZAQ9PJEsv4Qvm05T9L6+Gmq6N3bMVLKRXs4RhDhaFbYH9GtUg1eI0yH
YjGyRfqzW/nqVMstOLHt8CuPouq4p7eMzeDH3YHkxPm4GG5foCXMOd2DZrW0SCcr
7dhFeNVWzQ2m53eOhBzNQX+v3pgjVStsePhBRt2LyGfwkNzmqDgqWsMzSHRMY+cn
WQIDAQAB
-----END PUBLIC KEY-----
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,17 @@
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
})
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,56 @@
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())
@@ -0,0 +1,4 @@
package de.nowchess.account.domain
enum ChallengeColor:
case White, Black, Random
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,4 @@
package de.nowchess.account.domain
enum ChallengeStatus:
case Created, Canceled, Declined, Accepted
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,4 @@
package de.nowchess.account.domain
enum DeclineReason:
case Generic, Later, TooFast, TooSlow, TimeControl, Rated, Casual, Standard, Variant, NoBot, OnlyBot
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,7 @@
package de.nowchess.account.domain
sealed trait TimeControl
object TimeControl:
case class Clock(limit: Int, increment: Int) extends TimeControl
case object Unlimited extends TimeControl
@@ -0,0 +1,78 @@
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
@@ -0,0 +1,49 @@
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)
@@ -0,0 +1,23 @@
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"
@@ -0,0 +1,25 @@
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"
@@ -0,0 +1,43 @@
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"))
@@ -0,0 +1,98 @@
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)
@@ -0,0 +1,62 @@
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)
@@ -0,0 +1,202 @@
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,
)
@@ -0,0 +1,88 @@
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()
@@ -0,0 +1,91 @@
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()
@@ -0,0 +1,165 @@
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)
@@ -0,0 +1,197 @@
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,
)
@@ -0,0 +1,31 @@
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)
()
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,28 @@
-----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-----
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMwR4ESzHs9m+sH3b70y
VUfMcW6/5CVDIquKLdeCobU2hopBVvu+3s46KmmxVh+wjYw7wElgjNp+1z8S+H4V
MM7024UWdlzTPNJSgfjiIz94aDS74HkjR20uAs7Tn5NiVbUBS0HWCf4LwaBT4AbW
WDv0mVTC9vHeVPdia1kSAtXtR7daCsCiaDqrUya7P0bjpSkP3buJMGPJtIz8NOcB
QNWETFAu/MSpJQlWArn2HsCFE3SsQEytGSlgaDwiZ8Q4PIMIKZ1dcB0KxxnG2b4X
FYNxlqQ3GHZ6IFeK5bLMLOe/rgIRkckAZJpoVW/s0/i0+1lwzweOjJx5ChSGWsRJ
BwIDAQAB
-----END PUBLIC KEY-----
@@ -0,0 +1,107 @@
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)
@@ -0,0 +1,179 @@
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))
+58
View File
@@ -51,3 +51,61 @@
* 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-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-19)
### 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-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-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))
+4 -1
View File
@@ -8,6 +8,8 @@ version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
@Suppress("UNCHECKED_CAST")
val scoverageExcluded = rootProject.extra["SCOVERAGE_EXCLUDED"] as List<String>
repositories {
mavenCentral()
@@ -19,6 +21,7 @@ scala {
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
excludedFiles.set(scoverageExcluded)
}
configurations.scoverage {
@@ -31,7 +34,7 @@ configurations.scoverage {
dependencies {
implementation("org.scala-lang:scala3-compiler_3") {
compileOnly("org.scala-lang:scala3-compiler_3") {
version {
strictly(versions["SCALA3"]!!)
}
@@ -1,11 +0,0 @@
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]
}
@@ -0,0 +1,3 @@
package de.nowchess.api.dto
final case class ApiErrorDto(code: String, message: String, field: Option[String])
@@ -0,0 +1,9 @@
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,
)
@@ -0,0 +1,10 @@
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,
)
@@ -0,0 +1,6 @@
package de.nowchess.api.dto
final case class ErrorEventDto(`type`: String, error: ApiErrorDto)
object ErrorEventDto:
def apply(error: ApiErrorDto): ErrorEventDto = ErrorEventDto("error", error)
@@ -0,0 +1,8 @@
package de.nowchess.api.dto
final case class GameFullDto(
gameId: String,
white: PlayerInfoDto,
black: PlayerInfoDto,
state: GameStateDto,
)

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