Repository navigation
19.0.0
The essential shape: coherent API, domain-owned behavior, declared scope
A surface-only release. It retires the HalfMove concept, replaces the "halfmove" / "ply" vocabulary with "move" across the public model and reports, and applies a "data carriers, not behavior" pass that moves computed, translation, and factory logic off records and enums into dedicated utility / translator classes. No rule, parser, or analysis behavior changes — every verdict and every report is identical to 18.1.0; what changes is the shape of the API. It is heavily binary-incompatible, so the breaking list is long, but most migrations are a single type rename or a Type.method(x) → TypeUtility.method(x) call-site change.
Notable
- Records and enums are now data carriers. A new rule in
coding-conventions.mddraws the line: records and enums hold data; computational, translation, and factory logic lives in dedicated*Utility/*Translatorclasses.BitboardPositionis the one documented exception — it carries its move-generation / king-safety engine on the record for hot-path allocation reasons. - A last-chance API-polish pass was folded in before publication, so no further major bump is needed for it. It refined the data-carrier rule: type-creating factories (
of(...)from typed components,parse(...)from text), same-type conversions (to*), and tiny intrinsic self-description predicates live on the enum or record they describe, while rule evaluation stays in utilities — this returned theFen*SymbolandPromotionPieceTypefactories to their enums and addedMoveSpecification.isPromotion(). It also dropped thecalculateIs/calculateHasprefix from boolean predicates (is/has), switched PGN export to theto*idiom, surfaced immutable collection types in signatures, and gave whole-position dead detection its ownDeadPositionAnalyzer. See the Breaking list. HalfMoveis retired. The played-move row is nowMoveRecord, rebuilt inside the report layer fromBoard's existing per-move accessors; the public PGN movetext type isPgnMove.- "Move" is the consistent term for one player's action across the public model, move history, and reports. "halfmove" and "ply" now appear only where they are protocol- or engine-correct: FEN/PGN counters (
halfMoveClock, thePlyCounttag) and the engine/search internals. - Core board enums print conventional chess notation.
Square.toString()now returnsa1..h8(not theA1constant name);File→a..h;Rank→1..8;Piece→ the FEN letter (P/p, ...);PieceType→P/N/B/R/Q/K(andnonefor theNONEsentinels). Logging and diagnostics now read like a chess board. The explicit accessors (getName(),getLetter(),getNumber()) are unchanged. - Added a Maven Central version badge to the README.
Behavioral
- No rule, report, or adjudication behavior changes: move legality, FEN/SAN/PGN export, unwinnability and adjudication verdicts, and all report output are identical to 18.1.0. Parsing is identical except for the deliberate changes below. The one cosmetic change is the conventional
toString()on the coreboard.enumstypes (see Notable). - The lenient PGN parser now accepts
;rest-of-line comments (PGN import format, spec section 5), attaching them as commentary exactly like brace comments; previously the;leaked into a SAN token and the parse failed. The strict PGN parser still rejects them — export format does not include;comments. - The lenient PGN parser now reports a malformed game-termination marker — any digits/hyphen/slash run that is not one of
1-0/0-1/1/2-1/2/*(e.g.1-2,7/) — as aLenientPgnParserValidationExceptionwith the newLenientPgnParserValidationProblem.MOVETEXT_TERMINATION_MARKER_INVALID; previously it leaked a rawIllegalArgumentExceptionout ofResultTagValue.parse. The strict parser already guarded this. (Found by the parser fuzz harness.) NonePointerExceptionis now aUsageExceptionrather than aProgrammingMistakeException, and the FEN/PGNvalidate(...)methods now let aProgrammingMistakeExceptionpropagate instead of masking it asUNKNOWN_ERROR. See Breaking → Exceptions.
Breaking
PGN model:
PgnHalfMoverenamed toPgnMove.PgnGame.halfMoveList()renamed toPgnGame.moves(), andPgnGame.tagList()renamed toPgnGame.tags()(record components and accessors) — domain plurals over the implementation-type*Listsuffix.
Board:
getPerformedHalfMoveCount()renamed togetPerformedMoveCount().- Removed
Board(String fen). FEN strings now use the explicit factory pair:Board.fromFenStrict(String)for strict FEN andBoard.fromFenLenient(String)for recovered/tolerant FEN.Board(Fen)remains for already-parsed FEN values. This makes FEN construction match the library's strict/lenient parser vocabulary instead of hiding strict parsing behind aStringconstructor. - Removed
getHalfMoveList()andgetLastHalfMove(); the played-move list is no longer exposed onBoard. It is rebuilt inside thereportlayer, andPgnCreate.createPgnGame(board).moves()returns the played moves asPgnMoves. HalfMoveis gone from the public API (its replacement,MoveRecord, is package-private toreport).isUnwinnableQuick(Side)/isUnwinnableFull(Side)renamed tounwinnableQuick(Side)/unwinnableFull(Side), matching theUnwinnableQuickAnalyzer.unwinnableQuick(...)/UnwinnableFullAnalyzer.unwinnableFull(...)engines they delegate to. TheBoardmethods return the verdict directly; the analyzers return the full analysis.move(MoveSpecification),movesStrict(String...), andmovesLenient(String...)now returnvoidinstead ofboolean. The discarded return was alwaystrue(failure is signalled by exception), so it carried no information.calculateFiftyMoveRuleClaimRights()/calculateThreefoldRepetitionRuleClaimRights()renamed tofiftyMoveRuleClaimRights()/threefoldRepetitionRuleClaimRights()— dropping the implementation-flavouredcalculateprefix in favour of domain nouns, matching theunwinnableQuick/deadPositionQuickaccessors.- Removed
calculateInsufficientMaterial()and the four-waycommon.enums.InsufficientMaterialenum (NONE/WHITE_ONLY/BLACK_ONLY/BOTH). It was fully derivable from — and read confusingly beside — the side-specific booleans; useisInsufficientMaterial()andisInsufficientMaterial(Side). - Collection accessors use domain plurals instead of the implementation-type
*Listsuffix, and the notation variants gainAs:getPossibleMoveSpecificationList()→getLegalMoveSpecifications(),getPerformedMoveSpecificationList()→getPerformedMoveSpecifications(),getPerformedLegalMoveList()→getPerformedMoves(),getLegalMovesSan()→getLegalMovesAsSan(),getLegalMovesUci()→getLegalMovesAsUci(), andgetSanList()→getPerformedMovesAsSan()(the performed-move SAN history, mirroringgetLegalMovesAsSan()).getLegalMoves()is already a domain plural and is unchanged, as are the single-movegetSan()/getLan().
Collection / *List naming (library-wide final pass):
SquareUtility.getPromotionRankList(Side)→getPromotionRankSquares(Side);SquareUtility.calculateEnPassantCaptureTargetSquareList(Side)→getEnPassantCaptureTargetSquares(Side).UciMoveValidationUtility.getUciMoveList()→getUciMoves().MoveSuffixAnnotation.calculateValueList(),ResultTagValue.calculateList(), andSetUpTagValue.calculateList()→allowedValuesText()— each returns the allowed-values text, not a list.BitboardPositionUtility.toSquareSet(long)→toSquares(long)(returnsImmutableSet<Square>— the return type carries the set-ness, the name carries the domain).MobilitySolution.getPiecePlacementSet()→getPiecePlacements().- Public collection constants also use domain names:
ForgivenFenItem.EMPTY_LIST,ForgivenSanItem.EMPTY_LIST, andForgivenTagItem.EMPTY_LIST→NO_ITEMS;TagUtility.SEVEN_TAG_ROSTER_TAG_LIST→SEVEN_TAG_ROSTER_TAGS. ListUtility.calculateCommaSeparatedList/calculateSpaceSeparatedList/calculateLineSeparatedList→toCommaSeparatedString/toSpaceSeparatedString/toLineSeparatedString;ListUtility.calculateSquareList(Set<Square>)→formatSquares(...)— these return aString, so theListsuffix was misleading.- Removed
ImmutableUtility.constructListSquare(Square...)— a thinImmutableListadapter redundant withNulls.listOf(...); call sites useNulls.listOf(...).
Kept deliberately: Nulls' Java-collection adapters (copyOfList, asList, subList, listOf, …), PgnCreate.toPgnLines, and the PGN parsers' parseLines(...) — there "list" / "lines" is the genuine Java-adapter or domain concept, not an implementation-type leak.
Reporter:
Reporter.printReport(...)renamed toReporter.report(...)and now returns the report asImmutableList<String>instead of writing toSystem.out; the caller decides where the lines go (e.g.Reporter.report(board).forEach(System.out::println)). Applies to all three overloads (Board, PGNString, PGN file). A library should not own the output stream.
Vocabulary — "side to move":
- The non-standard
havingMovevocabulary is replaced by the standard chess term. A position's side to move issideToMove; the side that makes a move ismovingSide(besideLegalMove.movingPiece()). Renames:Board.getHavingMove()→getSideToMove(); theFen/DynamicPositionhavingMove()component →sideToMove();LegalMove.havingMove()→movingSide();StrictFenSemanticValidationProblem.INVALID_HAVING_MOVE_RANGE→INVALID_SIDE_TO_MOVE_RANGE.
Boolean accessors (JavaBeans is / has idiom, no get prefix):
Side.getIsWhite()/getIsBlack()→isWhite()/isBlack().CastlingRight.getHasKingSide()/getHasQueenSide()→hasKingSide()/hasQueenSide().File.getIsBorderFile()→isBorderFile().StandardTag.getIsSevenTagRosterTag()→isSevenTagRosterTag().
Formatting / model:
HalfMoveUtilityrenamed toMoveNumberFormat.DynamicPosition.isEnPassantCapturePossible()removed — testenPassantCaptureTargetSquare() != Square.NONEdirectly.LegalMove.pieceCaptured()renamed tocapturedPiece()(record component and accessor) — a "captured piece" noun, reading besidemovingPiece.UciMove.text()renamed touci()(record component and accessor) — the generictextbecomes the domain term.
Model invariants:
UnwinnabilityFullAnalysis.mateLine()now returnsImmutableList<UciMove>instead ofList<UciMove>, and the record copies the list defensively on construction — the component is now guaranteed immutable.ClaimRights'sclaimableMovescomponent is nowImmutableList<ClaimableMove>(wasList<ClaimableMove>); the canonical constructor requires an already-immutable list rather than defensively copying an arbitraryList.PgnCreate.toPgnLines(...)(both overloads) now returnsImmutableList<String>instead ofList<String>— the last return-side mutable-collection leak, now aligned with the rest of the API.MoveSpecification's canonical constructor now validates its structural invariants on every path (previously the public four-argument constructor bypassed validation). Inconsistent from / to / castling / promotion combinations now throwIllegalArgumentExceptionat construction instead of yielding a malformed value.
FEN:
- Added
StrictFenParser.parse(String)andStrictFenParser.validate(String)as the public strict FEN parser facade, symmetrical withLenientFenParser.parse(String)/validate(String). FenParserRawandFenParserAdvancedare no longer public API. Their lexical and advanced-validation stages remain internal implementation details behindStrictFenParser; callers should useStrictFenParser.parse(String)/validate(String)orBoard.fromFenStrict(String).FenParserRawrenamed internally toStrictFenFieldParser;FenParserAdvancedrenamed internally toStrictFenSemanticParser, making the strict-parser staging explicit.FenRawrenamed toFenField;FenRawValidationExceptionrenamed to the package-privatefen.StrictFenFieldValidationException, matching the internal strict FEN field parser vocabulary.FenAdvancedValidationProblemrenamed toStrictFenSemanticValidationProblem;FenAdvancedValidationExceptionrenamed tofen.StrictFenSemanticValidationException;LenientFenParserValidationProblem.ADVANCED_INVALIDrenamed toSTRICT_SEMANTIC_INVALID.ForgivenFenItemCode.MISSING_HALFMOVE_AND_FULLMOVE→MISSING_HALF_MOVE_CLOCK_AND_FULL_MOVE_NUMBER;MISSING_FULLMOVE_NUMBER→MISSING_FULL_MOVE_NUMBER.StrictFenSemanticValidationProblem.INVALID_FULL_MOVE_NUMBER_TOO_BIG_ABSOLUT→INVALID_FULL_MOVE_NUMBER_TOO_BIG_ABSOLUTE(typo fix in a public enum constant).FenConstants.POSSIBLE_FEN_AFTER_FIRST_HALF_MOVErenamed toPOSSIBLE_FEN_AFTER_FIRST_MOVE.
Parser entry points — plain parse / validate when a parser has one input kind; source suffixes only when it has several:
- SAN (text only):
LenientSanParser.parseText(String, Board)/StrictSanParser.parseText(String, Board)→parse(...). RemovedLenientSanParser.validateText(String, Board)— avoid-returning convenience with no strict counterpart;parse(...)validates by construction. - SAN
parsenow returns its model directly, matching FEN/PGN.StrictSanParser.parse(String, Board)andBoard.moveStrict(String)return the resolvedMoveSpecification— the one-fieldStrictSanParserValidationResultwrapper is removed. The lenient side keeps a result type because it carries more than the move:LenientSanParserValidationResult→LenientSanParseResult(the resolved move plus the forgiven items), returned byLenientSanParser.parse(...)andBoard.moveLenient(...). The...ParserValidationResultsuffix stays reserved for whatvalidate()returns (cf. the FEN/PGN validate results). - FEN (text only):
LenientFenParser.parseText(String)→parse(String);LenientFenParser.validateText(String)→validate(String). Strict FEN now also hasStrictFenParser.parse(String)/validate(String). - PGN (text, file path, line list) — source kind matters, so suffixes stay: the
parse(Path)/parse(Path, String)/parse(String)path overloads →parsePath(...);parse(List<String>)→parseLines(...); thevalidate(Path)/validate(Path, String)/validate(String)overloads →validatePath(...).parseText(String)andvalidateText(String)are unchanged. This removes theparse(String pgnPath)-beside-parseText(String pgn)footgun (same parameter type, opposite meaning). Applies to bothStrictPgnParserandLenientPgnParser.
Enum behavior moved to utilities (the "enums carry data" pass) — each former Enum.method(...) is now EnumUtility.method(enum, ...):
KingSafetyCheck/MovementCheck/CastlingChecktoMoveCheck(...)→analyze.{KingSafetyCheck,MovementCheck,CastlingCheck}Translator.toMoveCheck(...).Piece.calculate{Rook,Knight,Bishop,Queen,King,Pawn}Piece(Side)andPiece.calculate(Side, PieceType), plus the duplicatePieceType.calculate(Side, PieceType), consolidated intoboard.enums.PieceUtility.PromotionPieceType.calculate(Side, PromotionPieceType)→ the instance methodPromotionPieceType.toPiece(Side).Square.flip(nowSquareUtility.rotate180— the operation is a 180-degree rotation / point reflection through the centre, not a single-axis flip),getPromotionRank(nowgetPromotionRankSquares),calculateJumpOverSquare, the en-passant-target-squares method (nowgetEnPassantCaptureTargetSquares), and the king / queen-side-rook / king-side-rook original-square methods →board.enums.SquareUtility.Rank's side-relative rule methods (ground / promotion / pawn-initial / two-square-advance / en-passant ranks and per-side validity) →board.enums.RankUtility.FenSideSymbol.calculate(Side)→FenSideSymbol.of(Side)andFenPieceSymbol.calculate(Piece)→FenPieceSymbol.of(Piece)(the type-creating factories stay on the enums; the char self-parse isparse(char)— see below).SanTerminalMarker.calculate(...)/append(...)→san.SanTerminalMarkerUtility;SanFormat.isCapture()→san.SanFormatUtility.isCapture(...).
Enum factories split by input — of(...) from typed components, parse(...) from text:
Square.calculate(String)→parse(String);Square.calculate(File, Rank)andSquare.calculate(int, int)→of(...).Rank.calculateRank(char)→parse(char);Rank.calculateRank(int)→of(int).File.calculateFile(char)→parse(char).calculate(...)→parse(...)onMoveSuffixAnnotation(String),NotationMovingPiece(char),NotationPromotionPiece(char),FenSideSymbol(char),FenPieceSymbol(char),ResultTagValue(String),SetUpTagValue(String), andStandardTag(String).- Removed the unused
NotationPromotionPiece.existsIgnoreCase(char)andcalculateIgnoreCase(char).
FEN / SAN / LAN serializers use the to* idiom:
FenBoard.calculateFen(Board)→FenBoard.toFen(Board).MoveToSan.calculateSanLastMove(LegalMove, List<LegalMove>, SanTerminalMarker)→MoveToSan.toSan(LegalMove move, List<LegalMove> legalMovesBeforeMove, SanTerminalMarker).MoveToLan.calculateLanLastMove(LegalMove, SanTerminalMarker)→MoveToLan.toLan(LegalMove move, SanTerminalMarker).UciMoveUtility.convertMoveSpecificationToUci(Side, MoveSpecification)→toUci(...),convertUciMoveToMoveSpecification(Board, UciMove)→toMoveSpecification(...), andconvertUciMoveToSan(Board, UciMove)→toSan(...)— dropping theconvert*prefix for theto*idiom.PgnUtility.calculateBoard(PgnGame)/calculateBoard(Path, String)→toBoard(...)— replays a PGN game onto a fresh board.TagUtility.calculateTagValue(PgnGame, String)/calculateTagValue(PgnGame, StandardTag)→readTagValue(...), matching the sibling tag readers (readFen,readResult, …).
EnumConstants removed from the public API:
- The square / piece / side alias layer (
import static …EnumConstants.E4) was a non-idiomatic shortcut over the real enum types and is no longer part of the published library. Production code now references the enums directly (import static …board.enums.Square.E4, etc.), andEnumConstantshas moved to the test sources as a fixture convenience. Callers should use the real enum constants —Square.E4,Side.WHITE,PieceType.KING. (It had also stopped being a constant interface: types likeLegalMove/CastlingUtilitynever exposed the aliases through their public API.)
Removed from the published jar:
- Deleted unused types
BoardUtility,CheckmateOrStalemate, andSemiOpenFilesUtility. - Deleted the unused placement-only Zobrist hash —
BitboardPosition.zobristPieces()/hashDelta(MoveSpecification, Side)and the package-privateZobristKeys. Built and tested but never wired in: the helpmate transposition table and the repetition check key on the exact, collision-free value recordsHelpmateSearchKey/DynamicPosition, and a placement-only hash (no side-to-move / castling / en-passant) had no internal use and could not be safely exposed as a position key. - Relocated test-only support out of
main(public, but used only by the test / oracle layers):StandardMoveUtility,LegalMoveCalculation,SquareOccupation,IoUtility,PseudoLegalMove.
Boolean predicate methods drop the calculateIs / calculateHas prefix for the JavaBeans is / has idiom:
CastlingUtility.calculateIsCastlingMove→isCastlingMove;calculateQueenSideCastlingIsOriginalPosition/calculateKingSideCastlingIsOriginalPosition→isQueenSideCastlingOriginalPosition/isKingSideCastlingOriginalPosition.BasicUtility.calculateIsDisjoint→isDisjoint.EnPassantCaptureUtility.calculateIsPawnTwoSquareAdvanceMove/calculateIsPotentialEnPassantCapture→isPawnTwoSquareAdvanceMove/isPotentialEnPassantCapture.PawnDiagonalMoveUtility.calculateIsPawnDiagonalMove→isPawnDiagonalMove.TagUtility.calculateIsContainsAllSevenTagRosterTags→hasAllSevenTagRosterTags.PromotionUtility.calculateIsPromotion(MoveSpecification)removed; the predicate is now intrinsic to the record asMoveSpecification.isPromotion().
PGN export uses the to* idiom:
PgnCreate.createPgnString(...)→toPgnString(...)(all three overloads) andcreatePgnLines(...)→toPgnLines(...)(both overloads).createPgnGame(...)is unchanged — it builds aPgnGame, it does not serialize one.
Immutable collection types in signatures:
UciMoveValidationUtility.getUciMoves()returnsImmutableList<UciMove>(wasList<UciMove>).SquareUtility.getPromotionRankSquares(Side)(renamed fromSquare.getPromotionRank) andgetEnPassantCaptureTargetSquares(Side)returnImmutableList<Square>(wasList<Square>).- The empty-board square getters —
KnightEmptyBoardSquares.getKnightSquares,KingNonCastlingEmptyBoardSquares.getKingSquares,PawnDiagonalSquares.getPawnDiagonalSquares, andPawn{Any,One,Two}AdvanceEmptyBoardSquares.getPawnSquares— returnImmutableSet<Square>(wasSet<Square>). BitboardPosition.potentialToSquares(Square, long)andlegalMoves(Side, long)returnImmutableSet<Square>/ImmutableSet<MoveSpecification>— previously a freshly-allocated mutableTreeSetleaked through a publicSet<...>return.- Callers assigning to
List/Setare unaffected; the change is binary-incompatible and only tightens the declared type.
Exceptions:
NonePointerExceptionnow extendsUsageExceptioninstead ofProgrammingMistakeException. Reading a property of aNONEsentinel (e.g.Piece.NONE.getSide()after reading an empty square) is caller misuse, not a library bug. Code that caughtProgrammingMistakeExceptionto trap NONE-access should catchUsageException(orNonePointerException) instead.- The strict / lenient FEN and PGN
validate(...)methods now let aProgrammingMistakeExceptionpropagate rather than folding it into anUNKNOWN_ERRORresult, so a genuine library bug fails fast. Caller-fault runtime errors (including NONE-access, now aUsageException) still surface asUNKNOWN_ERROR.
BasicChessUtility deleted - its rule queries moved to their domain owners:
BasicChessUtility.calculateOutcome(board)→board.outcome()(the current-position outcome is now a query onBoard, besideisCheckmate()/isStalemate(); same precedence, same non-nullOutcome.ONGOINGfor ongoing positions).BasicChessUtility.calculateIsResetHalfMoveClock(move)→move.resetsHalfMoveClock()(aLegalMovepredicate - true for a pawn move or any capture).BasicChessUtility.calculateSideMoved(...)andcalculateFullMoveNumber(Side, int, int)are removed with no replacement; they were internal helpers with no production caller.- The
BasicChessUtilityclass is deleted.
Dead position is its own analyzer:
- The no-side overloads
UnwinnableQuickAnalyzer.unwinnableQuick(Board)andUnwinnableFullAnalyzer.unwinnableFull(Board)are removed. Whole-position dead detection now lives inunwinnability.DeadPositionAnalyzer, withBoard.deadPositionQuick()/Board.deadPositionFull()convenience methods and its own verdicts:DeadPositionQuickVerdict(DEAD/POSSIBLY_ALIVE) andDeadPositionFullVerdict(DEAD/ALIVE/UNDETERMINED). Migration:UnwinnableQuickAnalyzer.unwinnableQuick(board) == UNWINNABLEbecomesboard.deadPositionQuick() == DEAD; for the full check the twoWINNABLE_*results map toALIVE. Behaviour is unchanged — the per-side analyzers and verdicts are untouched.
Noninstantiable classes are final:
- The static-only utility, constant, translator, and analyzer classes (including
UnwinnableQuickAnalyzer/UnwinnableFullAnalyzer) are nowfinalwith private constructors (previouslyabstractor implicitly instantiable). They were never meant to be subclassed or instantiated, so this affects only code that did so.