├── images ├── logo.png └── banner.png ├── example ├── json │ ├── README.md │ ├── clobber.json │ ├── clobber10.json │ ├── jesonMor.json │ ├── breakthrough.json │ ├── joust.json │ ├── kono.json │ ├── antichess.json │ ├── shatranj.json │ ├── miniXiangqi.json │ ├── tenCubed.json │ ├── hoppelPoppel.json │ ├── newZealand.json │ ├── nightrider.json │ ├── knightmate.json │ ├── grasshopper.json │ ├── micro.json │ ├── mini.json │ ├── racingKings.json │ ├── ordaMirror.json │ ├── dart.json │ ├── nano.json │ ├── chess.json │ ├── legan.json │ ├── berolina.json │ ├── threeKings.json │ ├── domination.json │ ├── wolf.json │ ├── spawn.json │ ├── dobutsu.json │ ├── shako.json │ ├── crazyhouse.json │ ├── grand.json │ ├── pocketKnight.json │ ├── capablanca.json │ ├── omega.json │ ├── threeCheck.json │ ├── chess960.json │ ├── miniRandom.json │ ├── andernach.json │ ├── atomic.json │ ├── koth.json │ ├── opulent.json │ ├── horde.json │ ├── seirawan.json │ ├── musketeer.json │ ├── troitzky.json │ ├── shogi.json │ ├── xiangqi.json │ ├── extinction.json │ ├── kinglet.json │ ├── manchu.json │ └── orda.json ├── pubspec.yaml ├── pgn │ ├── racing.pgn │ ├── chess.pgn │ ├── atomic.pgn │ └── 960.pgn ├── example_variant.json ├── viewer.dart ├── example.dart └── json.dart ├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── lib ├── src │ ├── first_where_extension.dart │ ├── move │ │ ├── move_generator.dart │ │ ├── pass_move.dart │ │ ├── multi_move.dart │ │ ├── move_meta.dart │ │ ├── drop_move.dart │ │ ├── gating_move.dart │ │ ├── static_move.dart │ │ ├── wrapper_move.dart │ │ ├── move_list_extension.dart │ │ ├── move.dart │ │ └── move_processor.dart │ ├── actions │ │ ├── base_actions.dart │ │ ├── actions │ │ │ ├── block.dart │ │ │ ├── points.dart │ │ │ ├── transfer.dart │ │ │ ├── immortality.dart │ │ │ └── explosion.dart │ │ ├── events.dart │ │ ├── trigger.dart │ │ ├── effects.dart │ │ ├── definitions.dart │ │ └── actions.dart │ ├── serialisation │ │ ├── pass_adapters.dart │ │ ├── drop_adapters.dart │ │ ├── type_adapter.dart │ │ ├── state_transform_adapters.dart │ │ ├── first_move_adapters.dart │ │ └── promo_adapters.dart │ ├── game │ │ ├── game_info.dart │ │ └── game_utils.dart │ ├── state │ │ ├── state_meta.dart │ │ └── state_transformer.dart │ ├── variant │ │ ├── options │ │ │ ├── pass_options.dart │ │ │ ├── first_move_options.dart │ │ │ └── output_options.dart │ │ └── variants │ │ │ ├── musketeer.dart │ │ │ ├── small.dart │ │ │ ├── asymmetric.dart │ │ │ ├── shape.dart │ │ │ ├── fairy.dart │ │ │ ├── shogi.dart │ │ │ └── other.dart │ ├── regions │ │ ├── built_region.dart │ │ ├── xor_region.dart │ │ ├── union_region.dart │ │ ├── subtract_region.dart │ │ ├── intersect_region.dart │ │ ├── set_region.dart │ │ ├── area.dart │ │ └── regions.dart │ ├── castling_rights.dart │ ├── move_gen_params.dart │ ├── drops.dart │ ├── square.dart │ ├── engine.dart │ └── navigator.dart └── bishop.dart ├── pubspec.yaml ├── test ├── perft_test.dart ├── constants.dart ├── state_transform_test.dart ├── draw_test.dart ├── gating_test.dart ├── promo_test.dart ├── castling_test.dart ├── teleport_test.dart ├── perft.dart ├── divide.dart ├── square_test.dart ├── crazyhouse_test.dart ├── hopper_test.dart ├── pieces_test.dart └── region_test.dart ├── analysis_options.yaml ├── .github └── workflows │ └── test_package.yml ├── CONTRIBUTING.md └── LICENSE /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexobviously/bishop/HEAD/images/logo.png -------------------------------------------------------------------------------- /example/json/README.md: -------------------------------------------------------------------------------- 1 | All built-in variants are provided here in JSON format for reference. -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexobviously/bishop/HEAD/images/banner.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | packages 3 | .project 4 | pubspec.lock 5 | .packages 6 | .dart_tool 7 | temp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 80, 3 | "[dart]": { 4 | "editor.rulers": [ 5 | 80 6 | ], 7 | } 8 | } -------------------------------------------------------------------------------- /lib/src/first_where_extension.dart: -------------------------------------------------------------------------------- 1 | extension IterableExtension on Iterable { 2 | T? firstWhereOrNull(bool Function(T e) test) { 3 | for (T e in this) { 4 | if (test(e)) return e; 5 | } 6 | return null; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bishop_example 2 | description: A sample command-line application. 3 | version: 0.1.0 4 | publish_to: 'none' 5 | 6 | environment: 7 | sdk: '>=2.17.1 <3.0.0' 8 | 9 | dependencies: 10 | args: ^2.4.0 11 | colorize: ^3.0.0 12 | bishop: 13 | path: ../ 14 | nice_json: ^1.0.0 15 | -------------------------------------------------------------------------------- /lib/src/move/move_generator.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | typedef MoveGenFunction = List Function({ 4 | required BishopState state, 5 | required int player, 6 | MoveGenParams params, 7 | }); 8 | 9 | abstract class MoveGenerator { 10 | MoveGenFunction build(BuiltVariant variant); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/move/pass_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A move that simply passes the turn for the active player. 4 | class PassMove extends Move { 5 | @override 6 | int get from => Bishop.invalid; 7 | @override 8 | int get to => Bishop.invalid; 9 | 10 | const PassMove(); 11 | 12 | String algebraic() => 'pass'; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/actions/base_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | part 'actions/block.dart'; 4 | part 'actions/checks.dart'; 5 | part 'actions/explosion.dart'; 6 | part 'actions/hands.dart'; 7 | part 'actions/immortality.dart'; 8 | part 'actions/points.dart'; 9 | part 'actions/region.dart'; 10 | part 'actions/transfer.dart'; 11 | -------------------------------------------------------------------------------- /lib/src/serialisation/pass_adapters.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | class NoPassAdapter extends BasicAdapter { 4 | const NoPassAdapter() : super('bishop.pass.none', NoPass.new); 5 | } 6 | 7 | class StandardPassAdapter extends BasicAdapter { 8 | const StandardPassAdapter() : super('bishop.pass.standard', StandardPass.new); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/move/multi_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A move that consists of multiple steps. 4 | /// Not yet fully implemented. 5 | class MultiMove extends Move { 6 | final List moves; 7 | const MultiMove({required this.moves}); 8 | 9 | @override 10 | int get from => moves.first.from; 11 | 12 | @override 13 | int get to => moves.last.to; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/game/game_info.dart: -------------------------------------------------------------------------------- 1 | // A convenience class for use with other packages (e.g. Squares) 2 | part of 'game.dart'; 3 | 4 | class GameInfo { 5 | final Move? lastMove; 6 | final String? lastFrom; 7 | final String? lastTo; 8 | final String? checkSq; 9 | 10 | const GameInfo({ 11 | this.lastMove, 12 | this.lastFrom, 13 | this.lastTo, 14 | this.checkSq, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bishop 2 | version: 1.4.4 3 | description: Bishop is a chess logic package with flexible variant support. 4 | repository: https://github.com/alexobviously/bishop 5 | issue_tracker: https://github.com/alexobviously/bishop/issues 6 | environment: 7 | sdk: '>=3.2.0 <4.0.0' 8 | 9 | dev_dependencies: 10 | benchmark_harness: ^2.0.0 11 | collection: ^1.17.1 12 | lints: ^2.0.0 13 | test: ^1.17.10 -------------------------------------------------------------------------------- /test/perft_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'perft.dart'; 5 | 6 | void main() { 7 | group('Perft', () { 8 | for (PerftTest pt in Perfts.standard) { 9 | test('Perft ${pt.fen} [${pt.variant.name}]', () { 10 | Game g = Game(variant: pt.variant.build(), fen: pt.fen); 11 | int nodes = g.perft(pt.depth); 12 | expect(nodes, equals(pt.nodes)); 13 | }); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /example/json/clobber.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clobber", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "5x6", 6 | "pieceTypes": {"P": {"betza": "cW"}}, 7 | "castlingOptions": {"enabled": false}, 8 | "promotionOptions": "bishop.promo.none", 9 | "materialConditions": {"enabled": false}, 10 | "gameEndConditions": { 11 | "stalemate": "lose", 12 | "elimination": "lose" 13 | }, 14 | "startPosition": "PpPpP/pPpPp/PpPpP/pPpPp/PpPpP/pPpPp w - - 0 1", 15 | "enPassant": false 16 | } -------------------------------------------------------------------------------- /lib/src/state/state_meta.dart: -------------------------------------------------------------------------------- 1 | part of 'state.dart'; 2 | 3 | class StateMeta { 4 | final BuiltVariant variant; 5 | final MoveMeta? moveMeta; 6 | final List>? checks; 7 | 8 | const StateMeta({ 9 | required this.variant, 10 | this.moveMeta, 11 | this.checks, 12 | }); 13 | 14 | String? get algebraic => moveMeta?.algebraic; 15 | String? get prettyName => moveMeta?.formatted; 16 | List get inCheck => 17 | checks?.map((e) => e.isNotEmpty).toList() ?? [null, null]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/move/move_meta.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// Contains move names. 4 | class MoveMeta { 5 | /// Simple algebraic form, e.g. b1c3, e7e8q. 6 | final String algebraic; 7 | 8 | /// A formatted string representation of a move, specific to the variant's 9 | /// move formatting scheme. In most chess-related cases, this will likely 10 | /// be SAN, e.g. e5, Nxc7, b8=Q. 11 | final String formatted; 12 | 13 | const MoveMeta({ 14 | required this.algebraic, 15 | required this.formatted, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | always_declare_return_types: true 6 | camel_case_types: true 7 | cancel_subscriptions: true 8 | constant_identifier_names: true 9 | noop_primitive_operations: true 10 | prefer_const_constructors: true 11 | # prefer_final_locals: true # todo: there are like 500 of these lol 12 | prefer_single_quotes: true 13 | require_trailing_commas: true 14 | unawaited_futures: true 15 | unnecessary_to_list_in_spreads: true -------------------------------------------------------------------------------- /.github/workflows/test_package.yml: -------------------------------------------------------------------------------- 1 | name: Bishop Package Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | container: 14 | image: dart:latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Print Dart SDK version 20 | run: dart --version 21 | 22 | - name: Install dependencies 23 | run: dart pub get 24 | 25 | - name: Run Tests 26 | run: dart test test -------------------------------------------------------------------------------- /example/json/clobber10.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clobber10", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": {"P": {"betza": "cW"}}, 7 | "castlingOptions": {"enabled": false}, 8 | "promotionOptions": "bishop.promo.none", 9 | "materialConditions": {"enabled": false}, 10 | "gameEndConditions": { 11 | "stalemate": "lose", 12 | "elimination": "lose" 13 | }, 14 | "startPosition": "PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP w - - 0 1", 15 | "enPassant": false 16 | } -------------------------------------------------------------------------------- /lib/src/move/drop_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A move where a piece is dropped from the player's hand onto the board. 4 | /// This is only for hand drops, not gating moves, which are currently still 5 | /// part of `StandardMove`. 6 | class DropMove extends Move { 7 | @override 8 | bool get handDrop => true; 9 | @override 10 | int get from => Bishop.hand; 11 | @override 12 | final int to; 13 | final int piece; 14 | 15 | @override 16 | int get dropPiece => piece; 17 | 18 | const DropMove({required this.to, required this.piece}); 19 | 20 | String algebraic(BoardSize size) => '@${size.squareName(to)}'; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/move/gating_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A move that involves a piece being moved out of the player's gate, and 4 | /// onto the player's first rank. 5 | class GatingMove extends WrapperMove { 6 | @override 7 | final int dropPiece; 8 | 9 | /// For gating drops that are also castling moves - should we gate on square 10 | /// that the king came from (false) or the rook (true). 11 | final bool dropOnRookSquare; 12 | 13 | const GatingMove({ 14 | required super.child, 15 | required this.dropPiece, 16 | this.dropOnRookSquare = false, 17 | }); 18 | 19 | @override 20 | bool get gate => true; 21 | } 22 | -------------------------------------------------------------------------------- /example/json/jesonMor.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jeson Mor", 3 | "description": "Knights only. Move a knight onto the central square and off it again to win.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "9x9", 6 | "pieceTypes": {"N": {"betza": "N", "value": 300}}, 7 | "castlingOptions": {"enabled": false}, 8 | "promotionOptions": "bishop.promo.none", 9 | "materialConditions": {"enabled": false}, 10 | "startPosition": "nnnnnnnnn/9/9/9/9/9/9/9/NNNNNNNNN w - - 0 1", 11 | "enPassant": false, 12 | "halfMoveDraw": 100, 13 | "actions": [ 14 | { 15 | "id": "bishop.action.exitRegionEnding", 16 | "region": {"b": 4, "t": 4, "l": 4, "r": 4} 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /lib/src/actions/actions/block.dart: -------------------------------------------------------------------------------- 1 | part of '../base_actions.dart'; 2 | 3 | class ActionBlockOrigin extends Action { 4 | ActionBlockOrigin() 5 | : super( 6 | action: (trigger) => [ 7 | EffectModifySquare( 8 | trigger.move.from, 9 | makePiece( 10 | trigger.variant.pieceIndexLookup['*']!, 11 | Bishop.neutralPassive, 12 | ), 13 | ), 14 | ], 15 | ); 16 | } 17 | 18 | class BlockOriginAdapter extends BasicAdapter { 19 | BlockOriginAdapter() 20 | : super('bishop.action.blockOrigin', ActionBlockOrigin.new); 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "bishop", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "example", 14 | "cwd": "example", 15 | "request": "launch", 16 | "type": "dart", 17 | "program": "example.dart", 18 | "args": [] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /lib/src/variant/options/pass_options.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | abstract class PassOptions { 4 | const PassOptions(); 5 | 6 | MoveChecker? build(BuiltVariant variant); 7 | 8 | static const PassOptions none = NoPass(); 9 | static const PassOptions standard = StandardPass(); 10 | } 11 | 12 | /// Never generates a pass move. 13 | class NoPass implements PassOptions { 14 | const NoPass(); 15 | @override 16 | MoveChecker? build(BuiltVariant variant) => null; 17 | } 18 | 19 | /// Always generates a pass move. 20 | class StandardPass implements PassOptions { 21 | const StandardPass(); 22 | @override 23 | MoveChecker build(BuiltVariant variant) => (_) => true; 24 | } 25 | -------------------------------------------------------------------------------- /example/json/breakthrough.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Breakthrough", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfF", 9 | "regionEffects": [ 10 | { 11 | "whiteRegion": "blackCamp", 12 | "blackRegion": "whiteCamp", 13 | "winGame": true 14 | } 15 | ] 16 | } 17 | }, 18 | "castlingOptions": {"enabled": false}, 19 | "promotionOptions": "bishop.promo.standard", 20 | "materialConditions": {"enabled": false}, 21 | "startPosition": "pppppppp/pppppppp/8/8/8/8/PPPPPPPP/PPPPPPPP w - - 0 1", 22 | "enPassant": false, 23 | "regions": { 24 | "whiteCamp": {"b": 0, "t": 0}, 25 | "blackCamp": {"b": 7, "t": 7} 26 | } 27 | } -------------------------------------------------------------------------------- /example/pgn/racing.pgn: -------------------------------------------------------------------------------- 1 | [Event "Rated Racing Kings game"] 2 | [Site "https://lichess.org/TC5BxFYS"] 3 | [Date "2023.02.27"] 4 | [White "RoyalManiac"] 5 | [Black "Walker_22"] 6 | [Result "1-0"] 7 | [UTCDate "2023.02.27"] 8 | [UTCTime "15:23:00"] 9 | [WhiteElo "2473"] 10 | [BlackElo "2249"] 11 | [WhiteRatingDiff "+2"] 12 | [BlackRatingDiff "-2"] 13 | [Variant "Racing Kings"] 14 | [TimeControl "30+0"] 15 | [ECO "?"] 16 | [Opening "?"] 17 | [Termination "Normal"] 18 | [Annotator "lichess.org"] 19 | 20 | 1. Nxc2 Nxf2 2. Rxf2 Rxc2 3. Rg6 Kb3 4. Kg3 Rc5 5. Qh7 Qd4 6. Bg2 Ba3 7. Rf4 Qd8 8. Kg4 Rb2 9. Ng3 Nc4 10. Nf5 Kb4 11. Kh5 Ka5 12. Kh6 Rb6 13. Kg7 Ka6 14. Qg8 Ka7 15. Qxd8 Rc8 16. Qxc8 Rb8 17. Kg8# { Game ends by variant rule. } 1-0 -------------------------------------------------------------------------------- /lib/src/move/static_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A move that starts and ends on the same square. 4 | /// This doesn't have any default implementation in the engine; it is intended 5 | /// for use in games that have a special move that doesn't move a piece, with 6 | /// its own move processor. 7 | class StaticMove extends Move { 8 | final int square; 9 | 10 | @override 11 | int get from => square; 12 | 13 | @override 14 | int get to => square; 15 | 16 | const StaticMove(this.square); 17 | 18 | String algebraic({BoardSize size = BoardSize.standard}) { 19 | String fromStr = size.squareName(from); 20 | String toStr = size.squareName(to); 21 | return '$fromStr$toStr'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/pgn/chess.pgn: -------------------------------------------------------------------------------- 1 | [Event "F/S Return Match"] 2 | [Site "Belgrade, Serbia JUG"] 3 | [Date "1992.11.04"] 4 | [Round "29"] 5 | [White "Fischer, Robert J."] 6 | [Black "Spassky, Boris V."] 7 | [Result "1/2-1/2"] 8 | 9 | 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 {This opening is called the Ruy Lopez.} 10 | 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 11 | 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5 12 | Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6 13 | 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5 14 | hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5 15 | 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6 16 | Nf2 42. g4 Bd3 43. Re6 1/2-1/2 -------------------------------------------------------------------------------- /lib/src/actions/events.dart: -------------------------------------------------------------------------------- 1 | part of 'actions.dart'; 2 | 3 | /// The type of event that triggers an action. 4 | enum ActionEvent { 5 | /// Actions with this event type are triggered in `makeMove` before the 6 | /// logic of a move is applied. Useful for custom validation logic. 7 | beforeMove, 8 | 9 | /// Actions with this event are applied directly after the logic of a move 10 | /// is applied, and are typically used to add custom move logic, or to 11 | /// validate that the move would not put the game in an illegal state. 12 | /// If you don't know which event to use, you probably want this one. 13 | afterMove; 14 | 15 | factory ActionEvent.import(String? name) => 16 | values.firstWhereOrNull((e) => e.name == name) ?? ActionEvent.afterMove; 17 | String export() => name; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/regions/built_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | /// A region that is created from a `BoardRegion` when `BuiltVariant` is built. 4 | class BuiltRegion implements Region { 5 | final List boardSquares; 6 | final BoardSize size; 7 | 8 | const BuiltRegion(this.boardSquares, this.size); 9 | 10 | bool containsSquare(int square) => boardSquares.contains(square); 11 | 12 | @override 13 | bool contains(int file, int rank) => containsSquare(size.square(file, rank)); 14 | 15 | @override 16 | List squares(BoardSize size) => boardSquares; 17 | 18 | @override 19 | Region translate(int x, int y) => BuiltRegion( 20 | boardSquares 21 | .map((e) => size.square(size.file(e) + x, size.rank(e) + y)) 22 | .toList(), 23 | size, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /example/json/joust.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Joust", 3 | "description": "The square a piece moves from is removed from the boardafter each move.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "N": {"betza": "mN"}, 8 | "*": { 9 | "betza": "", 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "value": 0, 15 | "actions": ["bishop.action.immortality"] 16 | } 17 | }, 18 | "castlingOptions": {"enabled": false}, 19 | "promotionOptions": "bishop.promo.none", 20 | "materialConditions": {"enabled": false}, 21 | "gameEndConditions": { 22 | "stalemate": "lose", 23 | "elimination": "lose" 24 | }, 25 | "startPosition": "8/8/8/4n3/3N4/8/8/8 w - - 0 1", 26 | "enPassant": false, 27 | "actions": ["bishop.action.blockOrigin"] 28 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are very much welcome! 4 | 5 | If you want to contribute code, fork the repo and make PRs to the `development` branch. Try to change as few things as possible per PR to make the reviewing process a bit easier! 6 | 7 | Good things to contribute are: 8 | * Popular variant defintions that are possible under the current system. 9 | * Implementations of small issues in the issue tracker. 10 | * Proposals for how to implement larger features. Probably best not to completely build these without proposing them since I use a fairly opinionated code style for Bishop. 11 | 12 | Guidelines: 13 | * Follow the Dart style guide in general. 14 | * Try not to use `var` or `dynamic`. 15 | * Use British English (pieces have a `colour`). 16 | 17 | Feature and variant requests are also very much welcome! Feel free to create issues on the issue tracker. -------------------------------------------------------------------------------- /example/pgn/atomic.pgn: -------------------------------------------------------------------------------- 1 | [Event "Rated Atomic game"] 2 | [Site "https://lichess.org/cV5xoxds"] 3 | [Date "2016.04.11"] 4 | [White "Arka50"] 5 | [Black "GrandLapin"] 6 | [Result "0-1"] 7 | [UTCDate "2016.04.11"] 8 | [UTCTime "03:08:28"] 9 | [WhiteElo "2706"] 10 | [BlackElo "2788"] 11 | [WhiteRatingDiff "-9"] 12 | [BlackRatingDiff "+10"] 13 | [WhiteTitle "GM"] 14 | [Variant "Atomic"] 15 | [TimeControl "240+0"] 16 | [ECO "?"] 17 | [Opening "?"] 18 | [Termination "Normal"] 19 | [Annotator "lichess.org"] 20 | 21 | 1. e3 e6 2. Nf3 Qf6 3. Bb5 c6 4. Bd3 Bb4 5. c3 Bxc3 6. Qa4 Nh6 7. Kd1 Na6 8. Qh4 d6 9. Ne5 Kd8 10. Nxf7+ g5 11. Qh5 Kc7 12. b4 b6 13. Qf3 Kb8 14. Qf6 Bd7 15. g4 e5 16. h4 Nf7 17. Qe7 h5 18. a4 Nc7 19. e4 a5 20. b5 c5 21. Nc3 Ne6 22. Qxe6 d5 23. exd5 e4 24. Nd5 Rd8 25. Bb2 c4 26. Be5+ Rxd5 27. Ke1 Ka7 28. Kf1 Rf8 29. f4 exf3+ 30. Kg1 Rf1+ 31. Kg2 Rxh1# { Game ends by variant rule. } 0-1 -------------------------------------------------------------------------------- /example/json/kono.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Five Field Kono", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "5x5", 6 | "pieceTypes": {"P": {"betza": "mF"}}, 7 | "castlingOptions": {"enabled": false}, 8 | "promotionOptions": "bishop.promo.none", 9 | "materialConditions": {"enabled": false}, 10 | "startPosition": "ppppp/p3p/5/P3P/PPPPP w - - 0 1", 11 | "enPassant": false, 12 | "regions": { 13 | "w": { 14 | "type": "set", 15 | "squares": [ 16 | "a5", 17 | "b5", 18 | "c5", 19 | "d5", 20 | "e5", 21 | "a4", 22 | "e4" 23 | ] 24 | }, 25 | "b": { 26 | "type": "set", 27 | "squares": [ 28 | "a1", 29 | "b1", 30 | "c1", 31 | "d1", 32 | "e1", 33 | "a2", 34 | "e2" 35 | ] 36 | } 37 | }, 38 | "actions": [ 39 | { 40 | "id": "bishop.action.fillRegion", 41 | "whiteId": "w", 42 | "blackId": "b" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /example/pgn/960.pgn: -------------------------------------------------------------------------------- 1 | [Event "Rated Chess960 game"] 2 | [Site "https://lichess.org/lTezXayN"] 3 | [Date "2023.02.27"] 4 | [White "Andrey_Efimenko"] 5 | [Black "firuza123"] 6 | [Result "1-0"] 7 | [UTCDate "2023.02.27"] 8 | [UTCTime "17:22:48"] 9 | [WhiteElo "2006"] 10 | [BlackElo "2111"] 11 | [WhiteRatingDiff "+7"] 12 | [BlackRatingDiff "-8"] 13 | [Variant "Chess960"] 14 | [TimeControl "60+0"] 15 | [ECO "?"] 16 | [Opening "?"] 17 | [Termination "Time forfeit"] 18 | [FEN "brnbknqr/pppppppp/8/8/8/8/PPPPPPPP/BRNBKNQR w KQkq - 0 1"] 19 | [SetUp "1"] 20 | [Annotator "lichess.org"] 21 | 22 | 1. e3 b6 2. Ng3 c5 3. c4 Ng6 4. Nb3 h5 5. d3 h4 6. Ne4 h3 7. g3 f5 8. Nc3 Bxh1 9. Qxh1 Qe6 10. Bf3 Nd6 11. O-O-O Ne5 12. Be2 Bc7 13. Nd5 Bd8 14. d4 cxd4 15. exd4 Nexc4 16. Bf3 Rc8 17. Kb1 Qh6 18. a3 e6 19. Nf4 Bg5 20. Nd2 Bxf4 21. gxf4 Nxd2+ 22. Rxd2 Qxf4 23. Rd1 Ke7 24. d5 e5 25. b4 e4 26. Be2 Ne8 27. Qg1 Kf7 28. Bb2 d6 29. Bb5 { White wins on time. } 1-0 -------------------------------------------------------------------------------- /example/example_variant.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example", 3 | "description": "An example variant for JSON serialisation", 4 | "bishopVersion": "1.4.1", 5 | "boardSize": "3x5", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "K", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | } 14 | }, 15 | "N": {"betza": "N", "value": 300}, 16 | "P": { 17 | "betza": "fmWfcF", 18 | "promoOptions": { 19 | "canPromote": true, 20 | "canPromoteTo": false 21 | }, 22 | "noSanSymbol": true, 23 | "value": 100 24 | } 25 | }, 26 | "castlingOptions": {"enabled": false}, 27 | "promotionOptions": "bishop.promo.standard", 28 | "materialConditions": {"enabled": false}, 29 | "startPosition": "nkn/ppp/3/PPP/NKN w - - 0 1", 30 | "enPassant": false, 31 | "actions": [ 32 | "example.action.doesNothing", 33 | { 34 | "id": "example.action.doesNothing", 35 | "something": "pawn" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /example/json/antichess.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Antichess", 3 | "description": "Lose all your pieces to win.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": -100 16 | }, 17 | "N": {"betza": "N", "value": -300}, 18 | "B": {"betza": "B", "value": -300}, 19 | "R": {"betza": "R", "value": -500}, 20 | "Q": {"betza": "Q", "value": -900}, 21 | "K": {"betza": "K", "value": -400} 22 | }, 23 | "castlingOptions": {"enabled": false}, 24 | "promotionOptions": "bishop.promo.standard", 25 | "materialConditions": {"enabled": false}, 26 | "gameEndConditions": { 27 | "stalemate": "win", 28 | "elimination": "win" 29 | }, 30 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 31 | "enPassant": true, 32 | "halfMoveDraw": 100, 33 | "repetitionDraw": 3, 34 | "forcedCapture": "any" 35 | } -------------------------------------------------------------------------------- /lib/src/move/wrapper_move.dart: -------------------------------------------------------------------------------- 1 | part of 'move.dart'; 2 | 3 | /// A type of move that wraps another move and extends its functionality. 4 | /// Currently [child] is limited to StandardMoves only. 5 | class WrapperMove implements Move { 6 | final StandardMove child; 7 | const WrapperMove({required this.child}); 8 | 9 | @override 10 | bool get capture => child.capture; 11 | 12 | @override 13 | int? get capturedPiece => child.capturedPiece; 14 | 15 | @override 16 | bool get castling => child.castling; 17 | 18 | @override 19 | int? get dropPiece => child.dropPiece; 20 | 21 | @override 22 | bool get enPassant => child.enPassant; 23 | 24 | @override 25 | int get from => child.from; 26 | 27 | @override 28 | bool get gate => child.gate; 29 | 30 | @override 31 | bool get handDrop => child.handDrop; 32 | 33 | @override 34 | int? get promoPiece => child.promoPiece; 35 | 36 | @override 37 | bool get promotion => child.promotion; 38 | 39 | @override 40 | bool get setEnPassant => child.setEnPassant; 41 | 42 | @override 43 | int get to => child.to; 44 | } 45 | -------------------------------------------------------------------------------- /example/json/shatranj.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shatranj", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfcF", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false, 12 | "promotesTo": ["Q"] 13 | }, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "A", "value": 120}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "F", "value": 140}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": {"enabled": false}, 31 | "promotionOptions": "bishop.promo.standard", 32 | "materialConditions": {"enabled": false}, 33 | "gameEndConditions": { 34 | "stalemate": "lose", 35 | "elimination": "lose" 36 | }, 37 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 38 | "enPassant": false, 39 | "halfMoveDraw": 100, 40 | "repetitionDraw": 3 41 | } -------------------------------------------------------------------------------- /test/constants.dart: -------------------------------------------------------------------------------- 1 | const int standard = 0; 2 | const int chess960 = 1; 3 | 4 | class Positions { 5 | static const String standardDefault = 6 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; 7 | static const String kiwiPete = 8 | 'r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1'; 9 | static const String ep = 10 | 'rnbqkbnr/pp1pppp1/7p/2pP4/8/8/PPP1PPPP/RNBQKBNR w KQkq c6 0 3'; 11 | static const String rookPin = '8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1'; 12 | static const String position4 = 13 | 'r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1'; 14 | static const String position5 = 15 | 'rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8'; 16 | static const String position6 = 17 | 'r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10'; 18 | static const String standardMicro = 'rnbqk/ppppp/5/PPPPP/RNBQK w Qq - 0 1'; 19 | static const String standardNano = 'knbr/p3/4/3P/RBNK w Qk - 0 1'; 20 | static const String standardMini = 21 | 'rbnkbr/pppppp/6/6/PPPPPP/RBNKBR w KQkq - 0 1'; 22 | } 23 | -------------------------------------------------------------------------------- /example/json/miniXiangqi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mini Xiangqi", 3 | "description": "A miniature variant of Xiangqi, played on a 7x7 board with no river.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "7x7", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "W", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "redPalace", 17 | "blackRegion": "blackPalace", 18 | "restrictMovement": true 19 | } 20 | ] 21 | }, 22 | "N": {"betza": "nN", "value": 400}, 23 | "R": {"betza": "R", "value": 900}, 24 | "C": {"betza": "mRcpR", "value": 450}, 25 | "P": {"betza": "fsW"} 26 | }, 27 | "castlingOptions": {"enabled": false}, 28 | "promotionOptions": "bishop.promo.none", 29 | "materialConditions": {"enabled": false}, 30 | "startPosition": "rcnkncr/p1ppp1p/7/7/7/P1PPP1P/RCNKNCR w - - 0 1", 31 | "enPassant": false, 32 | "regions": { 33 | "redPalace": {"b": 0, "t": 2, "l": 2, "r": 4}, 34 | "blackPalace": {"b": 4, "t": 6, "l": 2, "r": 4} 35 | }, 36 | "actions": ["bishop.action.flyingGenerals"] 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Bishop 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Alex Baker 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /example/json/tenCubed.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TenCubed", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "A": {"betza": "BN", "value": 900}, 30 | "M": {"betza": "RN", "value": 900}, 31 | "C": {"betza": "DAW", "value": 400}, 32 | "W": {"betza": "CF", "value": 400} 33 | }, 34 | "castlingOptions": {"enabled": false}, 35 | "promotionOptions": "bishop.promo.standard", 36 | "materialConditions": {"enabled": false}, 37 | "startPosition": "2cwamwc2/1rnbqkbnr1/pppppppppp/10/10/10/10/PPPPPPPPPP/1RNBQKBNR1/2CWAMWC2 w - - 0 1", 38 | "enPassant": false 39 | } -------------------------------------------------------------------------------- /lib/bishop.dart: -------------------------------------------------------------------------------- 1 | /// A chess logic package with flexible variant support. 2 | library bishop; 3 | 4 | export 'src/actions/actions.dart'; 5 | export 'src/actions/base_actions.dart'; 6 | export 'src/betza.dart'; 7 | export 'src/castling_rights.dart'; 8 | export 'src/constants.dart'; 9 | export 'src/drops.dart'; 10 | export 'src/engine.dart'; 11 | export 'src/fen.dart'; 12 | export 'src/first_where_extension.dart'; 13 | export 'src/game/game.dart'; 14 | export 'src/move/move_definition.dart'; 15 | export 'src/move/move_formatter.dart'; 16 | export 'src/move/move_generator.dart'; 17 | export 'src/move/move_list_extension.dart'; 18 | export 'src/move/move_processor.dart'; 19 | export 'src/move_gen_params.dart'; 20 | export 'src/move/move.dart'; 21 | export 'src/navigator.dart'; 22 | export 'src/piece_type.dart'; 23 | export 'src/pgn.dart'; 24 | export 'src/promotion.dart'; 25 | export 'src/regions/regions.dart'; 26 | export 'src/serialisation/serialisation.dart'; 27 | export 'src/start_pos_builder.dart'; 28 | export 'src/square.dart'; 29 | export 'src/state/state.dart'; 30 | export 'src/utils.dart'; 31 | export 'src/variant/variant.dart'; 32 | export 'src/zobrist.dart'; 33 | -------------------------------------------------------------------------------- /example/json/hoppelPoppel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hoppel-Poppel", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "mNcB", "value": 400}, 18 | "B": {"betza": "mBcN"}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": {"enabled": false}, 42 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 43 | "enPassant": true, 44 | "halfMoveDraw": 100, 45 | "repetitionDraw": 3 46 | } -------------------------------------------------------------------------------- /example/json/newZealand.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "New Zealand Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "mNcR", "value": 400}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "mRcN"}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": {"enabled": false}, 42 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 43 | "enPassant": true, 44 | "halfMoveDraw": 100, 45 | "repetitionDraw": 3 46 | } -------------------------------------------------------------------------------- /example/json/nightrider.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nightrider Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N0", "value": 500}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": {"enabled": false}, 42 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 43 | "enPassant": true, 44 | "halfMoveDraw": 100, 45 | "repetitionDraw": 3 46 | } -------------------------------------------------------------------------------- /lib/src/regions/xor_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | /// A region that contains all squares that are in [left] or [right], but not 4 | /// in both of them, i.e. XOR. 5 | class XorRegion extends BoardRegion { 6 | final BoardRegion left; 7 | final BoardRegion right; 8 | 9 | const XorRegion(this.left, this.right); 10 | 11 | factory XorRegion.fromJson(Map json) => XorRegion( 12 | BoardRegion.fromJson(json['l']), 13 | BoardRegion.fromJson(json['r']), 14 | ); 15 | 16 | @override 17 | Map toJson() => { 18 | 'type': 'xor', 19 | 'l': left.toJson(), 20 | 'r': right.toJson(), 21 | }; 22 | 23 | @override 24 | bool contains(int file, int rank) => 25 | left.contains(file, rank) ^ right.contains(file, rank); 26 | 27 | @override 28 | Iterable squares(BoardSize size) { 29 | final l = left.squares(size); 30 | final r = right.squares(size); 31 | return l 32 | .where((e) => !r.contains(e)) 33 | .followedBy(r.where((e) => !l.contains(e))); 34 | } 35 | 36 | @override 37 | XorRegion translate(int x, int y) => 38 | XorRegion(left.translate(x, y), right.translate(x, y)); 39 | } 40 | -------------------------------------------------------------------------------- /test/state_transform_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('State Transform', () { 6 | test('Vision Area', () { 7 | final v = Variant.standard().copyWith( 8 | stateTransformer: const VisionAreaStateTransformer(), 9 | ); 10 | final g = Game( 11 | variant: v, 12 | fen: 'rnbqkbnr/ppp1pppp/8/3p4/P7/8/1PPPPPNP/RNBQKB1R w KQkq - 0 1', 13 | ); 14 | expect(g.state.pieceOnSquare('d5'), 'p'); 15 | expect(g.state.transform(Bishop.white).pieceOnSquare('d5'), '.'); 16 | expect(g.state.transform(Bishop.black).pieceOnSquare('d5'), 'p'); 17 | g.makeMoveString('d2d4'); 18 | expect(g.state.transform(Bishop.white).pieceOnSquare('d5'), 'p'); 19 | expect(g.state.transform(Bishop.black).pieceOnSquare('d4'), 'P'); 20 | expect(g.state.transform(Bishop.white).pieceOnSquare('g2'), 'N'); 21 | expect(g.state.transform(Bishop.black).pieceOnSquare('g2'), '.'); 22 | g.makeMoveString('c8h3'); 23 | expect(g.state.transform(Bishop.black).pieceOnSquare('g2'), 'N'); 24 | expect(g.state.transform(Bishop.white).pieceOnSquare('h3'), 'b'); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/regions/union_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | /// A region that contains all of the squares of each region in [regions]. 4 | class UnionRegion extends BoardRegion { 5 | final List regions; 6 | 7 | const UnionRegion(this.regions); 8 | 9 | factory UnionRegion.fromJson(Map json) => UnionRegion( 10 | (json['regions'] as List).map((e) => BoardRegion.fromJson(e)).toList(), 11 | ); 12 | 13 | @override 14 | bool contains(int file, int rank) => 15 | regions.firstWhereOrNull((e) => e.contains(file, rank)) != null; 16 | 17 | @override 18 | Set squares(BoardSize size) => 19 | regions.expand((e) => e.squares(size)).toSet(); 20 | 21 | @override 22 | UnionRegion translate(int x, int y) => 23 | UnionRegion(regions.map((e) => e.translate(x, y)).toList()); 24 | 25 | @override 26 | Map toJson() => { 27 | 'type': 'union', 28 | 'regions': [...regions.map((e) => e.toJson())], 29 | }; 30 | 31 | @override 32 | String toString() => 'Union(${regions.map((e) => e.toString()).join(', ')})'; 33 | 34 | @override 35 | UnionRegion operator +(BoardRegion other) => UnionRegion([...regions, other]); 36 | } 37 | -------------------------------------------------------------------------------- /test/draw_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'constants.dart'; 5 | 6 | void main() { 7 | List tests = [ 8 | DrawTest( 9 | variant: Variant.standard(), 10 | fen: Positions.standardDefault, 11 | draw: false, 12 | ), 13 | DrawTest( 14 | variant: Variant.standard(), 15 | fen: '8/8/8/8/8/1k6/2q5/K7 w - - 0 1', 16 | draw: true, 17 | ), 18 | DrawTest( 19 | variant: Variant.standard(), 20 | fen: '6r1/8/8/8/8/1k6/8/K7 w - - 0 1', 21 | draw: false, 22 | ), 23 | DrawTest( 24 | variant: Variant.standard(), 25 | fen: '6n1/8/8/8/8/1k6/8/K7 w - - 0 1', 26 | draw: true, 27 | ), 28 | ]; 29 | group('Draws', () { 30 | for (DrawTest t in tests) { 31 | test('Draw Test: ${t.fen}', () { 32 | Game g = Game(variant: t.variant, fen: t.fen); 33 | expect(g.drawn, t.draw); 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | class DrawTest { 40 | final Variant variant; 41 | final String fen; 42 | final bool draw; 43 | 44 | const DrawTest({ 45 | required this.variant, 46 | required this.fen, 47 | required this.draw, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /example/json/knightmate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Knightmate", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": { 18 | "betza": "N", 19 | "royal": true, 20 | "castling": false, 21 | "promoOptions": { 22 | "canPromote": false, 23 | "canPromoteTo": false 24 | }, 25 | "value": 300 26 | }, 27 | "B": {"betza": "B", "value": 300}, 28 | "R": {"betza": "R", "value": 500}, 29 | "Q": {"betza": "Q", "value": 900}, 30 | "K": {"betza": "K", "value": 300} 31 | }, 32 | "castlingOptions": { 33 | "enabled": true, 34 | "kTarget": 6, 35 | "qTarget": 2, 36 | "fixedRooks": true, 37 | "kRook": 7, 38 | "qRook": 0, 39 | "rookPiece": "R", 40 | "useRookAsTarget": false 41 | }, 42 | "promotionOptions": "bishop.promo.standard", 43 | "materialConditions": {"enabled": false}, 44 | "startPosition": "rkbqnbkr/pppppppp/8/8/8/8/PPPPPPPP/RKBQNBKR w KQkq - 0 1", 45 | "enPassant": true, 46 | "halfMoveDraw": 100, 47 | "repetitionDraw": 3 48 | } -------------------------------------------------------------------------------- /example/json/grasshopper.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grasshopper Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "G": {"betza": "gQ", "value": 180} 30 | }, 31 | "castlingOptions": { 32 | "enabled": true, 33 | "kTarget": 6, 34 | "qTarget": 2, 35 | "fixedRooks": true, 36 | "kRook": 7, 37 | "qRook": 0, 38 | "rookPiece": "R", 39 | "useRookAsTarget": false 40 | }, 41 | "promotionOptions": "bishop.promo.standard", 42 | "materialConditions": {"enabled": false}, 43 | "startPosition": "rnbqkbnr/gggggggg/pppppppp/8/8/PPPPPPPP/GGGGGGGG/RNBQKBNR w KQkq - 0 1", 44 | "enPassant": true, 45 | "halfMoveDraw": 100, 46 | "repetitionDraw": 3 47 | } -------------------------------------------------------------------------------- /example/json/micro.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Micro Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "5x5", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "qTarget": 1, 33 | "fixedRooks": true, 34 | "qRook": 0, 35 | "useRookAsTarget": false 36 | }, 37 | "promotionOptions": "bishop.promo.standard", 38 | "materialConditions": { 39 | "enabled": true, 40 | "soloMaters": ["P", "Q", "R", "A", "C"], 41 | "pairMaters": ["N"], 42 | "combinedPairMaters": ["B"], 43 | "specialCases": [["B", "N"]] 44 | }, 45 | "startPosition": "rnbqk/ppppp/5/PPPPP/RNBQK w Qq - 0 1", 46 | "enPassant": true, 47 | "halfMoveDraw": 100, 48 | "repetitionDraw": 3 49 | } -------------------------------------------------------------------------------- /lib/src/regions/subtract_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | /// A region that consists of [positive], with [negative] cut out of it. 4 | class SubtractRegion extends BoardRegion { 5 | final BoardRegion positive; 6 | final BoardRegion negative; 7 | 8 | const SubtractRegion(this.positive, this.negative); 9 | 10 | factory SubtractRegion.fromJson(Map json) => SubtractRegion( 11 | BoardRegion.fromJson(json['+']), 12 | BoardRegion.fromJson(json['-']), 13 | ); 14 | 15 | @override 16 | Map toJson() => { 17 | 'type': 'sub', 18 | '+': positive.toJson(), 19 | '-': negative.toJson(), 20 | }; 21 | 22 | @override 23 | bool contains(int file, int rank) => 24 | positive.contains(file, rank) && !negative.contains(file, rank); 25 | 26 | @override 27 | Iterable squares(BoardSize size) => positive.squares(size).where( 28 | (e) => !negative.contains(size.file(e), size.rank(e)), 29 | ); 30 | 31 | @override 32 | SubtractRegion translate(int x, int y) => SubtractRegion( 33 | positive.translate(x, y), 34 | negative.translate(x, y), 35 | ); 36 | 37 | @override 38 | String toString() => 'Subtract($positive, $negative)'; 39 | } 40 | -------------------------------------------------------------------------------- /example/json/mini.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mini Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "6x6", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfcF", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "noSanSymbol": true, 14 | "value": 100 15 | }, 16 | "N": {"betza": "N", "value": 300}, 17 | "B": {"betza": "B", "value": 300}, 18 | "R": {"betza": "R", "value": 500}, 19 | "Q": {"betza": "Q", "value": 900}, 20 | "K": { 21 | "betza": "K", 22 | "royal": true, 23 | "promoOptions": { 24 | "canPromote": false, 25 | "canPromoteTo": false 26 | } 27 | } 28 | }, 29 | "castlingOptions": { 30 | "enabled": true, 31 | "kTarget": 4, 32 | "qTarget": 1, 33 | "fixedRooks": true, 34 | "kRook": 5, 35 | "qRook": 0, 36 | "useRookAsTarget": true 37 | }, 38 | "promotionOptions": "bishop.promo.standard", 39 | "materialConditions": { 40 | "enabled": true, 41 | "soloMaters": ["P", "Q", "R", "A", "C"], 42 | "pairMaters": ["N"], 43 | "combinedPairMaters": ["B"], 44 | "specialCases": [["B", "N"]] 45 | }, 46 | "startPosition": "rbnkbr/pppppp/6/6/PPPPPP/RBNKBR w KQkq - 0 1", 47 | "enPassant": false, 48 | "halfMoveDraw": 100, 49 | "repetitionDraw": 3 50 | } -------------------------------------------------------------------------------- /example/json/racingKings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Racing Kings", 3 | "description": "The first player to run their king to the finish line wins.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | }, 28 | "regionEffects": [ 29 | { 30 | "whiteRegion": "end", 31 | "blackRegion": "end", 32 | "winGame": true 33 | } 34 | ] 35 | } 36 | }, 37 | "castlingOptions": {"enabled": false}, 38 | "promotionOptions": "bishop.promo.standard", 39 | "materialConditions": {"enabled": false}, 40 | "startPosition": "8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1", 41 | "enPassant": true, 42 | "halfMoveDraw": 100, 43 | "repetitionDraw": 3, 44 | "forbidChecks": true, 45 | "regions": {"end": {"b": 7, "t": 7}} 46 | } -------------------------------------------------------------------------------- /example/json/ordaMirror.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Orda Mirror", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "K", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "blackCamp", 17 | "blackRegion": "whiteCamp", 18 | "winGame": true 19 | } 20 | ] 21 | }, 22 | "P": { 23 | "betza": "fmWfceFifmnD", 24 | "promoOptions": { 25 | "canPromote": true, 26 | "canPromoteTo": false 27 | }, 28 | "enPassantable": true, 29 | "noSanSymbol": true, 30 | "value": 100 31 | }, 32 | "L": {"betza": "mNcR", "value": 400}, 33 | "H": {"betza": "KN", "value": 700}, 34 | "A": {"betza": "mNcB", "value": 400}, 35 | "F": {"betza": "mQcN", "value": 500} 36 | }, 37 | "castlingOptions": {"enabled": false}, 38 | "promotionOptions": "bishop.promo.standard", 39 | "materialConditions": {"enabled": false}, 40 | "startPosition": "lhafkahl/8/pppppppp/8/8/PPPPPPPP/8/LHAFKAHL w - - 0 1", 41 | "enPassant": true, 42 | "halfMoveDraw": 100, 43 | "repetitionDraw": 3, 44 | "regions": { 45 | "whiteCamp": {"b": 0, "t": 0}, 46 | "blackCamp": {"b": 7, "t": 7} 47 | } 48 | } -------------------------------------------------------------------------------- /example/json/dart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dart", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "6x6", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfcF", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "noSanSymbol": true, 14 | "value": 100 15 | }, 16 | "N": {"betza": "N", "value": 300}, 17 | "B": {"betza": "B", "value": 300}, 18 | "R": {"betza": "R", "value": 500}, 19 | "K": { 20 | "betza": "K", 21 | "royal": true, 22 | "promoOptions": { 23 | "canPromote": false, 24 | "canPromoteTo": false 25 | } 26 | }, 27 | "X": { 28 | "betza": "", 29 | "promoOptions": { 30 | "canPromote": false, 31 | "canPromoteTo": false 32 | }, 33 | "value": 0, 34 | "actions": ["bishop.action.immortality"] 35 | } 36 | }, 37 | "castlingOptions": {"enabled": false}, 38 | "promotionOptions": "bishop.promo.standard", 39 | "materialConditions": {"enabled": false}, 40 | "startPosition": "knrppp/nbp3/rp3P/p3PR/3PBN/PPPRNK[XXXxxx] w - - 0 1", 41 | "enPassant": false, 42 | "halfMoveDraw": 100, 43 | "handOptions": { 44 | "enableHands": true, 45 | "addCapturesToHand": false, 46 | "dropBuilder": { 47 | "id": "bishop.drops.region", 48 | "region": {"b": 1, "t": 4, "l": 1, "r": 4} 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /example/json/nano.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nano Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "4x5", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 2, 33 | "qTarget": 1, 34 | "fixedRooks": true, 35 | "kRook": 3, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "knbr/p3/4/3P/RBNK w Qk - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3 52 | } -------------------------------------------------------------------------------- /example/json/chess.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3 52 | } -------------------------------------------------------------------------------- /example/json/legan.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Legan Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "mlfFcflW", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "noSanSymbol": true 14 | }, 15 | "N": {"betza": "N", "value": 300}, 16 | "B": {"betza": "B", "value": 300}, 17 | "R": {"betza": "R", "value": 500}, 18 | "Q": {"betza": "Q", "value": 900}, 19 | "K": { 20 | "betza": "K", 21 | "royal": true, 22 | "promoOptions": { 23 | "canPromote": false, 24 | "canPromoteTo": false 25 | } 26 | } 27 | }, 28 | "castlingOptions": {"enabled": false}, 29 | "promotionOptions": { 30 | "id": "bishop.promo.region", 31 | "wId": "wp", 32 | "bId": "bp" 33 | }, 34 | "materialConditions": {"enabled": false}, 35 | "startPosition": "knbrp3/bqpp4/npp5/rp1p3P/p3P1PR/5PPN/4PPQB/3PRBNK w - - 0 1", 36 | "enPassant": false, 37 | "halfMoveDraw": 100, 38 | "regions": { 39 | "wp": { 40 | "type": "union", 41 | "regions": [ 42 | {"b": 4, "t": 7, "l": 0, "r": 0}, 43 | {"b": 7, "t": 7, "l": 0, "r": 3} 44 | ] 45 | }, 46 | "bp": { 47 | "type": "union", 48 | "regions": [ 49 | {"b": 0, "t": 3, "l": 7, "r": 7}, 50 | {"b": 0, "t": 0, "l": 4, "r": 7} 51 | ] 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /example/json/berolina.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Berolina Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmFfceWifmnA", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 125 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3 52 | } -------------------------------------------------------------------------------- /example/json/threeKings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Three Kings Chess", 3 | "description": "Each player has three kings, but only one has to be captured for them to win.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "promoOptions": { 24 | "canPromote": false, 25 | "canPromoteTo": false 26 | } 27 | } 28 | }, 29 | "castlingOptions": {"enabled": false}, 30 | "promotionOptions": "bishop.promo.standard", 31 | "materialConditions": { 32 | "enabled": true, 33 | "soloMaters": ["P", "Q", "R", "A", "C"], 34 | "pairMaters": ["N"], 35 | "combinedPairMaters": ["B"], 36 | "specialCases": [["B", "N"]] 37 | }, 38 | "startPosition": "knbqkbnk/pppppppp/8/8/8/8/PPPPPPPP/KNBQKBNK w - - 0 1", 39 | "enPassant": true, 40 | "halfMoveDraw": 100, 41 | "repetitionDraw": 3, 42 | "actions": [ 43 | { 44 | "id": "bishop.action.checkPieceCount", 45 | "pieceType": "K", 46 | "count": 3 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /example/json/domination.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Domination", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3, 52 | "actions": [] 53 | } -------------------------------------------------------------------------------- /example/json/wolf.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wolf Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N0", "value": 500}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "W": {"betza": "RN", "value": 900}, 30 | "F": {"betza": "BN", "value": 900}, 31 | "S": { 32 | "betza": "fKifmnD", 33 | "promoOptions": { 34 | "canPromote": true, 35 | "canPromoteTo": false, 36 | "promotesTo": [ 37 | "B", 38 | "N", 39 | "R", 40 | "Q", 41 | "W", 42 | "F" 43 | ] 44 | }, 45 | "enPassantable": true 46 | }, 47 | "E": {"betza": "QN0", "value": 1400} 48 | }, 49 | "castlingOptions": {"enabled": false}, 50 | "promotionOptions": "bishop.promo.standard", 51 | "materialConditions": {"enabled": false}, 52 | "startPosition": "qwfrbbnk/pssppssp/1pp2pp1/8/8/8/8/1PP2PP1/PSSPPSSP/KNBBRFWQ w - - 0 1", 53 | "enPassant": true 54 | } -------------------------------------------------------------------------------- /example/json/spawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spawn Chess", 3 | "description": "Moving the exposed king adds a pawn to the player's hand.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B2"}, 19 | "R": {"betza": "R3"}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | }, 28 | "actions": [ 29 | { 30 | "id": "bishop.action.addToHand", 31 | "piece": "P" 32 | } 33 | ] 34 | } 35 | }, 36 | "castlingOptions": {"enabled": false}, 37 | "promotionOptions": "bishop.promo.standard", 38 | "materialConditions": { 39 | "enabled": true, 40 | "soloMaters": ["P", "Q", "R", "A", "C"], 41 | "pairMaters": ["N"], 42 | "combinedPairMaters": ["B"], 43 | "specialCases": [["B", "N"]] 44 | }, 45 | "startPosition": "rnb1nbnr/8/3k4/8/8/4K3/8/RNBN1BNR[PPpp] w - - 0 1", 46 | "enPassant": true, 47 | "halfMoveDraw": 100, 48 | "repetitionDraw": 3, 49 | "handOptions": { 50 | "enableHands": true, 51 | "addCapturesToHand": false 52 | } 53 | } -------------------------------------------------------------------------------- /example/json/dobutsu.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dobutsu Shogi", 3 | "description": "A simple Shogi variant aimed at children.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "3x4", 6 | "pieceTypes": { 7 | "L": { 8 | "betza": "K", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "blackCamp", 17 | "blackRegion": "whiteCamp", 18 | "winGame": true 19 | } 20 | ] 21 | }, 22 | "G": { 23 | "betza": "W", 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "E": { 30 | "betza": "F", 31 | "promoOptions": { 32 | "canPromote": false, 33 | "canPromoteTo": false 34 | } 35 | }, 36 | "C": { 37 | "betza": "fW", 38 | "promoOptions": { 39 | "canPromote": true, 40 | "canPromoteTo": false 41 | } 42 | }, 43 | "H": {"betza": "WfF"} 44 | }, 45 | "castlingOptions": {"enabled": false}, 46 | "promotionOptions": "bishop.promo.standard", 47 | "materialConditions": {"enabled": false}, 48 | "startPosition": "gle/1c1/1C1/ELG[-] w - - 0 1", 49 | "enPassant": false, 50 | "handOptions": { 51 | "enableHands": true, 52 | "addCapturesToHand": true, 53 | "dropBuilder": "bishop.drops.unrestricted" 54 | }, 55 | "regions": { 56 | "whiteCamp": {"b": 0, "t": 0}, 57 | "blackCamp": {"b": 3, "t": 3} 58 | } 59 | } -------------------------------------------------------------------------------- /example/json/shako.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shako", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "E": {"betza": "FA"}, 30 | "C": {"betza": "mRcpR", "value": 450} 31 | }, 32 | "castlingOptions": { 33 | "enabled": true, 34 | "kTarget": 7, 35 | "qTarget": 3, 36 | "fixedRooks": true, 37 | "kRook": 8, 38 | "qRook": 1, 39 | "rookPiece": "R", 40 | "useRookAsTarget": false 41 | }, 42 | "promotionOptions": "bishop.promo.standard", 43 | "materialConditions": { 44 | "enabled": true, 45 | "soloMaters": ["P", "Q", "R", "A", "C"], 46 | "pairMaters": ["N"], 47 | "combinedPairMaters": ["B"], 48 | "specialCases": [["B", "N"]] 49 | }, 50 | "startPosition": "c8c/ernbqkbnre/pppppppppp/10/10/10/10/PPPPPPPPPP/ERNBQKBNRE/C8C w KQkq - 0 1", 51 | "enPassant": true, 52 | "halfMoveDraw": 100, 53 | "repetitionDraw": 3 54 | } -------------------------------------------------------------------------------- /example/json/crazyhouse.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Crazyhouse", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3, 52 | "handOptions": { 53 | "enableHands": true, 54 | "addCapturesToHand": true 55 | } 56 | } -------------------------------------------------------------------------------- /lib/src/regions/intersect_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | /// A region that contains the intersection of [regions], i.e. all squares that 4 | /// are in all of the [regions]. 5 | class IntersectRegion extends BoardRegion { 6 | final List regions; 7 | 8 | const IntersectRegion(this.regions); 9 | 10 | factory IntersectRegion.fromJson(Map json) => 11 | IntersectRegion( 12 | (json['regions'] as List).map((e) => BoardRegion.fromJson(e)).toList(), 13 | ); 14 | 15 | @override 16 | bool contains(int file, int rank) => 17 | regions.firstWhereOrNull((e) => !e.contains(file, rank)) == null; 18 | 19 | @override 20 | Set squares(BoardSize size) => regions.fold>( 21 | regions.first.squares(size).toSet(), 22 | (a, b) => a.intersection(b.squares(size).toSet()), 23 | ); 24 | 25 | @override 26 | IntersectRegion translate(int x, int y) => 27 | IntersectRegion(regions.map((e) => e.translate(x, y)).toList()); 28 | 29 | @override 30 | Map toJson() => { 31 | 'type': 'intersect', 32 | 'regions': [...regions.map((e) => e.toJson())], 33 | }; 34 | 35 | @override 36 | String toString() => 'Intersection(' 37 | '${regions.map((e) => e.toString()).join(', ')})'; 38 | 39 | @override 40 | IntersectRegion operator &(BoardRegion other) => 41 | IntersectRegion([...regions, other]); 42 | } 43 | -------------------------------------------------------------------------------- /example/json/grand.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grand Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "C": {"betza": "RN", "value": 900}, 30 | "A": {"betza": "BN", "value": 900} 31 | }, 32 | "castlingOptions": {"enabled": false}, 33 | "promotionOptions": { 34 | "id": "bishop.promo.optional", 35 | "pieceLimits": { 36 | "Q": 1, 37 | "A": 1, 38 | "C": 1, 39 | "R": 2, 40 | "N": 2, 41 | "B": 2 42 | }, 43 | "ranks": [7, 2] 44 | }, 45 | "materialConditions": { 46 | "enabled": true, 47 | "soloMaters": ["P", "Q", "R", "A", "C"], 48 | "pairMaters": ["N"], 49 | "combinedPairMaters": ["B"], 50 | "specialCases": [["B", "N"]] 51 | }, 52 | "startPosition": "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R w - - 0 1", 53 | "enPassant": true, 54 | "halfMoveDraw": 100, 55 | "repetitionDraw": 3 56 | } -------------------------------------------------------------------------------- /example/json/pocketKnight.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pocket Knight", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[Nn] w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3, 52 | "handOptions": { 53 | "enableHands": true, 54 | "addCapturesToHand": false 55 | } 56 | } -------------------------------------------------------------------------------- /example/json/capablanca.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Capablanca Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "A": {"betza": "BN", "value": 900}, 30 | "C": {"betza": "RN", "value": 900} 31 | }, 32 | "castlingOptions": { 33 | "enabled": true, 34 | "kTarget": 8, 35 | "qTarget": 2, 36 | "fixedRooks": true, 37 | "kRook": 9, 38 | "qRook": 0, 39 | "rookPiece": "R", 40 | "useRookAsTarget": false 41 | }, 42 | "promotionOptions": "bishop.promo.standard", 43 | "materialConditions": { 44 | "enabled": true, 45 | "soloMaters": ["P", "Q", "R", "A", "C"], 46 | "pairMaters": ["N"], 47 | "combinedPairMaters": ["B"], 48 | "specialCases": [["B", "N"]] 49 | }, 50 | "startPosition": "rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR w KQkq - 0 1", 51 | "enPassant": true, 52 | "halfMoveDraw": 100, 53 | "repetitionDraw": 3 54 | } -------------------------------------------------------------------------------- /example/json/omega.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Omega Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "12x12", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmW3", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "C": {"betza": "DAW", "value": 400}, 30 | "W": {"betza": "FC", "value": 400}, 31 | "*": { 32 | "betza": "", 33 | "promoOptions": { 34 | "canPromote": false, 35 | "canPromoteTo": false 36 | }, 37 | "value": 0, 38 | "actions": ["bishop.action.immortality"] 39 | } 40 | }, 41 | "castlingOptions": { 42 | "enabled": true, 43 | "kTarget": 8, 44 | "qTarget": 4, 45 | "fixedRooks": true, 46 | "kRook": 9, 47 | "qRook": 2, 48 | "useRookAsTarget": false 49 | }, 50 | "promotionOptions": "bishop.promo.standard", 51 | "materialConditions": {"enabled": false}, 52 | "startPosition": "w**********w/*crnbqkbnrc*/*pppppppppp*/*10*/*10*/*10*/*10*/*10*/*10*/*PPPPPPPPPP*/*CRNBQKBNRC*/W**********W w - - 0 1", 53 | "enPassant": true 54 | } -------------------------------------------------------------------------------- /lib/src/castling_rights.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'constants.dart'; 4 | 5 | typedef CastlingRights = int; 6 | 7 | extension CastlingExtension on CastlingRights { 8 | bool get wk => this & Castling.k != 0; 9 | bool get wq => this & Castling.q != 0; 10 | bool get bk => this & Castling.bk != 0; 11 | bool get bq => this & Castling.bq != 0; 12 | String get formatted => 13 | '${this == 0 ? '-' : ''}${wk ? 'K' : ''}${wq ? 'Q' : ''}${bk ? 'k' : ''}${bq ? 'q' : ''}'; 14 | 15 | CastlingRights flip(int right) => this ^ right; 16 | CastlingRights remove(Colour colour) => 17 | this & (colour == Bishop.white ? Castling.whiteMask : Castling.blackMask); 18 | bool hasRight(int right) => this & right != 0; 19 | } 20 | 21 | CastlingRights castlingRights(String crString) { 22 | CastlingRights cr = 0; 23 | Castling.symbols.forEach((k, v) { 24 | if (crString.contains(k)) cr += v; 25 | }); 26 | return cr; 27 | } 28 | 29 | class Castling { 30 | static const int k = 1; 31 | static const int q = 2; 32 | static const int black = 4; 33 | static const int bk = 4; 34 | static const int bq = 8; 35 | static const int whiteMask = 12; 36 | static const int blackMask = 3; 37 | static const int mask = 15; 38 | static const int bothK = 5; 39 | static const int bothQ = 10; 40 | static const Map symbols = { 41 | 'K': k, 42 | 'Q': q, 43 | 'k': bk, 44 | 'q': bq, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /example/json/threeCheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Three Check", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "gameEndConditions": { 49 | "stalemate": "draw", 50 | "elimination": "lose", 51 | "checkLimit": 3 52 | }, 53 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 54 | "enPassant": true, 55 | "halfMoveDraw": 100, 56 | "repetitionDraw": 3 57 | } -------------------------------------------------------------------------------- /lib/src/variant/variants/musketeer.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | class Musketeer { 4 | static const String defaultFen = 5 | '8/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/8 w KQkq - 0 1'; 6 | static String fen({String whiteGate = '8', String blackGate = '8'}) => 7 | '$blackGate/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/$whiteGate w KQkq - 0 1'; 8 | static PieceType leopard() => PieceType.fromBetza('F2N'); 9 | static PieceType hawk() => PieceType.fromBetza('ADGH'); 10 | static PieceType unicorn() => PieceType.fromBetza('NC'); 11 | static PieceType spider() => PieceType.fromBetza('B2ND'); 12 | static PieceType fortress() => PieceType.fromBetza('B3vND'); 13 | static PieceType elephant() => PieceType.fromBetza('FWDA'); 14 | static PieceType cannon() => PieceType.fromBetza('FWDsN'); 15 | 16 | static Variant variant() { 17 | Variant standard = Variant.standard(); 18 | return standard.copyWith( 19 | name: 'Musketeer Chess', 20 | startPosition: defaultFen, 21 | gatingMode: GatingMode.fixed, 22 | pieceTypes: { 23 | ...standard.pieceTypes, 24 | 'A': PieceType.archbishop(), 25 | 'C': PieceType.chancellor(), 26 | 'D': PieceType.amazon(), // dragon 27 | 'L': leopard(), 28 | 'H': hawk(), 29 | 'U': unicorn(), 30 | 'S': spider(), 31 | 'F': fortress(), 32 | 'E': elephant(), 33 | 'O': cannon(), 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/json/chess960.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chess960", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": false, 35 | "rookPiece": "R", 36 | "useRookAsTarget": true 37 | }, 38 | "promotionOptions": "bishop.promo.standard", 39 | "materialConditions": { 40 | "enabled": true, 41 | "soloMaters": ["P", "Q", "R", "A", "C"], 42 | "pairMaters": ["N"], 43 | "combinedPairMaters": ["B"], 44 | "specialCases": [["B", "N"]] 45 | }, 46 | "outputOptions": { 47 | "castlingFormat": "shredder", 48 | "showPromoted": false, 49 | "virginFiles": false 50 | }, 51 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 52 | "startPosBuilder": "bishop.start.chess960", 53 | "enPassant": true, 54 | "halfMoveDraw": 100, 55 | "repetitionDraw": 3 56 | } -------------------------------------------------------------------------------- /example/json/miniRandom.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mini Random", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "6x6", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfcF", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "noSanSymbol": true, 14 | "value": 100 15 | }, 16 | "N": {"betza": "N", "value": 300}, 17 | "B": {"betza": "B", "value": 300}, 18 | "R": {"betza": "R", "value": 500}, 19 | "Q": {"betza": "Q", "value": 900}, 20 | "K": { 21 | "betza": "K", 22 | "royal": true, 23 | "promoOptions": { 24 | "canPromote": false, 25 | "canPromoteTo": false 26 | } 27 | } 28 | }, 29 | "castlingOptions": { 30 | "enabled": true, 31 | "kTarget": 4, 32 | "qTarget": 1, 33 | "fixedRooks": false, 34 | "rookPiece": "R", 35 | "useRookAsTarget": true 36 | }, 37 | "promotionOptions": "bishop.promo.standard", 38 | "materialConditions": { 39 | "enabled": true, 40 | "soloMaters": ["P", "Q", "R", "A", "C"], 41 | "pairMaters": ["N"], 42 | "combinedPairMaters": ["B"], 43 | "specialCases": [["B", "N"]] 44 | }, 45 | "outputOptions": { 46 | "castlingFormat": "shredder", 47 | "showPromoted": false, 48 | "virginFiles": false 49 | }, 50 | "startPosition": "rbnkbr/pppppp/6/6/PPPPPP/RBNKBR w KQkq - 0 1", 51 | "startPosBuilder": { 52 | "id": "bishop.start.randomChess", 53 | "size": "6x6" 54 | }, 55 | "enPassant": false, 56 | "halfMoveDraw": 100, 57 | "repetitionDraw": 3 58 | } -------------------------------------------------------------------------------- /example/json/andernach.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Andernach Chess", 3 | "description": "Capturing pieces, except for kings, change colour.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3, 52 | "actions": [ 53 | { 54 | "id": "bishop.action.transferOwnership", 55 | "quiet": false 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /example/json/atomic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Atomic Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 49 | "enPassant": true, 50 | "halfMoveDraw": 100, 51 | "repetitionDraw": 3, 52 | "actions": [ 53 | { 54 | "id": "bishop.action.explosionRadius", 55 | "radius": 1, 56 | "immunePieces": ["P"] 57 | }, 58 | "bishop.action.checkRoyalsAlive" 59 | ] 60 | } -------------------------------------------------------------------------------- /test/gating_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Gating', () { 6 | test('Flex - Simple', () { 7 | final g = Game(variant: CommonVariants.seirawan()); 8 | final m = g 9 | .generateLegalMoves() 10 | .from(g.size.squareNumber('b1')) 11 | .to(g.size.squareNumber('a3')); 12 | expect(m.length, 3); 13 | expect(m.gatingMoves.length, 2); 14 | g.makeMoveString('b1a3/h'); 15 | expect(g.state.pieceOnSquare('b1'), 'H'); 16 | expect(g.state.pieceOnSquare('a3'), 'N'); 17 | g.undo(); 18 | g.makeMoveString('b1a3'); 19 | expect(g.state.pieceOnSquare('b1'), '.'); 20 | expect(g.state.pieceOnSquare('a3'), 'N'); 21 | }); 22 | }); 23 | test('Flex - Castling', () { 24 | final g = Game( 25 | variant: CommonVariants.seirawan(), 26 | fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R[HEhe] w ' 27 | 'KQkqABCDEFGHabcdefgh - 0 1', 28 | ); 29 | final m = 30 | g.generateLegalMoves().from(g.size.squareNumber('e1')).castlingMoves; 31 | expect(m.length, 5); 32 | g.makeMoveString('e1g1/ee1'); 33 | expect( 34 | ['e1', 'f1', 'g1', 'h1'].map((e) => g.state.pieceOnSquare(e)), 35 | orderedEquals(['E', 'R', 'K', '.']), 36 | ); 37 | g.undo(); 38 | g.makeMoveString('e1g1/hh1'); 39 | expect( 40 | ['e1', 'f1', 'g1', 'h1'].map((e) => g.state.pieceOnSquare(e)), 41 | orderedEquals(['.', 'R', 'K', 'H']), 42 | ); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /example/json/koth.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "King of the Hill", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | }, 28 | "regionEffects": [ 29 | { 30 | "whiteRegion": "hill", 31 | "blackRegion": "hill", 32 | "winGame": true 33 | } 34 | ] 35 | } 36 | }, 37 | "castlingOptions": { 38 | "enabled": true, 39 | "kTarget": 6, 40 | "qTarget": 2, 41 | "fixedRooks": true, 42 | "kRook": 7, 43 | "qRook": 0, 44 | "rookPiece": "R", 45 | "useRookAsTarget": false 46 | }, 47 | "promotionOptions": "bishop.promo.standard", 48 | "materialConditions": { 49 | "enabled": true, 50 | "soloMaters": ["P", "Q", "R", "A", "C"], 51 | "pairMaters": ["N"], 52 | "combinedPairMaters": ["B"], 53 | "specialCases": [["B", "N"]] 54 | }, 55 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 56 | "enPassant": true, 57 | "halfMoveDraw": 100, 58 | "repetitionDraw": 3, 59 | "regions": { 60 | "hill": {"b": 3, "t": 4, "l": 3, "r": 4} 61 | } 62 | } -------------------------------------------------------------------------------- /example/json/opulent.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Opulent Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "NW", "value": 400}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "C": {"betza": "RN", "value": 900}, 30 | "A": {"betza": "BN", "value": 900}, 31 | "W": {"betza": "FC", "value": 400}, 32 | "L": {"betza": "HFD", "value": 400} 33 | }, 34 | "castlingOptions": {"enabled": false}, 35 | "promotionOptions": { 36 | "id": "bishop.promo.optional", 37 | "pieceLimits": { 38 | "Q": 1, 39 | "A": 1, 40 | "C": 1, 41 | "R": 2, 42 | "N": 2, 43 | "B": 2, 44 | "W": 2, 45 | "L": 2 46 | }, 47 | "ranks": [7, 2] 48 | }, 49 | "materialConditions": { 50 | "enabled": true, 51 | "soloMaters": ["P", "Q", "R", "A", "C"], 52 | "pairMaters": ["N"], 53 | "combinedPairMaters": ["B"], 54 | "specialCases": [["B", "N"]] 55 | }, 56 | "startPosition": "rw6wr/clbnqknbla/pppppppppp/10/10/10/10/PPPPPPPPPP/CLBNQKNBLA/RW6WR w - - 0 1", 57 | "enPassant": true, 58 | "halfMoveDraw": 100, 59 | "repetitionDraw": 3 60 | } -------------------------------------------------------------------------------- /lib/src/variant/variants/small.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// Variants of chess played on smaller boards. 4 | class SmallVariants { 5 | static Variant mini() { 6 | Variant standard = Variant.standard(); 7 | return standard 8 | .copyWith( 9 | name: 'Mini Chess', 10 | boardSize: BoardSize.mini, 11 | startPosition: 'rbnkbr/pppppp/6/6/PPPPPP/RBNKBR w KQkq - 0 1', 12 | castlingOptions: CastlingOptions.mini, 13 | enPassant: false, 14 | ) 15 | .withPiece('P', PieceType.simplePawn()); 16 | } 17 | 18 | static Variant miniRandom() { 19 | Variant mini = Variant.mini(); 20 | return mini.copyWith( 21 | name: 'Mini Random', 22 | startPosBuilder: const RandomChessStartPosBuilder(size: BoardSize.mini), 23 | castlingOptions: CastlingOptions.miniRandom, 24 | outputOptions: OutputOptions.chess960, 25 | ); 26 | } 27 | 28 | static Variant micro() { 29 | Variant standard = Variant.standard(); 30 | return standard.copyWith( 31 | name: 'Micro Chess', 32 | boardSize: const BoardSize(5, 5), 33 | startPosition: 'rnbqk/ppppp/5/PPPPP/RNBQK w Qq - 0 1', 34 | castlingOptions: CastlingOptions.micro, 35 | ); 36 | } 37 | 38 | static Variant nano() { 39 | Variant standard = Variant.standard(); 40 | return standard.copyWith( 41 | name: 'Nano Chess', 42 | boardSize: const BoardSize(4, 5), 43 | startPosition: 'knbr/p3/4/3P/RBNK w Qk - 0 1', 44 | castlingOptions: CastlingOptions.nano, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/json/horde.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Horde Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | } 29 | }, 30 | "castlingOptions": { 31 | "enabled": true, 32 | "kTarget": 6, 33 | "qTarget": 2, 34 | "fixedRooks": true, 35 | "kRook": 7, 36 | "qRook": 0, 37 | "rookPiece": "R", 38 | "useRookAsTarget": false 39 | }, 40 | "promotionOptions": "bishop.promo.standard", 41 | "materialConditions": { 42 | "enabled": true, 43 | "soloMaters": ["P", "Q", "R", "A", "C"], 44 | "pairMaters": ["N"], 45 | "combinedPairMaters": ["B"], 46 | "specialCases": [["B", "N"]] 47 | }, 48 | "startPosition": "rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1", 49 | "enPassant": true, 50 | "firstMoveOptions": { 51 | "id": "bishop.first.pair", 52 | "white": { 53 | "id": "bishop.first.ranks", 54 | "ranks": [0, 1] 55 | }, 56 | "black": { 57 | "id": "bishop.first.ranks", 58 | "ranks": [6, 7] 59 | } 60 | }, 61 | "halfMoveDraw": 100, 62 | "repetitionDraw": 3 63 | } -------------------------------------------------------------------------------- /example/json/seirawan.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Seirawan Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "H": {"betza": "BN", "value": 900}, 30 | "E": {"betza": "RN", "value": 900} 31 | }, 32 | "castlingOptions": { 33 | "enabled": true, 34 | "kTarget": 6, 35 | "qTarget": 2, 36 | "fixedRooks": true, 37 | "kRook": 7, 38 | "qRook": 0, 39 | "rookPiece": "R", 40 | "useRookAsTarget": false 41 | }, 42 | "promotionOptions": "bishop.promo.standard", 43 | "materialConditions": { 44 | "enabled": true, 45 | "soloMaters": ["P", "Q", "R", "E", "H"], 46 | "pairMaters": ["N"], 47 | "combinedPairMaters": ["B"], 48 | "specialCases": [["B", "N"]] 49 | }, 50 | "outputOptions": { 51 | "castlingFormat": "standard", 52 | "showPromoted": false, 53 | "virginFiles": true 54 | }, 55 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[HEhe] w KQkqABCDEFGHabcdefgh - 0 1", 56 | "enPassant": true, 57 | "halfMoveDraw": 100, 58 | "repetitionDraw": 3, 59 | "gatingMode": "flex" 60 | } -------------------------------------------------------------------------------- /test/promo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/expect.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | void main() { 6 | group('Promotions', () { 7 | final grand = Variant.grand(); 8 | test('Grand - optional, all promos available', () { 9 | Game g = Game( 10 | variant: grand, 11 | fen: '10/10/10/2k4P2/10/10/3K6/10/10/10 w - - 0 1', 12 | ); 13 | final m = g.generateLegalMoves().from(grand.boardSize.squareNumber('h7')); 14 | expect(m.length, 7); 15 | }); 16 | test('Grand - optional, no queen or bishop', () { 17 | Game g = Game( 18 | variant: grand, 19 | fen: '10/10/10/2k4P2/10/10/3K6/3QBBN3/10/10 w - - 0 1', 20 | ); 21 | final m = g.generateLegalMoves().from(grand.boardSize.squareNumber('h7')); 22 | expect(m.length, 5); 23 | }); 24 | test('Grand - forced, no queen or bishop', () { 25 | Game g = Game( 26 | variant: grand, 27 | fen: '10/7P2/10/2k7/10/10/3K6/3QBBN3/10/10 w - - 0 1', 28 | ); 29 | final m = g.generateLegalMoves().from(grand.boardSize.squareNumber('h9')); 30 | expect(m.length, 4); 31 | }); 32 | test('Internal type (shogi)', () { 33 | final g = Game( 34 | variant: Shogi.shogi(), 35 | fen: '3gk2g1/9/1N7/9/9/9/9/9/1N2K3L[] w - - 0 1', 36 | ); 37 | g.makeMoveString('i1i9g'); 38 | g.makeMoveString('h9i9'); 39 | g.makeMoveString('b7c9g'); 40 | g.makeMoveString('d9c9'); 41 | expect(g.handSymbols[Bishop.black], unorderedEquals(['n', 'l'])); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/actions/actions/points.dart: -------------------------------------------------------------------------------- 1 | part of '../base_actions.dart'; 2 | 3 | class ActionPointsEnding extends Action { 4 | final List stateVariables; 5 | final List limits; 6 | 7 | ActionPointsEnding({ 8 | required this.limits, 9 | this.stateVariables = const [0, 1], 10 | super.event = ActionEvent.afterMove, 11 | super.precondition, 12 | super.condition, 13 | }) : assert(limits.length == 2), 14 | super( 15 | action: (trigger) { 16 | int whitePts = trigger.getCustomState(stateVariables.first); 17 | int blackPts = trigger.getCustomState(stateVariables.last); 18 | bool white = whitePts >= limits.first; 19 | bool black = blackPts >= limits.last; 20 | if (!white && !black) return []; 21 | if (white ^ black) { 22 | return [ 23 | EffectSetGameResult( 24 | WonGamePoints( 25 | winner: white ? Bishop.white : Bishop.black, 26 | points: white ? whitePts : blackPts, 27 | ), 28 | ), 29 | ]; 30 | } 31 | bool whiteWins = whitePts > blackPts; 32 | return [ 33 | EffectSetGameResult( 34 | whitePts == blackPts 35 | ? DrawnGamePoints(whitePts) 36 | : WonGamePoints( 37 | winner: whiteWins ? Bishop.white : Bishop.black, 38 | points: whiteWins ? whitePts : blackPts, 39 | ), 40 | ), 41 | ]; 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /test/castling_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Castling', () { 6 | test('Don\'t allow castling with piece between rook and dest', () { 7 | final g = Game( 8 | fen: 9 | '1r2k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/5Q1p/PPPBBPPP/RN2K2R w KQk - 2 2', 10 | ); 11 | expect(g.generateLegalMoves().castlingMoves.length, 1); 12 | }); 13 | // https://github.com/alexobviously/bishop/issues/72 14 | test('Chess960 K-C1:R-A1 castling', () { 15 | final g = Game( 16 | variant: CommonVariants.chess960(), 17 | fen: 'rnkqbbrn/pppppppp/8/8/8/8/PPPPPPPP/R1K1BBRN w AGag - 0 1', 18 | ); 19 | final moves = g.generateLegalMoves(); 20 | expect(moves.castlingMoves.length, 1); 21 | expect(moves.from(g.size.squareNumber('c1')).length, 3); 22 | }); 23 | // https://github.com/alexobviously/bishop/issues/77 24 | test('Chess960 K-B1:R-C1 castling', () { 25 | final g = Game( 26 | variant: CommonVariants.chess960(), 27 | fen: 'rkr4n/ppppqbpp/3bpp1n/8/8/2BPPN2/PPPQBPPP/RKR4N w KQkq - 6 7', 28 | ); 29 | final moves = g.generateLegalMoves(); 30 | expect(moves.castlingMoves.length, 1); 31 | expect(moves.from(g.size.squareNumber('b1')).length, 1); 32 | }); 33 | test('Castling rights when a rook takes a rook', () { 34 | final g = Game( 35 | fen: 'rnbqk1nr/ppp1ppb1/6p1/3p4/8/2N5/PPPPPPP1/R1BQKBNR w KQkq - 0 5', 36 | ); 37 | g.makeMoveString('h1h8'); 38 | expect(g.state.castlingRights, castlingRights('Qq')); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/actions/trigger.dart: -------------------------------------------------------------------------------- 1 | part of 'actions.dart'; 2 | 3 | /// A class that contains all the data relevant to the action being triggered. 4 | class ActionTrigger { 5 | /// The event type triggering the action. 6 | final ActionEvent event; 7 | 8 | /// The state of the game at the time of the trigger. If the event is not 9 | /// `ActionEvent.beforeMove`, then this will reflect the state after the 10 | /// basic parts of the move have been made. This includes the turn. 11 | final BishopState state; 12 | 13 | /// The variant being played. 14 | final BuiltVariant variant; 15 | 16 | /// The move that is being made to trigger this action. 17 | final Move move; 18 | 19 | /// The piece moved in the last move, for convenience. 20 | final int piece; 21 | 22 | const ActionTrigger({ 23 | required this.event, 24 | required this.variant, 25 | required this.state, 26 | required this.move, 27 | required this.piece, 28 | }); 29 | 30 | ActionTrigger copyWith({ 31 | ActionEvent? event, 32 | BishopState? state, 33 | BuiltVariant? variant, 34 | Move? move, 35 | int? piece, 36 | }) => 37 | ActionTrigger( 38 | event: event ?? this.event, 39 | variant: variant ?? this.variant, 40 | state: state ?? this.state, 41 | move: move ?? this.move, 42 | piece: piece ?? this.piece, 43 | ); 44 | 45 | BoardSize get size => variant.boardSize; 46 | List get board => state.board; 47 | 48 | /// Retrieves the value of the custom state variable [i]. 49 | int getCustomState(int i) => 50 | state.board[variant.boardSize.secretSquare(i)] >> Bishop.flagsStartBit; 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/regions/set_region.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | class DirectionSetRegion extends BoardRegion { 4 | final Iterable directions; 5 | const DirectionSetRegion(this.directions); 6 | 7 | factory DirectionSetRegion.fromJson(Map json) => 8 | DirectionSetRegion( 9 | (json['squares'] as List) 10 | .map((e) => Direction.fromString(e)) 11 | .toSet(), 12 | ); 13 | 14 | @override 15 | bool contains(int file, int rank) => 16 | directions.contains(Direction(file, rank)); 17 | 18 | @override 19 | Iterable squares(BoardSize size) => 20 | directions.map((e) => size.square(e.h, e.v)); 21 | 22 | @override 23 | Map toJson() => { 24 | 'type': 'dset', 25 | 'squares': directions.map((e) => e.simpleString).toList(), 26 | }; 27 | 28 | @override 29 | BoardRegion translate(int x, int y) => 30 | DirectionSetRegion(directions.map((e) => e.translate(x, y)).toSet()); 31 | 32 | @override 33 | String toString() => 'DirectionSetRegion($directions)'; 34 | } 35 | 36 | class SetRegion extends DirectionSetRegion { 37 | final Iterable squareNames; 38 | SetRegion(this.squareNames) 39 | : super(squareNames.map((e) => Direction.fromSquareName(e)).toSet()); 40 | 41 | factory SetRegion.fromJson(Map json) => 42 | SetRegion((json['squares'] as List).cast().toSet()); 43 | 44 | @override 45 | Map toJson() => { 46 | 'type': 'set', 47 | 'squares': squareNames.toList(), 48 | }; 49 | 50 | @override 51 | String toString() => 'SetRegion($squareNames)'; 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/game/game_utils.dart: -------------------------------------------------------------------------------- 1 | part of 'game.dart'; 2 | 3 | extension GameUtils on Game { 4 | /// Performs a [divide perft test](https://www.chessprogramming.org/Perft#Divide), to [depth]. 5 | Map divide(int depth) { 6 | List moves = generateLegalMoves(); 7 | Map perfts = {}; 8 | for (Move m in moves) { 9 | makeMove(m, false); 10 | perfts[toAlgebraic(m)] = perft(depth - 1); 11 | undo(); 12 | } 13 | return perfts; 14 | } 15 | 16 | /// Returns a naive material evaluation of the current position, from the perspective of [player]. 17 | /// Return value is in [centipawns](https://www.chessprogramming.org/Centipawns). 18 | /// For example, if white has captured a rook from black with no compensation, this will return +500. 19 | int evaluate(Colour player) { 20 | int eval = 0; 21 | for (int i = 0; i < size.numIndices; i++) { 22 | if (!size.onBoard(i)) continue; 23 | Square square = board[i]; 24 | if (square.isNotEmpty) { 25 | Colour colour = square.colour; 26 | int type = square.type; 27 | int value = variant.pieces[type].value; 28 | if (colour == player) { 29 | eval += value; 30 | } else { 31 | eval -= value; 32 | } 33 | } 34 | } 35 | if (variant.handsEnabled) { 36 | eval += state.hands![player].fold( 37 | 0, 38 | (p, e) => p + variant.pieces[e].value + Bishop.handBonusValue, 39 | ); 40 | eval -= state.hands![player.opponent].fold( 41 | 0, 42 | (p, e) => p + variant.pieces[e].value + Bishop.handBonusValue, 43 | ); 44 | } 45 | return eval; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/variant/variants/asymmetric.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | class Orda { 4 | static PieceType kheshig() => PieceType.fromBetza('KN', value: 700); 5 | static Variant orda() => Variant.standard().copyWith( 6 | name: 'Orda', 7 | startPosition: 'lhaykahl/8/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1', 8 | materialConditions: MaterialConditions.none, 9 | pieceTypes: { 10 | 'K': PieceType.king(), 11 | 'P': PieceType.pawn(), 12 | 'N': PieceType.knight().withNoPromotion(), 13 | 'B': PieceType.bishop().withNoPromotion(), 14 | 'R': PieceType.rook().withNoPromotion(), 15 | 'Q': PieceType.queen(), 16 | 'L': PieceType.kniroo().withNoPromotion(), // Lancer 17 | 'H': kheshig(), 18 | 'A': PieceType.knibis().withNoPromotion(), // Archer 19 | 'Y': Shogi.silver().withNoPromotion(), // Yurt 20 | }, 21 | // TODO: set asymmetric castling options when available 22 | // for now, the fen covers it though 23 | ).withCampMate(); 24 | 25 | static Variant ordaMirror() => Variant.standard().copyWith( 26 | name: 'Orda Mirror', 27 | startPosition: 'lhafkahl/8/pppppppp/8/8/PPPPPPPP/8/LHAFKAHL w - - 0 1', 28 | castlingOptions: CastlingOptions.none, 29 | materialConditions: MaterialConditions.none, 30 | pieceTypes: { 31 | 'K': PieceType.king(), 32 | 'P': PieceType.pawn(), 33 | 'L': PieceType.kniroo(), // Lancer 34 | 'H': kheshig(), 35 | 'A': PieceType.knibis(), // Archer 36 | 'F': PieceType.fromBetza('mQcN', value: 500), // Falcon 37 | }, 38 | ).withCampMate(); 39 | } 40 | -------------------------------------------------------------------------------- /example/json/musketeer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Musketeer Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "A": {"betza": "BN", "value": 900}, 30 | "C": {"betza": "RN", "value": 900}, 31 | "D": {"betza": "QN", "value": 1200}, 32 | "L": {"betza": "F2N"}, 33 | "H": {"betza": "ADGH"}, 34 | "U": {"betza": "NC"}, 35 | "S": {"betza": "B2ND"}, 36 | "F": {"betza": "B3vND"}, 37 | "E": {"betza": "FWDA"}, 38 | "O": {"betza": "FWDsN"} 39 | }, 40 | "castlingOptions": { 41 | "enabled": true, 42 | "kTarget": 6, 43 | "qTarget": 2, 44 | "fixedRooks": true, 45 | "kRook": 7, 46 | "qRook": 0, 47 | "rookPiece": "R", 48 | "useRookAsTarget": false 49 | }, 50 | "promotionOptions": "bishop.promo.standard", 51 | "materialConditions": { 52 | "enabled": true, 53 | "soloMaters": ["P", "Q", "R", "A", "C"], 54 | "pairMaters": ["N"], 55 | "combinedPairMaters": ["B"], 56 | "specialCases": [["B", "N"]] 57 | }, 58 | "startPosition": "8/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/8 w KQkq - 0 1", 59 | "enPassant": true, 60 | "halfMoveDraw": 100, 61 | "repetitionDraw": 3, 62 | "gatingMode": "fixed" 63 | } -------------------------------------------------------------------------------- /lib/src/serialisation/drop_adapters.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | class StandardDropAdapter extends BasicAdapter { 4 | const StandardDropAdapter() 5 | : super('bishop.drops.standard', StandardDropBuilder.new); 6 | } 7 | 8 | class UnrestrictedDropAdapter extends BasicAdapter { 9 | const UnrestrictedDropAdapter() 10 | : super('bishop.drops.unrestricted', UnrestrictedDropBuilder.new); 11 | } 12 | 13 | class RegionDropAdapter extends BishopTypeAdapter { 14 | @override 15 | String get id => 'bishop.drops.region'; 16 | 17 | @override 18 | RegionDropBuilder build(Map? params) => RegionDropBuilder( 19 | whiteRegion: params?['wRegion'] != null || params?['region'] != null 20 | ? BoardRegion.fromJson(params?['wRegion'] ?? params?['region']) 21 | : null, 22 | blackRegion: params?['bRegion'] != null || params?['region'] != null 23 | ? BoardRegion.fromJson(params?['bRegion'] ?? params?['region']) 24 | : null, 25 | whiteId: params?['wId'], 26 | blackId: params?['bId'], 27 | ); 28 | 29 | @override 30 | Map export(RegionDropBuilder e) { 31 | bool same = e.whiteRegion == e.blackRegion && e.whiteRegion != null; 32 | return { 33 | if (same) 'region': e.whiteRegion!.toJson(), 34 | if (e.whiteRegion != null && !same) 'wRegion': e.whiteRegion!.toJson(), 35 | if (e.blackRegion != null && !same) 'bRegion': e.blackRegion!.toJson(), 36 | if (e.whiteId != null && e.whiteRegion == null) 'wId': e.whiteId, 37 | if (e.blackId != null && e.blackRegion == null) 'bId': e.blackId, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/viewer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bishop/bishop.dart'; 4 | import 'play.dart'; 5 | 6 | late final GameNavigator navigator; 7 | bool loaded = false; 8 | 9 | void main(List args) async { 10 | stdin.echoMode = false; 11 | stdin.lineMode = false; 12 | stdin.asBroadcastStream().listen(_onKey); 13 | printMagenta('Loading PGN...'); 14 | int t1 = DateTime.now().millisecondsSinceEpoch; 15 | String filename = args.first; 16 | final pgn = File(filename).readAsStringSync(); 17 | final pgnData = parsePgn(pgn); 18 | printRed(pgnData.moves.join(', ')); 19 | navigator = GameNavigator( 20 | game: pgnData.buildGame(), 21 | ); 22 | int dur = DateTime.now().millisecondsSinceEpoch - t1; 23 | printMagenta('Loaded PGN in ${dur}ms'); 24 | printYellow('Use the arrow keys or WASD to navigate. Press q to exit.'); 25 | navigator.stream.listen(_handleNavNode); 26 | _handleNavNode(navigator.current); 27 | loaded = true; 28 | } 29 | 30 | void _onKey(List charCodes) { 31 | if (charCodes.first == 113) { 32 | exit(0); 33 | } 34 | if (!loaded) return; 35 | String charStr = charCodes.join(','); 36 | if (const ['27,91,68', '65', '97'].contains(charStr)) { 37 | navigator.previous(); 38 | } 39 | if (const ['27,91,67', '68', '100'].contains(charStr)) { 40 | navigator.next(); 41 | } 42 | if (const ['27,91,65', '87', '119'].contains(charStr)) { 43 | navigator.goToEnd(); 44 | } 45 | if (const ['27,91,66', '83', '115'].contains(charStr)) { 46 | navigator.goToStart(); 47 | } 48 | } 49 | 50 | void _handleNavNode(NavigatorNode node) { 51 | print(node.gameState.ascii()); 52 | if (node.moveMeta != null) { 53 | printYellow(node.moveString!); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/json/troitzky.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Troitzky Chess", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "10x10", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": { 22 | "betza": "K", 23 | "royal": true, 24 | "promoOptions": { 25 | "canPromote": false, 26 | "canPromoteTo": false 27 | } 28 | }, 29 | "*": { 30 | "betza": "", 31 | "promoOptions": { 32 | "canPromote": false, 33 | "canPromoteTo": false 34 | }, 35 | "value": 0, 36 | "actions": ["bishop.action.immortality"] 37 | } 38 | }, 39 | "castlingOptions": {"enabled": false}, 40 | "promotionOptions": { 41 | "id": "bishop.promo.region", 42 | "wId": "wp", 43 | "bId": "bp" 44 | }, 45 | "materialConditions": {"enabled": false}, 46 | "startPosition": "****qk****/**rnbbnr**/*pppppppp*/*8*/10/10/*8*/*PPPPPPPP*/**RNBBNR**/****QK**** w - - 0 1", 47 | "enPassant": true, 48 | "regions": { 49 | "wp": { 50 | "type": "set", 51 | "squares": [ 52 | "a6", 53 | "b8", 54 | "c9", 55 | "d9", 56 | "e10", 57 | "f10", 58 | "g9", 59 | "h9", 60 | "i8", 61 | "j6" 62 | ] 63 | }, 64 | "bp": { 65 | "type": "set", 66 | "squares": [ 67 | "a5", 68 | "b3", 69 | "c2", 70 | "d2", 71 | "e1", 72 | "f1", 73 | "g2", 74 | "h2", 75 | "i3", 76 | "j5" 77 | ] 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /test/teleport_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Teleportation', () { 6 | final v = Variant( 7 | name: 'Pseudo-duck', 8 | startPosition: 'b3/4/4/3B w - - 0 1', 9 | boardSize: const BoardSize(4, 4), 10 | pieceTypes: { 11 | 'M': PieceType.fromBetza('m*'), 12 | 'C': PieceType.fromBetza('c*'), 13 | 'B': PieceType.fromBetza('*'), 14 | }, 15 | ); 16 | test('Move Definitions', () { 17 | final a = PieceType.fromBetza('*'); 18 | expect(a.moves.length, 1); 19 | expect(a.moves.first, isA()); 20 | expect(a.moves.first.modality, Modality.both); 21 | 22 | final b = PieceType.fromBetza('m*cfhN'); 23 | expect(b.moves.length, 5); 24 | expect(b.moves.first, isA()); 25 | expect(b.moves.first.modality, Modality.quiet); 26 | expect(b.moves.last, isA()); 27 | }); 28 | test('Quiet', () { 29 | final g = Game(variant: v, fen: 'bbbb/4/4/3M w - - 0 1'); 30 | final moves = g.generateLegalMoves(); 31 | expect(moves.length, 11); 32 | }); 33 | test('Capture', () { 34 | final g = Game(variant: v, fen: 'bbbb/4/4/3C w - - 0 1'); 35 | final moves = g.generateLegalMoves(); 36 | expect(moves.length, 4); 37 | }); 38 | test('Both', () { 39 | final g = Game(variant: v, fen: 'bbbb/4/4/3B w - - 0 1'); 40 | final moves = g.generateLegalMoves(); 41 | expect(moves.length, 15); 42 | }); 43 | test('Mix', () { 44 | final g = Game(variant: v, fen: 'bbbb/4/4/2CM w - - 0 1'); 45 | final moves = g.generateLegalMoves(); 46 | expect(moves.length, 14); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/actions/actions/transfer.dart: -------------------------------------------------------------------------------- 1 | part of '../base_actions.dart'; 2 | 3 | /// Transfers the ownership of the moving piece to the opponent. 4 | /// Royal pieces will not be transferred. 5 | /// The default behaviour is for this to be executed for any move, but 6 | /// [capture] or [quiet] can be set to false. 7 | class ActionTransferOwnership extends Action { 8 | final bool capture; 9 | final bool quiet; 10 | 11 | ActionTransferOwnership({ 12 | this.capture = true, 13 | this.quiet = true, 14 | super.condition, 15 | }) : super( 16 | event: ActionEvent.afterMove, 17 | precondition: Conditions.merge([ 18 | Conditions.movingPieceIsRoyal.invert(), 19 | if (capture && !quiet) Conditions.isCapture, 20 | if (quiet && !capture) Conditions.isNotCapture, 21 | ]), 22 | action: (trigger) { 23 | final piece = trigger.board[trigger.move.to]; 24 | return [EffectModifySquare(trigger.move.to, piece.flipColour())]; 25 | }, 26 | ); 27 | } 28 | 29 | class TransferOwnershipAdapter 30 | extends BishopTypeAdapter { 31 | @override 32 | String get id => 'bishop.action.transferOwnership'; 33 | 34 | @override 35 | ActionTransferOwnership build(Map? params) => 36 | ActionTransferOwnership( 37 | capture: params?['capture'] ?? true, 38 | quiet: params?['quiet'] ?? true, 39 | ); 40 | 41 | @override 42 | Map? export(ActionTransferOwnership e) { 43 | if (e.condition != null) { 44 | throw const BishopException('Unsupported export of condition'); 45 | } 46 | return { 47 | if (!e.capture) 'capture': e.capture, 48 | if (!e.quiet) 'quiet': e.quiet, 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/json/shogi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shogi", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "9x9", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "K", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | } 14 | }, 15 | "N": { 16 | "betza": "fN", 17 | "promoOptions": { 18 | "canPromote": true, 19 | "canPromoteTo": false, 20 | "promotesTo": ["G"] 21 | } 22 | }, 23 | "S": { 24 | "betza": "FfW", 25 | "promoOptions": { 26 | "canPromote": true, 27 | "canPromoteTo": false, 28 | "promotesTo": ["G"] 29 | } 30 | }, 31 | "L": { 32 | "betza": "fR", 33 | "promoOptions": { 34 | "canPromote": true, 35 | "canPromoteTo": false, 36 | "promotesTo": ["G"] 37 | } 38 | }, 39 | "P": { 40 | "betza": "fW", 41 | "promoOptions": { 42 | "canPromote": true, 43 | "canPromoteTo": false, 44 | "promotesTo": ["G"] 45 | } 46 | }, 47 | "G": {"betza": "WfF"}, 48 | "R": { 49 | "betza": "R", 50 | "promoOptions": { 51 | "canPromote": true, 52 | "canPromoteTo": false, 53 | "promotesTo": ["D"] 54 | } 55 | }, 56 | "D": {"betza": "FR"}, 57 | "B": { 58 | "betza": "B", 59 | "promoOptions": { 60 | "canPromote": true, 61 | "canPromoteTo": false, 62 | "promotesTo": ["H"] 63 | } 64 | }, 65 | "H": {"betza": "WB"} 66 | }, 67 | "castlingOptions": {"enabled": false}, 68 | "promotionOptions": { 69 | "id": "bishop.promo.optional", 70 | "ranks": [6, 2] 71 | }, 72 | "materialConditions": {"enabled": false}, 73 | "startPosition": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[] w - - 0 1", 74 | "enPassant": false, 75 | "handOptions": { 76 | "enableHands": true, 77 | "addCapturesToHand": true 78 | } 79 | } -------------------------------------------------------------------------------- /example/json/xiangqi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Xiangqi", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "9x10", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "W", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "redPalace", 17 | "blackRegion": "blackPalace", 18 | "restrictMovement": true 19 | } 20 | ] 21 | }, 22 | "A": { 23 | "betza": "F", 24 | "regionEffects": [ 25 | { 26 | "whiteRegion": "redPalace", 27 | "blackRegion": "blackPalace", 28 | "restrictMovement": true 29 | } 30 | ] 31 | }, 32 | "B": { 33 | "betza": "nA", 34 | "regionEffects": [ 35 | { 36 | "whiteRegion": "redSide", 37 | "blackRegion": "blackSide", 38 | "restrictMovement": true 39 | } 40 | ] 41 | }, 42 | "N": {"betza": "nN", "value": 400}, 43 | "R": {"betza": "R", "value": 900}, 44 | "C": {"betza": "mRcpR", "value": 450}, 45 | "P": { 46 | "betza": "fsW", 47 | "value": 100, 48 | "regionEffects": [ 49 | { 50 | "whiteRegion": "redSide", 51 | "blackRegion": "blackSide", 52 | "pieceType": { 53 | "betza": "fW", 54 | "value": 100 55 | } 56 | } 57 | ] 58 | } 59 | }, 60 | "castlingOptions": {"enabled": false}, 61 | "promotionOptions": "bishop.promo.none", 62 | "materialConditions": {"enabled": false}, 63 | "startPosition": "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1", 64 | "enPassant": false, 65 | "regions": { 66 | "redSide": {"b": 0, "t": 4}, 67 | "blackSide": {"b": 5, "t": 9}, 68 | "redPalace": {"b": 0, "t": 2, "l": 3, "r": 5}, 69 | "blackPalace": {"b": 7, "t": 9, "l": 3, "r": 5} 70 | }, 71 | "actions": ["bishop.action.flyingGenerals"] 72 | } -------------------------------------------------------------------------------- /lib/src/actions/effects.dart: -------------------------------------------------------------------------------- 1 | part of 'actions.dart'; 2 | 3 | abstract class ActionEffect { 4 | const ActionEffect(); 5 | } 6 | 7 | /// Causes [square] to be set to [content]. 8 | class EffectModifySquare extends ActionEffect { 9 | final int square; 10 | final int content; 11 | const EffectModifySquare(this.square, this.content); 12 | } 13 | 14 | /// Sets the custom state variable at index [variable] to [value]. 15 | /// You can set a number of variables equal to the size of your board, so on an 16 | /// 8x8 board, the highest [variable] is 63. 17 | /// [value] can be up to 8191, or 2^45 if you assume your code will never 18 | /// execute on a 32-bit vm. 19 | class EffectSetCustomState extends ActionEffect { 20 | final int variable; 21 | final int value; 22 | const EffectSetCustomState(this.variable, this.value); 23 | } 24 | 25 | /// Causes [piece] to be added to [player]'s hand. 26 | class EffectAddToHand extends ActionEffect { 27 | final int player; 28 | final int piece; 29 | const EffectAddToHand(this.player, this.piece); 30 | } 31 | 32 | /// Causes [piece] to be removed from [player]'s hand. 33 | /// If such a piece doesn't exist to be removed, nothing will happen. 34 | class EffectRemoveFromHand extends ActionEffect { 35 | final int player; 36 | final int piece; 37 | const EffectRemoveFromHand(this.player, this.piece); 38 | } 39 | 40 | /// Sets the result of the game to [result]. This will end the game. 41 | class EffectSetGameResult extends ActionEffect { 42 | final GameResult? result; 43 | const EffectSetGameResult(this.result); 44 | } 45 | 46 | /// This effect will cause the move being processed to be marked as invalid, 47 | /// meaning that it won't appear in a legal moves list. 48 | class EffectInvalidateMove extends EffectSetGameResult { 49 | const EffectInvalidateMove() : super(const InvalidMoveResult()); 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/variant/variants/shape.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// Variants that have an unusual board shape. 4 | abstract class ShapeVariants { 5 | /// Variant with a circular shaped board. 6 | static Variant troitzky() => Variant( 7 | name: 'Troitzky Chess', 8 | boardSize: const BoardSize(10, 10), 9 | startPosition: '****qk****/**rnbbnr**/*pppppppp*/*8*/10' 10 | '/10/*8*/*PPPPPPPP*/**RNBBNR**/****QK**** w - - 0 1', 11 | pieceTypes: Bishop.chessPieces, 12 | promotionOptions: const RegionPromotion(whiteId: 'wp', blackId: 'bp'), 13 | regions: { 14 | 'wp': SetRegion( 15 | ['a6', 'b8', 'c9', 'd9', 'e10', 'f10', 'g9', 'h9', 'i8', 'j6'], 16 | ), 17 | 'bp': SetRegion( 18 | ['a5', 'b3', 'c2', 'd2', 'e1', 'f1', 'g2', 'h2', 'i3', 'j5'], 19 | ), 20 | }, 21 | enPassant: true, 22 | ).withBlocker(); 23 | 24 | /// Not fully working yet - en passant is broken in most cases. 25 | static Variant omega() => Variant( 26 | name: 'Omega Chess', 27 | boardSize: const BoardSize(12, 12), 28 | startPosition: 'w**********w/*crnbqkbnrc*/*pppppppppp*/*10*/*10*/*10*' 29 | '/*10*/*10*/*10*/*PPPPPPPPPP*/*CRNBQKBNRC*/W**********W w - - 0 1', 30 | pieceTypes: { 31 | ...Bishop.chessPieces, 32 | 'P': PieceType.longMovePawn(3), 33 | 'C': PieceType.fromBetza('DAW', value: 400), // Champion 34 | 'W': PieceType.fromBetza('FC', value: 400), // Wizard 35 | }, 36 | enPassant: true, 37 | castlingOptions: const CastlingOptions( 38 | enabled: true, 39 | kTarget: Bishop.fileI, 40 | qTarget: Bishop.fileE, 41 | kRook: Bishop.fileJ, 42 | qRook: Bishop.fileC, 43 | ), 44 | ).withBlocker(); 45 | } 46 | -------------------------------------------------------------------------------- /example/json/extinction.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extinction Chess", 3 | "description": "The first player that does not have pieces of all types loses the game.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": {"betza": "N", "value": 300}, 18 | "B": {"betza": "B", "value": 300}, 19 | "R": {"betza": "R", "value": 500}, 20 | "Q": {"betza": "Q", "value": 900}, 21 | "K": {"betza": "K", "castling": true} 22 | }, 23 | "castlingOptions": { 24 | "enabled": true, 25 | "kTarget": 6, 26 | "qTarget": 2, 27 | "fixedRooks": true, 28 | "kRook": 7, 29 | "qRook": 0, 30 | "rookPiece": "R", 31 | "useRookAsTarget": false 32 | }, 33 | "promotionOptions": "bishop.promo.standard", 34 | "materialConditions": { 35 | "enabled": true, 36 | "soloMaters": ["P", "Q", "R", "A", "C"], 37 | "pairMaters": ["N"], 38 | "combinedPairMaters": ["B"], 39 | "specialCases": [["B", "N"]] 40 | }, 41 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 42 | "enPassant": true, 43 | "halfMoveDraw": 100, 44 | "repetitionDraw": 3, 45 | "actions": [ 46 | { 47 | "id": "bishop.action.checkPieceCount", 48 | "pieceType": "P" 49 | }, 50 | { 51 | "id": "bishop.action.checkPieceCount", 52 | "pieceType": "N" 53 | }, 54 | { 55 | "id": "bishop.action.checkPieceCount", 56 | "pieceType": "B" 57 | }, 58 | { 59 | "id": "bishop.action.checkPieceCount", 60 | "pieceType": "R" 61 | }, 62 | { 63 | "id": "bishop.action.checkPieceCount", 64 | "pieceType": "Q" 65 | }, 66 | { 67 | "id": "bishop.action.checkPieceCount", 68 | "pieceType": "K" 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /example/json/kinglet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kinglet Chess", 3 | "description": "The first player to capture all the opponent's pawns wins.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "P": { 8 | "betza": "fmWfceFifmnD", 9 | "promoOptions": { 10 | "canPromote": true, 11 | "canPromoteTo": false 12 | }, 13 | "enPassantable": true, 14 | "noSanSymbol": true, 15 | "value": 100 16 | }, 17 | "N": { 18 | "betza": "N", 19 | "promoOptions": { 20 | "canPromote": false, 21 | "canPromoteTo": false 22 | }, 23 | "value": 300 24 | }, 25 | "B": { 26 | "betza": "B", 27 | "promoOptions": { 28 | "canPromote": false, 29 | "canPromoteTo": false 30 | }, 31 | "value": 300 32 | }, 33 | "R": { 34 | "betza": "R", 35 | "promoOptions": { 36 | "canPromote": false, 37 | "canPromoteTo": false 38 | }, 39 | "value": 500 40 | }, 41 | "Q": { 42 | "betza": "Q", 43 | "promoOptions": { 44 | "canPromote": false, 45 | "canPromoteTo": false 46 | }, 47 | "value": 900 48 | }, 49 | "K": {"betza": "K", "castling": true} 50 | }, 51 | "castlingOptions": { 52 | "enabled": true, 53 | "kTarget": 6, 54 | "qTarget": 2, 55 | "fixedRooks": true, 56 | "kRook": 7, 57 | "qRook": 0, 58 | "rookPiece": "R", 59 | "useRookAsTarget": false 60 | }, 61 | "promotionOptions": "bishop.promo.standard", 62 | "materialConditions": { 63 | "enabled": true, 64 | "soloMaters": ["P", "Q", "R", "A", "C"], 65 | "pairMaters": ["N"], 66 | "combinedPairMaters": ["B"], 67 | "specialCases": [["B", "N"]] 68 | }, 69 | "startPosition": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 70 | "enPassant": true, 71 | "halfMoveDraw": 100, 72 | "repetitionDraw": 3, 73 | "actions": [ 74 | { 75 | "id": "bishop.action.checkPieceCount", 76 | "pieceType": "P" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /lib/src/move/move_list_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | extension MoveListExtension on Iterable { 4 | /// All moves that involve a promotion. 5 | List get promoMoves => where((e) => e.promotion).toList(); 6 | 7 | /// All moves that involve castling. 8 | List get castlingMoves => where((e) => e.castling).toList(); 9 | 10 | /// All moves that involve gating. 11 | List get gatingMoves => where((e) => e.gate).toList(); 12 | 13 | /// All moves that involve a hand drop. 14 | List get handDropMoves => where((e) => e.handDrop).toList(); 15 | 16 | /// All moves that involve a capture. 17 | List get captures => where((e) => e.capture).toList(); 18 | 19 | /// All moves that don't involve a capture. 20 | List get quiet => where((e) => !e.capture).toList(); 21 | 22 | /// Moves that are a pass. Usually should only be 0 or 1 of these. 23 | List get passMoves => 24 | where((e) => e is PassMove).map((e) => e as PassMove).toList(); 25 | 26 | /// All moves from [square]. 27 | List from(int square) => where((e) => e.from == square).toList(); 28 | 29 | /// All moves to [square]. 30 | List to(int square) => where((e) => e.to == square).toList(); 31 | 32 | List toAlgebraic(Game g) => map((e) => g.toAlgebraic(e)).toList(); 33 | } 34 | 35 | extension NormalMoveListExtenion on Iterable { 36 | /// All moves that involve a promotion. 37 | List get promoMoves => where((e) => e.promotion).toList(); 38 | 39 | /// All moves that involve castling. 40 | List get castlingMoves => where((e) => e.castling).toList(); 41 | 42 | /// All moves that involve gating. 43 | List get gatingMoves => where((e) => e.gate).toList(); 44 | 45 | /// All moves that involve a hand drop. 46 | List get handDropMoves => where((e) => e.handDrop).toList(); 47 | } 48 | -------------------------------------------------------------------------------- /example/json/manchu.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Manchu", 3 | "description": "An asymmetric variant of Xiangqi, where red exchanges most oftheir pieces for one very powerful piece.", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "9x10", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "W", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "redPalace", 17 | "blackRegion": "blackPalace", 18 | "restrictMovement": true 19 | } 20 | ] 21 | }, 22 | "A": { 23 | "betza": "F", 24 | "regionEffects": [ 25 | { 26 | "whiteRegion": "redPalace", 27 | "blackRegion": "blackPalace", 28 | "restrictMovement": true 29 | } 30 | ] 31 | }, 32 | "B": { 33 | "betza": "nA", 34 | "regionEffects": [ 35 | { 36 | "whiteRegion": "redSide", 37 | "blackRegion": "blackSide", 38 | "restrictMovement": true 39 | } 40 | ] 41 | }, 42 | "N": {"betza": "nN", "value": 400}, 43 | "R": {"betza": "R", "value": 900}, 44 | "C": {"betza": "mRcpR", "value": 450}, 45 | "P": { 46 | "betza": "fsW", 47 | "value": 100, 48 | "regionEffects": [ 49 | { 50 | "whiteRegion": "redSide", 51 | "blackRegion": "blackSide", 52 | "pieceType": { 53 | "betza": "fW", 54 | "value": 100 55 | } 56 | } 57 | ] 58 | }, 59 | "M": {"betza": "RcpRnN", "value": 1500} 60 | }, 61 | "castlingOptions": {"enabled": false}, 62 | "promotionOptions": "bishop.promo.none", 63 | "materialConditions": {"enabled": false}, 64 | "startPosition": "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/9/9/M1BAKAB2 w - - 0 1", 65 | "enPassant": false, 66 | "regions": { 67 | "redSide": {"b": 0, "t": 4}, 68 | "blackSide": {"b": 5, "t": 9}, 69 | "redPalace": {"b": 0, "t": 2, "l": 3, "r": 5}, 70 | "blackPalace": {"b": 7, "t": 9, "l": 3, "r": 5} 71 | }, 72 | "actions": ["bishop.action.flyingGenerals"] 73 | } -------------------------------------------------------------------------------- /lib/src/move_gen_params.dart: -------------------------------------------------------------------------------- 1 | class MoveGenParams { 2 | final bool captures; 3 | final bool quiet; 4 | final bool castling; 5 | final bool legal; 6 | final bool ignorePieces; 7 | final int? pieceType; 8 | final int? onlySquare; 9 | final bool onlyOne; 10 | 11 | bool get onlyPiece => pieceType != null; 12 | 13 | const MoveGenParams({ 14 | required this.captures, 15 | required this.quiet, 16 | required this.castling, 17 | required this.legal, 18 | this.ignorePieces = false, 19 | this.pieceType, 20 | this.onlySquare, 21 | this.onlyOne = false, 22 | }); 23 | static const normal = MoveGenParams( 24 | captures: true, 25 | quiet: true, 26 | castling: true, 27 | legal: true, 28 | ); 29 | static const onlyQuiet = MoveGenParams( 30 | captures: false, 31 | quiet: true, 32 | castling: true, 33 | legal: true, 34 | ); 35 | static const onlyCaptures = MoveGenParams( 36 | captures: true, 37 | quiet: false, 38 | castling: false, 39 | legal: true, 40 | ); 41 | static const attacks = MoveGenParams( 42 | captures: true, 43 | quiet: false, 44 | castling: false, 45 | legal: false, 46 | ); 47 | factory MoveGenParams.pieceCaptures(int pieceType) => MoveGenParams( 48 | captures: true, 49 | quiet: false, 50 | castling: false, 51 | legal: false, 52 | pieceType: pieceType, 53 | ); 54 | factory MoveGenParams.squareAttacks(int square, [bool onlyOne = true]) => 55 | MoveGenParams( 56 | captures: true, 57 | quiet: false, 58 | castling: false, 59 | legal: false, 60 | onlySquare: square, 61 | onlyOne: onlyOne, 62 | ); 63 | static const premoves = MoveGenParams( 64 | captures: true, 65 | quiet: true, 66 | castling: true, 67 | legal: false, 68 | ignorePieces: true, 69 | ); 70 | 71 | @override 72 | String toString() => 'MoveGenParams($captures, $quiet, $castling, $legal' 73 | ' $ignorePieces, $pieceType, $onlySquare)'; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/serialisation/type_adapter.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | /// An adapter to allow serialisation of class-based variant parameters, such 4 | /// as actions, or promotion options. 5 | abstract class BishopTypeAdapter { 6 | String get id; 7 | Map? export(T e); 8 | T build(Map? params); 9 | Type get type => T; 10 | const BishopTypeAdapter(); 11 | } 12 | 13 | /// A simplified adapter for types that don't take any parameters. 14 | class BasicAdapter implements BishopTypeAdapter { 15 | @override 16 | final String id; 17 | final T Function() builder; 18 | const BasicAdapter(this.id, this.builder); 19 | 20 | @override 21 | T build(Map? params) => builder(); 22 | 23 | @override 24 | Map? export(T e) => null; 25 | 26 | @override 27 | Type get type => T; 28 | } 29 | 30 | /// A type adapter that also takes a list of adapters, allowing deep 31 | /// serialisation. Use this for classes that contain other serialisable 32 | /// classes. 33 | abstract class DeepAdapter extends BishopTypeAdapter { 34 | @override 35 | T build( 36 | Map? params, { 37 | List adapters = const [], 38 | }); 39 | @override 40 | Map? export( 41 | T e, { 42 | List adapters = const [], 43 | }); 44 | 45 | /// A shortcut for BishopSerialisation.export. 46 | dynamic serialise( 47 | X object, { 48 | List adapters = const [], 49 | bool strict = true, 50 | }) => 51 | BishopSerialisation.export(object, adapters: adapters, strict: strict); 52 | 53 | /// A shortcut for BishopSerialisation.build. 54 | X? deserialise( 55 | dynamic input, { 56 | List adapters = const [], 57 | bool strict = true, 58 | X? Function(dynamic input)? fallback, 59 | }) => 60 | BishopSerialisation.build( 61 | input, 62 | adapters: adapters, 63 | strict: strict, 64 | fallback: fallback, 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /test/perft.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | import 'constants.dart'; 4 | 5 | class Perfts { 6 | static List standard = [ 7 | PerftTest(fen: Positions.standardDefault, depth: 3, nodes: 8902), 8 | PerftTest(fen: Positions.kiwiPete, depth: 2, nodes: 2039), 9 | // PerftTest(fen: Positions.kiwiPete, depth: 3, nodes: 97862), 10 | PerftTest(fen: Positions.rookPin, depth: 4, nodes: 43238), 11 | PerftTest(fen: Positions.position4, depth: 3, nodes: 9467), 12 | PerftTest(fen: Positions.position5, depth: 3, nodes: 62379), 13 | PerftTest(fen: Positions.position6, depth: 3, nodes: 89890), 14 | ]; 15 | 16 | static List ruleVariants = [ 17 | PerftTest( 18 | variant: Variants.crazyhouse, 19 | fen: '2k5/8/8/8/8/8/8/4K3[QRBNPqrbnp] w - - 0 1', 20 | depth: 2, 21 | nodes: 75353, 22 | ), 23 | PerftTest( 24 | variant: Variants.antichess, 25 | fen: Positions.standardDefault, 26 | depth: 4, 27 | nodes: 153299, 28 | ), 29 | // todo: atomic tests currently fail because of castling 30 | // it seems like lichess and pychess allow castling to both target and rook, 31 | // while we just allow target 32 | // PerftTest( 33 | // variant: Variants.atomic, 34 | // fen: 'r4b1r/2kb1N2/p2Bpnp1/8/2Pp3p/1P1PPP2/P5PP/R3K2R b KQ - 0 1', 35 | // depth: 2, 36 | // nodes: 148, 37 | // ), 38 | // PerftTest( 39 | // variant: Variants.atomic, 40 | // fen: '1R4kr/4K3/8/8/8/8/8/8 b k - 0 1', 41 | // depth: 4, 42 | // nodes: 17915, 43 | // ), 44 | // todo: figure out why this is different (12560 vs 12407) 45 | // PerftTest( 46 | // variant: Variants.threeCheck, 47 | // fen: '7r/1p4p1/pk3p2/RN6/8/P5Pp/3p1P1P/4R1K1 w - - 1 39 +2+0', 48 | // depth: 3, 49 | // nodes: 12407, 50 | // ), 51 | ]; 52 | } 53 | 54 | class PerftTest { 55 | final Variants variant; 56 | final String fen; 57 | final int depth; 58 | final int nodes; 59 | 60 | PerftTest({ 61 | this.variant = Variants.chess, 62 | required this.fen, 63 | required this.depth, 64 | required this.nodes, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/variant/options/first_move_options.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | abstract class FirstMoveOptions { 4 | const FirstMoveOptions(); 5 | 6 | PieceMoveChecker? build(BuiltVariant variant); 7 | 8 | factory FirstMoveOptions.pair( 9 | FirstMoveOptions? white, 10 | FirstMoveOptions? black, 11 | ) => 12 | FirstMoveOptionsPair(white, black); 13 | 14 | factory FirstMoveOptions.set(Map set) => 15 | FirstMoveOptionsSet(set); 16 | 17 | factory FirstMoveOptions.ranks(List white, List black) => 18 | FirstMoveOptionsPair( 19 | RanksFirstMoveOptions(white), 20 | RanksFirstMoveOptions(black), 21 | ); 22 | 23 | static const standard = InitialStateFirstMoveOptions(); 24 | } 25 | 26 | class FirstMoveOptionsPair implements FirstMoveOptions { 27 | final FirstMoveOptions? white; 28 | final FirstMoveOptions? black; 29 | 30 | const FirstMoveOptionsPair(this.white, this.black); 31 | 32 | @override 33 | PieceMoveChecker? build(BuiltVariant variant) { 34 | final List built = [ 35 | white?.build(variant), 36 | black?.build(variant), 37 | ]; 38 | return (params) => built[params.colour]?.call(params) ?? false; 39 | } 40 | } 41 | 42 | class FirstMoveOptionsSet implements FirstMoveOptions { 43 | final Map set; 44 | const FirstMoveOptionsSet(this.set); 45 | 46 | @override 47 | PieceMoveChecker build(BuiltVariant variant) { 48 | final Map builtSet = 49 | set.map((k, v) => MapEntry(variant.pieceIndex(k), v.build(variant))); 50 | return (params) => builtSet[params.piece.type]?.call(params) ?? false; 51 | } 52 | } 53 | 54 | class RanksFirstMoveOptions implements FirstMoveOptions { 55 | final List ranks; 56 | const RanksFirstMoveOptions(this.ranks); 57 | 58 | @override 59 | PieceMoveChecker build(BuiltVariant variant) => 60 | (params) => ranks.contains(params.size.rank(params.from)); 61 | } 62 | 63 | class InitialStateFirstMoveOptions implements FirstMoveOptions { 64 | const InitialStateFirstMoveOptions(); 65 | @override 66 | PieceMoveChecker build(BuiltVariant variant) => 67 | (params) => params.piece.inInitialState; 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/serialisation/state_transform_adapters.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | class PairTransformAdapter extends DeepAdapter { 4 | @override 5 | String get id => 'bishop.st.pair'; 6 | 7 | @override 8 | StateTransformerPair build( 9 | Map? params, { 10 | List adapters = const [], 11 | }) => 12 | StateTransformerPair( 13 | deserialise(params?['white'], adapters: adapters), 14 | deserialise(params?['black'], adapters: adapters), 15 | ); 16 | 17 | @override 18 | Map? export( 19 | StateTransformerPair e, { 20 | List adapters = const [], 21 | }) => 22 | { 23 | 'white': e.white != null 24 | ? serialise(e.white!, adapters: adapters) 25 | : null, 26 | 'black': e.black != null 27 | ? serialise(e.black!, adapters: adapters) 28 | : null, 29 | }; 30 | } 31 | 32 | class VisionAreaAdapter extends BishopTypeAdapter { 33 | @override 34 | String get id => 'bishop.st.visionArea'; 35 | 36 | @override 37 | VisionAreaStateTransformer build(Map? params) => 38 | VisionAreaStateTransformer( 39 | area: params?['area'] == null 40 | ? Area.radius1 41 | : Area.fromStrings(params!['area'].cast()), 42 | ); 43 | 44 | @override 45 | Map export(VisionAreaStateTransformer e) => { 46 | if (e.area != Area.radius1) 'area': e.area.export(), 47 | }; 48 | } 49 | 50 | class HideFlagsAdapter extends BishopTypeAdapter { 51 | @override 52 | String get id => 'bishop.st.hideFlags'; 53 | 54 | @override 55 | HideFlagsStateTransformer build(Map? params) => 56 | HideFlagsStateTransformer( 57 | forSelf: params?['self'] ?? false, 58 | forOpponent: params?['opponent'] ?? true, 59 | ); 60 | 61 | @override 62 | Map export(HideFlagsStateTransformer e) => { 63 | if (e.forSelf) 'self': e.forSelf, 64 | if (!e.forOpponent) 'opponent': e.forOpponent, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /test/divide.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:bishop/bishop.dart'; 5 | 6 | import 'constants.dart'; 7 | 8 | void main(List args) async { 9 | bool showCorrect = true; 10 | String fen = Positions.kiwiPete; 11 | int depth = 3; 12 | Game game = Game(variant: Variant.standard(), fen: fen); 13 | Map results = game.divide(depth); 14 | results.forEach((key, value) => print('$key: $value')); 15 | 16 | Map stockfish = {}; 17 | await Process.start('stockfish', []).then((Process process) { 18 | process.stdin.writeln('position fen $fen'); 19 | process.stdout.transform(utf8.decoder).listen((data) { 20 | for (var line in data.split('\n')) { 21 | for (var entry in line.split(',')) { 22 | int colon = entry.indexOf(':'); 23 | if (colon != -1) { 24 | String key = entry.substring(0, colon); 25 | int value = int.parse(entry.substring(colon + 2)); 26 | stockfish[key] = value; 27 | if (results[key] == null) { 28 | if (key.contains('searched')) { 29 | // end of results 30 | results.forEach((key, value) { 31 | if (stockfish[key] == null) { 32 | print('Stockfish didn\'t generate $key, but we did'); 33 | } else { 34 | if (value != stockfish[key]) { 35 | int diff = stockfish[key] - value; 36 | print( 37 | 'Error on $key - $value (bishop) vs ${stockfish[key]} (stockfish) [${(diff > 0) ? "+$diff" : diff}]', 38 | ); 39 | } else if (showCorrect) { 40 | print('$key correct ($value)'); 41 | } 42 | } 43 | }); 44 | } else { 45 | print('$key not generated by us'); 46 | } 47 | } 48 | } 49 | } 50 | } 51 | //print(stockfish); 52 | //print("s"); 53 | }); 54 | process.exitCode.then((value) { 55 | print('stockfish finished'); 56 | print(stockfish); 57 | }); 58 | process.stdin.writeln('go perft $depth'); 59 | //process.kill(); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/regions/area.dart: -------------------------------------------------------------------------------- 1 | part of 'regions.dart'; 2 | 3 | class Area implements Region { 4 | final List directions; 5 | 6 | int get minX => directions.map((e) => e.h).reduce(min); 7 | int get minY => directions.map((e) => e.v).reduce(min); 8 | int get maxX => directions.map((e) => e.h).reduce(max); 9 | int get maxY => directions.map((e) => e.v).reduce(max); 10 | 11 | const Area({required this.directions}); 12 | 13 | factory Area.fromStrings(List directions) => 14 | Area(directions: directions.map((e) => Direction.fromString(e)).toList()); 15 | 16 | List export() => directions.map((e) => e.simpleString).toList(); 17 | 18 | factory Area.filled({ 19 | required int width, 20 | required int height, 21 | int xOffset = 0, 22 | int yOffset = 0, 23 | bool omitCentre = false, 24 | }) { 25 | int xStart = -(width ~/ 2) + xOffset; 26 | int yStart = -(height ~/ 2) + yOffset; 27 | List dirs = List.generate( 28 | width, 29 | (x) => List.generate(height, (y) => Direction(x + xStart, y + yStart)), 30 | ).expand((e) => e).toList(); 31 | if (omitCentre) { 32 | dirs.remove(const Direction(0, 0)); 33 | } 34 | return Area(directions: dirs); 35 | } 36 | 37 | factory Area.radius(int size, {bool omitCentre = false}) => Area.filled( 38 | width: size * 2 + 1, 39 | height: size * 2 + 1, 40 | omitCentre: omitCentre, 41 | ); 42 | 43 | static const radius1 = Area( 44 | directions: [ 45 | // 46 | Direction(-1, 1), Direction(0, 1), Direction(1, 1), 47 | Direction(-1, 0), Direction(0, 0), Direction(1, 0), 48 | Direction(-1, -1), Direction(0, -1), Direction(1, -1), 49 | ], 50 | ); 51 | 52 | @override 53 | Area translate(int x, int y) => 54 | Area(directions: directions.map((e) => e.translate(x, y)).toList()); 55 | 56 | @override 57 | bool contains(int file, int rank) => 58 | directions.contains(Direction(file, rank)); 59 | 60 | @override 61 | List squares(BoardSize size) { 62 | List dirs = [...directions]; 63 | dirs.removeWhere( 64 | (e) => e.h < 0 || e.v < 0 || e.h > size.maxFile || e.v > size.maxRank, 65 | ); 66 | return dirs.map((e) => size.square(e.h, e.v)).toList(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/drops.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | typedef DropBuilderFunction = List Function(MoveParams params); 4 | 5 | class Drops { 6 | static DropBuilderFunction standard({bool restrictPromoPieces = true}) => 7 | (MoveParams params) { 8 | final state = params.state; 9 | Set hand = state.handPieceTypes(params.colour); 10 | if (hand.isEmpty) return []; 11 | final variant = params.variant; 12 | final size = variant.boardSize; 13 | List drops = []; 14 | for (int i = 0; i < size.numIndices; i++) { 15 | if (!size.onBoard(i)) continue; 16 | if (state.board[i].isNotEmpty) continue; 17 | int hRank = size.rank(i); 18 | bool onEdgeRank = hRank == Bishop.rank1 || hRank == size.maxRank; 19 | for (int p in hand) { 20 | if (restrictPromoPieces) { 21 | if (onEdgeRank && 22 | variant.pieces[p].type.promoOptions.canPromote) { 23 | continue; 24 | } 25 | } 26 | final m = DropMove(to: i, piece: p); 27 | // NormalMove m = NormalMove.drop(to: i, dropPiece: p); 28 | drops.add(m); 29 | } 30 | } 31 | return drops; 32 | }; 33 | 34 | static DropBuilderFunction pair( 35 | DropBuilderFunction white, 36 | DropBuilderFunction black, 37 | ) => 38 | (params) => params.colour == Bishop.white ? white(params) : black(params); 39 | 40 | static DropBuilderFunction none() => (_) => []; 41 | 42 | static DropBuilderFunction region(Region region) => (MoveParams params) { 43 | Set hand = params.state.handPieceTypes(params.colour); 44 | if (hand.isEmpty) return []; 45 | final size = params.variant.boardSize; 46 | List drops = []; 47 | for (int i in size.squaresForRegion(region)) { 48 | if (!size.onBoard(i)) continue; 49 | if (params.state.board[i].isNotEmpty) continue; 50 | drops.addAll(hand.map((e) => DropMove(to: i, piece: e))); 51 | } 52 | return drops; 53 | }; 54 | 55 | static DropBuilderFunction regions(Region? white, Region? black) => pair( 56 | white == null ? none() : region(white), 57 | black == null ? none() : region(black), 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /test/square_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Square/Piece Representation', () { 6 | test('Empty', () { 7 | int x = Bishop.empty; 8 | expect(x.isEmpty, true); 9 | expect(x.piece, 0); 10 | expect(x.hasFlag(4), false); 11 | expect(x.inInitialState, false); 12 | }); 13 | test('Simple Piece', () { 14 | int x = makePiece(3, Bishop.white); 15 | expect(x.isEmpty, false); 16 | expect(x.type, 3); 17 | expect(x.colour, Bishop.white); 18 | expect(x.flags, 0); 19 | expect(x.inInitialState, false); 20 | }); 21 | test('Promoted Piece', () { 22 | int x = makePiece(2, Bishop.black, internalType: 1); 23 | expect(x.isEmpty, false); 24 | expect(x.type, 2); 25 | expect(x.internalType, 1); 26 | expect(x.colour, Bishop.black); 27 | expect(x.flags, 0); 28 | expect(x.piece, makePiece(2, Bishop.black)); 29 | expect(x.inInitialState, false); 30 | }); 31 | test('Piece with Flags', () { 32 | int x = makePiece(6, Bishop.black).setFlag(3).setFlag(7); 33 | expect(x.isEmpty, false); 34 | expect(x.type, 6); 35 | expect(x.hasFlag(3), true); 36 | expect(x.hasFlag(4), false); 37 | expect(x.hasFlag(7), true); 38 | expect(x.inInitialState, false); 39 | }); 40 | test('Piece in Initial State', () { 41 | int x = makePiece(1, Bishop.white, initialState: true); 42 | expect(x.isEmpty, false); 43 | expect(x.type, 1); 44 | expect(x.inInitialState, true); 45 | }); 46 | test('Compare makePiece to alterations', () { 47 | int x = makePiece(7, Bishop.white) 48 | .flipColour() 49 | .setFlag(7) 50 | .toggleFlag(8) 51 | .toggleFlag(4) 52 | .unsetFlag(8) 53 | .setInitialState(true) 54 | .setInternalType(4); 55 | int y = makePiece( 56 | 7, 57 | Bishop.black, 58 | flags: makeFlags([4, 7]), 59 | internalType: 4, 60 | initialState: true, 61 | ); 62 | expect(x, y); 63 | }); 64 | }); 65 | test('Remove Flags', () { 66 | int x = makePiece(1, Bishop.white).setFlag(3).setFlag(7); 67 | expect(x.hasFlag(3), true); 68 | expect(x.removeFlags().flags, 0); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/actions/actions/immortality.dart: -------------------------------------------------------------------------------- 1 | part of '../base_actions.dart'; 2 | 3 | /// Prevents pieces from being captured (or otherwise destroyed by actions that 4 | /// occur before this one). For example, if [pieceType] is 'Q', queens will not 5 | /// be capturable. 6 | /// It's also possible to specify a [flag], which will make any pieces with that 7 | /// flag immortal. [flag] and [pieceType] can be combined. 8 | /// If adding this action to a piece type's actions field, it is not necessary 9 | /// to specify the piece type, as it will be added when `BuiltVariant` is 10 | /// created. 11 | class ActionImmortality extends Action { 12 | final String? pieceType; 13 | final int? pieceIndex; 14 | final int? flag; 15 | 16 | ActionImmortality({ 17 | this.pieceType, 18 | this.pieceIndex, 19 | this.flag, 20 | super.event = ActionEvent.afterMove, 21 | super.condition, 22 | }) : super( 23 | precondition: Conditions.merge([ 24 | if (pieceType != null) Conditions.capturedPieceIs(pieceType), 25 | if (flag != null) Conditions.capturedPieceHasFlag(flag), 26 | if (pieceIndex != null) Conditions.capturedPieceType(pieceIndex), 27 | ]), 28 | action: ActionDefinitions.invalidateMove, 29 | ); 30 | 31 | @override 32 | Action forPieceType(int type) => ActionImmortality( 33 | pieceIndex: type, 34 | flag: flag, 35 | event: event, 36 | condition: condition, 37 | ); 38 | } 39 | 40 | class ImmortalityAdapter extends BishopTypeAdapter { 41 | @override 42 | String get id => 'bishop.action.immortality'; 43 | 44 | @override 45 | ActionImmortality build(Map? params) => ActionImmortality( 46 | event: ActionEvent.import(params?['event']), 47 | pieceType: params?['pieceType'], 48 | flag: params?['flag'], 49 | ); 50 | 51 | @override 52 | Map? export(ActionImmortality e) { 53 | if (e.condition != null) { 54 | throw const BishopException('Unsupported export of condition'); 55 | } 56 | return { 57 | if (e.event != ActionEvent.afterMove) 'event': e.event.export(), 58 | if (e.pieceType != null) 'pieceType': e.pieceType, 59 | if (e.pieceIndex != null) 'pieceIndex': e.pieceIndex, 60 | if (e.flag != null) 'flag': e.flag, 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/json/orda.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Orda", 3 | "description": "", 4 | "bishopVersion": "1.4.3", 5 | "boardSize": "8x8", 6 | "pieceTypes": { 7 | "K": { 8 | "betza": "K", 9 | "royal": true, 10 | "promoOptions": { 11 | "canPromote": false, 12 | "canPromoteTo": false 13 | }, 14 | "regionEffects": [ 15 | { 16 | "whiteRegion": "blackCamp", 17 | "blackRegion": "whiteCamp", 18 | "winGame": true 19 | } 20 | ] 21 | }, 22 | "P": { 23 | "betza": "fmWfceFifmnD", 24 | "promoOptions": { 25 | "canPromote": true, 26 | "canPromoteTo": false 27 | }, 28 | "enPassantable": true, 29 | "noSanSymbol": true, 30 | "value": 100 31 | }, 32 | "N": { 33 | "betza": "N", 34 | "promoOptions": { 35 | "canPromote": false, 36 | "canPromoteTo": false 37 | }, 38 | "value": 300 39 | }, 40 | "B": { 41 | "betza": "B", 42 | "promoOptions": { 43 | "canPromote": false, 44 | "canPromoteTo": false 45 | }, 46 | "value": 300 47 | }, 48 | "R": { 49 | "betza": "R", 50 | "promoOptions": { 51 | "canPromote": false, 52 | "canPromoteTo": false 53 | }, 54 | "value": 500 55 | }, 56 | "Q": {"betza": "Q", "value": 900}, 57 | "L": { 58 | "betza": "mNcR", 59 | "promoOptions": { 60 | "canPromote": false, 61 | "canPromoteTo": false 62 | }, 63 | "value": 400 64 | }, 65 | "H": {"betza": "KN", "value": 700}, 66 | "A": { 67 | "betza": "mNcB", 68 | "promoOptions": { 69 | "canPromote": false, 70 | "canPromoteTo": false 71 | }, 72 | "value": 400 73 | }, 74 | "Y": { 75 | "betza": "FfW", 76 | "promoOptions": { 77 | "canPromote": false, 78 | "canPromoteTo": false 79 | } 80 | } 81 | }, 82 | "castlingOptions": { 83 | "enabled": true, 84 | "kTarget": 6, 85 | "qTarget": 2, 86 | "fixedRooks": true, 87 | "kRook": 7, 88 | "qRook": 0, 89 | "rookPiece": "R", 90 | "useRookAsTarget": false 91 | }, 92 | "promotionOptions": "bishop.promo.standard", 93 | "materialConditions": {"enabled": false}, 94 | "startPosition": "lhaykahl/8/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1", 95 | "enPassant": true, 96 | "halfMoveDraw": 100, 97 | "repetitionDraw": 3, 98 | "regions": { 99 | "whiteCamp": {"b": 0, "t": 0}, 100 | "blackCamp": {"b": 7, "t": 7} 101 | } 102 | } -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:bishop/bishop.dart'; 5 | 6 | import 'play.dart'; 7 | 8 | final parser = ArgParser() 9 | ..addFlag('ai', abbr: 'a', negatable: false) 10 | ..addOption('variant', abbr: 'v', defaultsTo: 'chess') 11 | ..addOption( 12 | 'movelimit', 13 | abbr: 'l', 14 | defaultsTo: '200', 15 | help: 'Number of half moves before game is terminated, 0 is unlimited.', 16 | ) 17 | ..addOption('pgn', abbr: 'p', help: 'PGN output file') 18 | ..addFlag('help', abbr: 'h', negatable: false); 19 | 20 | void main(List args) async { 21 | final parsedArgs = parser.parse(args); 22 | if (parsedArgs['help']) { 23 | print(parser.usage); 24 | return; 25 | } 26 | String v = parsedArgs['variant']; 27 | bool ai = parsedArgs['ai']; 28 | int moveLimit = int.parse(parsedArgs['movelimit']); 29 | Variant variant = variantFromString(v) ?? Variant.standard(); 30 | print('Starting game with variant ${variant.name}'); 31 | await Future.delayed(const Duration(seconds: 3)); 32 | Game game = Game(variant: variant); 33 | Engine engine = Engine(game: game); 34 | int i = 0; 35 | 36 | while (!game.gameOver) { 37 | String playerName = Bishop.playerName[game.turn]; 38 | print(game.ascii()); 39 | print(game.fen); 40 | Move? m; 41 | if (ai) { 42 | printYellow('~~ $playerName is thinking..'); 43 | EngineResult res = await engine.search(); 44 | printYellow('Best Move: ${formatEngineResult(res, game)}'); 45 | m = res.move; 46 | } else { 47 | m = game.getRandomMove(); 48 | } 49 | if (m == null) { 50 | printRed('couldn\'t find a move'); 51 | } 52 | print('$playerName: ${game.toSan(m!)}'); 53 | game.makeMove(m); 54 | i++; 55 | if (moveLimit > 0 && i >= moveLimit) break; 56 | } 57 | print(game.ascii()); 58 | printYellow(game.pgn()); 59 | printCyan(game.result?.readable ?? 'Game Over (too long)'); 60 | if (parsedArgs['pgn'] != null) { 61 | final f = File(parsedArgs['pgn']!); 62 | f.writeAsStringSync(game.pgn(includeVariant: true)); 63 | printMagenta('Wrote PGN to ${parsedArgs['pgn']}'); 64 | } 65 | } 66 | 67 | String formatEngineResult(EngineResult res, Game game) { 68 | if (!res.hasMove) return 'No Move'; 69 | String san = game.toSan(res.move!); 70 | return '$san (${res.eval}) [depth ${res.depth}]'; 71 | } 72 | -------------------------------------------------------------------------------- /test/crazyhouse_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | /* 5 | +---+---+---+---+---+---+---+---+ 6 | | r | | | q | k | | | n |8 [qp] 7 | +---+---+---+---+---+---+---+---+ 8 | | | | p | | | | | |7 9 | +---+---+---+---+---+---+---+---+ 10 | | | | b | | p | p | b | |6 11 | +---+---+---+---+---+---+---+---+ 12 | | p | p | | p | P | | B | n |5 13 | +---+---+---+---+---+---+---+---+ 14 | | | | | | | | | p |4 15 | +---+---+---+---+---+---+---+---+ 16 | | | | | | | P | | |3 17 | +---+---+---+---+---+---+---+---+ 18 | | P | P | P | P | | | P | P |2 19 | +---+---+---+---+---+---+---+---+ 20 | | R | N | B | | | R | K | |1 * [RN] 21 | +---+---+---+---+---+---+---+---+ 22 | a b c d e f g h 23 | 24 | Fen: r2qk2n/2p5/2b1ppb1/pp1pP1Bn/7p/5P2/PPPP2PP/RNB2RK1[RNqp] w q - 0 31 25 | */ 26 | 27 | void main() { 28 | group('Crazyhouse', () { 29 | test('Crazyhouse take promoted pawn', () { 30 | Game g = Game(variant: Variant.crazyhouse()); 31 | List moves = 32 | 'e2e3,d7d5,d1f3,g8f6,f3f4,b8c6,f1b5,c8d7,b5c6,d7c6,g1e2,b@d6,n@e5,d6e5,f4e5,n@g4,e5g3,e7e6,f2f3,f8d6,b@f4,d6f4,g3f4,b@e5,f4b4,a7a5,b4c5,e5d6,c5c3,d6b4,c3d3,g4e5,d3d4,e5d7,b@g3,b4c5,d4d3,b7b5,e1g1,h7h5,e2d4,c5d4,e3d4,h5h4,g3f4,f6h5,f4e3,g7g5,b@e5,f7f6,d3g6,n@f7,e3g5,d7e5,d4e5,b@f5,p@g7,f5g6,g7h8q,f7h8' 33 | .split(','); 34 | for (String m in moves) { 35 | bool ok = g.makeMoveString(m); 36 | expect(ok, true); 37 | } 38 | 39 | expect(g.handString, 'NRqp'); 40 | }); 41 | test('Put pawn on first, last lane', () { 42 | Game g = Game( 43 | variant: Variant.crazyhouse(), 44 | ); 45 | List moves = [ 46 | 'e2e4', 47 | 'e7e5', 48 | 'd2d4', 49 | 'e5d4', 50 | 'd1f3', 51 | 'g8f6', 52 | 'c1g5', 53 | ]; 54 | for (String m in moves) { 55 | bool ok = g.makeMoveString(m); 56 | expect(ok, true); 57 | } 58 | List gmoves = g.generateLegalMoves(); 59 | Set smoves = {}; 60 | for (Move m in gmoves) { 61 | smoves.add(g.toAlgebraic(m)); 62 | } 63 | expect(smoves.contains('p@e7'), true); 64 | expect(smoves.contains('p@d1'), false); 65 | expect(smoves.contains('p@g8'), false); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/move/move.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | part 'drop_move.dart'; 4 | part 'gating_move.dart'; 5 | part 'move_meta.dart'; 6 | part 'multi_move.dart'; 7 | part 'standard_move.dart'; 8 | part 'static_move.dart'; 9 | part 'pass_move.dart'; 10 | part 'wrapper_move.dart'; 11 | 12 | abstract class Move { 13 | const Move(); 14 | 15 | /// The board location this move starts at. 16 | int get from; 17 | 18 | /// The board location this move ends at. 19 | int get to; 20 | 21 | /// The piece (including colour and flags) that is being captured, if one is. 22 | int? get capturedPiece => null; 23 | 24 | /// Whether a piece is captured as a result of this move. 25 | bool get capture => false; 26 | 27 | /// If this move is en passant. 28 | bool get enPassant => false; 29 | 30 | /// If this move sets the en passant flag. 31 | bool get setEnPassant => false; 32 | 33 | /// The piece (type only) that is being promoted to. 34 | int? get promoPiece => null; 35 | 36 | /// Whether the moved piece is promoted. 37 | bool get promotion => false; 38 | 39 | /// Whether this is a castling move. 40 | bool get castling => false; 41 | 42 | /// Whether this is a gated drop, e.g. the drops in Seirawan chess. 43 | bool get gate => false; 44 | 45 | /// Whether this is a drop move where the piece came from the hand to an empty 46 | /// square, e.g. the drops in Crazyhouse. 47 | bool get handDrop => false; 48 | 49 | /// The piece (type only) that is being dropped, if one is. 50 | int? get dropPiece => null; 51 | 52 | @override 53 | int get hashCode => 54 | from.hashCode ^ 55 | to.hashCode ^ 56 | capturedPiece.hashCode ^ 57 | promoPiece.hashCode ^ 58 | enPassant.hashCode ^ 59 | setEnPassant.hashCode ^ 60 | castling.hashCode ^ 61 | gate.hashCode ^ 62 | gate.hashCode ^ 63 | dropPiece.hashCode; 64 | 65 | @override 66 | bool operator ==(Object other) { 67 | if (other is! Move) return false; 68 | if (other.runtimeType != runtimeType) return false; 69 | return from == other.from && 70 | to == other.to && 71 | capturedPiece == other.capturedPiece && 72 | promoPiece == other.promoPiece && 73 | enPassant == other.enPassant && 74 | setEnPassant == other.setEnPassant && 75 | castling == other.castling && 76 | gate == other.gate && 77 | dropPiece == other.dropPiece; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/serialisation/first_move_adapters.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | class FirstMovePairAdapter extends DeepAdapter { 4 | @override 5 | String get id => 'bishop.first.pair'; 6 | 7 | @override 8 | FirstMoveOptionsPair build( 9 | Map? params, { 10 | List adapters = const [], 11 | }) => 12 | FirstMoveOptionsPair( 13 | deserialise(params?['white'], adapters: adapters), 14 | deserialise(params?['black'], adapters: adapters), 15 | ); 16 | 17 | @override 18 | Map? export( 19 | FirstMoveOptionsPair e, { 20 | List adapters = const [], 21 | }) => 22 | { 23 | 'white': e.white != null 24 | ? serialise(e.white!, adapters: adapters) 25 | : null, 26 | 'black': e.black != null 27 | ? serialise(e.black!, adapters: adapters) 28 | : null, 29 | }; 30 | } 31 | 32 | class FirstMoveSetAdapter extends DeepAdapter { 33 | @override 34 | String get id => 'bishop.first.set'; 35 | 36 | @override 37 | FirstMoveOptionsSet build( 38 | Map? params, { 39 | List adapters = const [], 40 | }) => 41 | FirstMoveOptionsSet( 42 | BishopSerialisation.buildMap( 43 | params!, 44 | adapters: adapters, 45 | ), 46 | ); 47 | 48 | @override 49 | Map? export( 50 | FirstMoveOptionsSet e, { 51 | List adapters = const [], 52 | }) => 53 | BishopSerialisation.exportMap(e.set, adapters: adapters); 54 | } 55 | 56 | class RanksFirstMoveAdapter extends BishopTypeAdapter { 57 | @override 58 | String get id => 'bishop.first.ranks'; 59 | 60 | @override 61 | RanksFirstMoveOptions build(Map? params) => 62 | RanksFirstMoveOptions((params!['ranks'] as List).cast()); 63 | 64 | @override 65 | Map export(RanksFirstMoveOptions e) => {'ranks': e.ranks}; 66 | } 67 | 68 | /// This one is not exported if it's at the top level (not in a pair/set). 69 | class InitialFirstMoveAdapter 70 | extends BasicAdapter { 71 | const InitialFirstMoveAdapter() 72 | : super('bishop.first.initial', InitialStateFirstMoveOptions.new); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/actions/actions/explosion.dart: -------------------------------------------------------------------------------- 1 | part of '../base_actions.dart'; 2 | 3 | /// Creates an explosion in [area] whenever a piece is captured. 4 | class ActionExplodeOnCapture extends Action { 5 | final Area area; 6 | final List? immunePieces; 7 | final bool alwaysSuicide; 8 | 9 | ActionExplodeOnCapture( 10 | this.area, { 11 | this.immunePieces, 12 | this.alwaysSuicide = true, 13 | }) : super( 14 | event: ActionEvent.afterMove, 15 | precondition: Conditions.isCapture, 16 | action: ActionDefinitions.explosion( 17 | area, 18 | immunePieces: immunePieces, 19 | alwaysSuicide: alwaysSuicide, 20 | ), 21 | ); 22 | } 23 | 24 | /// Creates an explosion with [radius] whenever a piece is captured. 25 | class ActionExplosionRadius extends ActionExplodeOnCapture { 26 | final int radius; 27 | 28 | ActionExplosionRadius( 29 | this.radius, { 30 | super.immunePieces, 31 | super.alwaysSuicide = true, 32 | }) : super(Area.radius(radius)); 33 | } 34 | 35 | class ExplodeOnCaptureAdapter 36 | extends BishopTypeAdapter { 37 | @override 38 | String get id => 'bishop.action.explodeOnCapture'; 39 | 40 | @override 41 | ActionExplodeOnCapture build(Map? params) => 42 | ActionExplodeOnCapture( 43 | Area.fromStrings(params!['area'].cast()), 44 | immunePieces: params['immunePieces']?.cast(), 45 | alwaysSuicide: params['alwaysSuicide'] ?? true, 46 | ); 47 | 48 | @override 49 | Map export(ActionExplodeOnCapture e) => { 50 | 'area': e.area.export(), 51 | if (e.immunePieces?.isNotEmpty ?? false) 'immunePieces': e.immunePieces, 52 | if (!e.alwaysSuicide) 'alwaysSuicide': e.alwaysSuicide, 53 | }; 54 | } 55 | 56 | class ExplosionRadiusAdapter extends BishopTypeAdapter { 57 | @override 58 | String get id => 'bishop.action.explosionRadius'; 59 | 60 | @override 61 | ActionExplosionRadius build(Map? params) => 62 | ActionExplosionRadius( 63 | params!['radius'], 64 | immunePieces: params['immunePieces']?.cast(), 65 | alwaysSuicide: params['alwaysSuicide'] ?? true, 66 | ); 67 | 68 | @override 69 | Map export(ActionExplosionRadius e) => { 70 | 'radius': e.radius, 71 | if (e.immunePieces?.isNotEmpty ?? false) 'immunePieces': e.immunePieces, 72 | if (!e.alwaysSuicide) 'alwaysSuicide': e.alwaysSuicide, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/regions/regions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:bishop/bishop.dart'; 3 | 4 | part 'area.dart'; 5 | part 'built_region.dart'; 6 | part 'rect_region.dart'; 7 | part 'intersect_region.dart'; 8 | part 'region_effect.dart'; 9 | part 'set_region.dart'; 10 | part 'subtract_region.dart'; 11 | part 'union_region.dart'; 12 | part 'xor_region.dart'; 13 | 14 | abstract class Region { 15 | const Region(); 16 | bool contains(int file, int rank); 17 | Iterable squares(BoardSize size); 18 | Region translate(int x, int y); 19 | } 20 | 21 | abstract class BoardRegion extends Region { 22 | const BoardRegion(); 23 | factory BoardRegion.fromJson(Map json) { 24 | String? type = json['type']; 25 | if (type != null) { 26 | return _builders[json['type']]!(json); 27 | } 28 | return RectRegion.fromJson(json); 29 | } 30 | 31 | static const _builders = { 32 | 'rect': RectRegion.fromJson, 33 | 'union': UnionRegion.fromJson, 34 | 'intersect': IntersectRegion.fromJson, 35 | 'sub': SubtractRegion.fromJson, 36 | 'xor': XorRegion.fromJson, 37 | 'set': SetRegion.fromJson, 38 | 'dset': DirectionSetRegion.fromJson, 39 | }; 40 | 41 | Map toJson(); 42 | 43 | @override 44 | BoardRegion translate(int x, int y); 45 | 46 | factory BoardRegion.lrbt(int? l, int? r, int? b, int? t) => 47 | RectRegion.lrbt(l, r, b, t); 48 | 49 | factory BoardRegion.square(int file, int rank) => 50 | RectRegion.square(file, rank); 51 | 52 | factory BoardRegion.rank(int rank) => RectRegion.rank(rank); 53 | factory BoardRegion.file(int file) => RectRegion.file(file); 54 | 55 | BuiltRegion build(BoardSize size) => 56 | BuiltRegion(squares(size).toList(), size); 57 | 58 | UnionRegion operator +(BoardRegion other) => UnionRegion([ 59 | this, 60 | if (other is UnionRegion) ...other.regions else other, 61 | ]); 62 | IntersectRegion operator &(BoardRegion other) => IntersectRegion([ 63 | this, 64 | if (other is IntersectRegion) ...other.regions else other, 65 | ]); 66 | SubtractRegion operator -(BoardRegion other) => SubtractRegion(this, other); 67 | XorRegion operator ^(BoardRegion other) => XorRegion(this, other); 68 | } 69 | 70 | class BoardRegionAdapter extends BishopTypeAdapter { 71 | @override 72 | BoardRegion build(Map? params) => 73 | BoardRegion.fromJson(params!); 74 | 75 | @override 76 | Map export(BoardRegion e) => e.toJson(); 77 | 78 | @override 79 | String get id => 'bishop.region.board'; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/variant/variants/fairy.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// Variants where the focus is on novel piece types. 4 | class FairyVariants { 5 | /// Knights capture like bishops, bishops capture like knights. 6 | static Variant hoppelPoppel() => Variant.standard().withPieces({ 7 | 'N': PieceType.knibis(), 8 | 'B': PieceType.biskni(), 9 | }).copyWith( 10 | name: 'Hoppel-Poppel', 11 | materialConditions: MaterialConditions.none, 12 | ); 13 | 14 | /// Rooks capture like knights, knights capture like rooks. 15 | static Variant newZealand() => Variant.standard().withPieces({ 16 | 'R': PieceType.rookni(), 17 | 'N': PieceType.kniroo(), 18 | }).copyWith( 19 | name: 'New Zealand Chess', 20 | materialConditions: MaterialConditions.none, 21 | ); 22 | 23 | /// https://en.wikipedia.org/wiki/Grasshopper_chess 24 | static Variant grasshopper() => 25 | Variant.standard().withPiece('G', PieceType.grasshopper()).copyWith( 26 | name: 'Grasshopper Chess', 27 | startPosition: 28 | 'rnbqkbnr/gggggggg/pppppppp/8/8/PPPPPPPP/GGGGGGGG/RNBQKBNR' 29 | ' w KQkq - 0 1', 30 | materialConditions: MaterialConditions.none, 31 | ); 32 | 33 | /// Knights are replaced with nightriders. 34 | static Variant nightrider() => Variant.standard() 35 | .copyWith( 36 | name: 'Nightrider Chess', 37 | materialConditions: MaterialConditions.none, 38 | ) 39 | .withPiece('N', PieceType.nightrider()); 40 | 41 | /// https://en.wikipedia.org/wiki/Berolina_pawn#Berolina_chess 42 | static Variant berolina() => Variant.standard() 43 | .withPiece('P', PieceType.berolinaPawn()) 44 | .copyWith(name: 'Berolina Chess'); 45 | 46 | /// https://en.wikipedia.org/wiki/Wolf_chess 47 | static Variant wolf() => Variant( 48 | name: 'Wolf Chess', 49 | boardSize: const BoardSize(8, 10), 50 | startPosition: 'qwfrbbnk/pssppssp/1pp2pp1/8/8' 51 | '/8/8/1PP2PP1/PSSPPSSP/KNBBRFWQ w - - 0 1', 52 | pieceTypes: { 53 | ...Bishop.chessPieces, 54 | 'W': PieceType.chancellor(), // Wolf 55 | 'F': PieceType.archbishop(), // Fox 56 | 'S': PieceType.fromBetza( 57 | 'fKifmnD', 58 | enPassantable: true, 59 | promoOptions: 60 | PiecePromoOptions.promotesTo(['B', 'N', 'R', 'Q', 'W', 'F']), 61 | ), // Sergeant 62 | 'N': PieceType.nightrider(), 63 | 'E': PieceType.fromBetza('QN0', value: 1400), // Elephant 64 | }, 65 | enPassant: true, 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/state/state_transformer.dart: -------------------------------------------------------------------------------- 1 | part of 'state.dart'; 2 | 3 | typedef StateTransformFunction = T? Function( 4 | BishopState state, [ 5 | int? player, 6 | ]); 7 | 8 | abstract class StateTransformer { 9 | StateTransformFunction? build(BuiltVariant variant); 10 | } 11 | 12 | class StateTransformerPair implements StateTransformer { 13 | final StateTransformer? white; 14 | final StateTransformer? black; 15 | 16 | const StateTransformerPair(this.white, this.black); 17 | 18 | @override 19 | StateTransformFunction? build(BuiltVariant variant) { 20 | final List built = [ 21 | white?.build(variant), 22 | black?.build(variant), 23 | ]; 24 | return (state, [player]) => 25 | player == null ? null : built[player]?.call(state, player); 26 | } 27 | } 28 | 29 | class MaskStateTransformer implements StateTransformer { 30 | final List Function(BishopState state) maskBuilder; 31 | const MaskStateTransformer(this.maskBuilder); 32 | 33 | @override 34 | StateTransformFunction build(BuiltVariant variant) => (state, [player]) => 35 | MaskedState.mask(mask: maskBuilder(state), state: state); 36 | } 37 | 38 | class VisionAreaStateTransformer implements StateTransformer { 39 | final Area area; 40 | const VisionAreaStateTransformer({this.area = Area.radius1}); 41 | 42 | @override 43 | StateTransformFunction build(BuiltVariant variant) => 44 | (state, [player]) => player == null 45 | ? null 46 | : MaskedState.mask( 47 | mask: buildMask( 48 | variant.boardSize, 49 | visibleSquares( 50 | board: state.board, 51 | size: variant.boardSize, 52 | player: player, 53 | area: area, 54 | ), 55 | ), 56 | state: state, 57 | ); 58 | } 59 | 60 | class HideFlagsStateTransformer implements StateTransformer { 61 | final bool forSelf; 62 | final bool forOpponent; 63 | 64 | const HideFlagsStateTransformer({ 65 | this.forSelf = false, 66 | this.forOpponent = true, 67 | }); 68 | 69 | @override 70 | StateTransformFunction build(BuiltVariant variant) => 71 | (state, [player]) => player == null 72 | ? null 73 | : state.copyWith( 74 | board: state.board.map((e) { 75 | if (e.isEmpty) return e; 76 | if ((forSelf && e.colour == player) || 77 | (forOpponent && e.colour == player.opponent)) { 78 | return e.removeFlags(); 79 | } 80 | return e; 81 | }).toList(), 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/square.dart: -------------------------------------------------------------------------------- 1 | import 'constants.dart'; 2 | 3 | // Square anatomy 4 | // [00-01] 2 bit: colour (0: white, 1: black) 5 | // [02-09] 8 bits: piece type 6 | // [10-17] 8 bits: internal piece type 7 | // [18] 1 bit: initial state flag 8 | // [19-31] 13 bits: flags in reality probably 45 bits 9 | 10 | typedef Square = int; 11 | 12 | /// Contains methods for dealing with the internal anatomy of the 13 | /// square representation. 14 | extension SquareLogic on int { 15 | /// Colour only. 16 | int get colour => this & 3; 17 | 18 | /// Piece type only. 19 | int get type => (this >> 2) & 255; 20 | 21 | /// Internal type only. 22 | int get internalType => (this >> 10) & 255; 23 | 24 | /// Colour and piece type. 25 | int get piece => this & 1023; 26 | 27 | /// Flags only. 28 | int get flags => this >> Bishop.flagsStartBit; 29 | 30 | /// Whether there is a piece. 31 | bool get isEmpty => type == 0; 32 | 33 | /// Whether there isn't a piece. 34 | bool get isNotEmpty => type != 0; 35 | 36 | /// Whether there is an internal type. 37 | bool get hasInternalType => internalType != 0; 38 | bool get inInitialState => hasBit(Bishop.initialStateBit); 39 | int setBit(int bit) => this | (1 << bit); 40 | int unsetBit(int bit) => this & ~(1 << bit); 41 | int toggleBit(int bit) => this ^ (1 << bit); 42 | bool hasBit(int bit) => (this & (1 << bit)) != 0; 43 | int setFlag(int flag) => setBit(Bishop.flagsStartBit + flag); 44 | int unsetFlag(int flag) => unsetBit(Bishop.flagsStartBit + flag); 45 | int toggleFlag(int flag) => toggleBit(Bishop.flagsStartBit + flag); 46 | bool hasFlag(int flag) => hasBit(Bishop.flagsStartBit + flag); 47 | int removeFlags() => this & ((1 << Bishop.flagsStartBit) - 1); 48 | int setInternalType(int type) => makePiece( 49 | this.type, 50 | colour, 51 | internalType: type, 52 | initialState: inInitialState, 53 | flags: flags, 54 | ); 55 | int setInitialState(bool initial) => initial 56 | ? setBit(Bishop.initialStateBit) 57 | : unsetBit(Bishop.initialStateBit); 58 | int flipColour() => this ^ 1; 59 | } 60 | 61 | int makeFlags(List flags) => flags.fold(0, (p, e) => p + (1 << e)); 62 | 63 | int makePiece( 64 | int piece, 65 | int colour, { 66 | int internalType = 0, 67 | bool initialState = false, 68 | int flags = 0, 69 | }) => 70 | (flags << Bishop.flagsStartBit) + 71 | (initialState ? 1 << Bishop.initialStateBit : 0) + 72 | (internalType << 10) + 73 | (piece << 2) + 74 | colour; 75 | 76 | String fileSymbol(int file) => String.fromCharCode(Bishop.asciiA + file); 77 | int fileFromSymbol(String symbol) => 78 | symbol.toLowerCase().codeUnits[0] - Bishop.asciiA; 79 | -------------------------------------------------------------------------------- /test/hopper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/expect.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | void main() { 6 | group('Hopper Pieces', () { 7 | List tests = [ 8 | const HopperTest( 9 | name: 'Cannon - Simple', 10 | fen: '4c3/8/1k6/8/8/2K1c3/8/4C3 w - - 0 1', 11 | numMoves: 9, 12 | ), 13 | const HopperTest( 14 | name: 'Cannon - Simple, Mate', 15 | fen: '4c3/8/2k5/8/8/2K1c3/8/4C3 w - - 0 1', 16 | numMoves: 9, 17 | checkmate: true, 18 | ), 19 | const HopperTest( 20 | name: 'Cannon - Complex', 21 | fen: '4c3/8/1k1C4/8/8/2K1c3/8/4C3 w - - 0 1', 22 | numMoves: 20, // Cd3 mates yourself 23 | ), 24 | const HopperTest( 25 | name: 'Cannon - Complex, Mate', 26 | fen: '8/8/1k1Cc3/8/8/2K1c3/8/4C3 w - - 0 1', 27 | numMoves: 16, 28 | checkmate: true, 29 | ), 30 | const HopperTest( 31 | name: 'Grasshopper - Simple', 32 | fen: '8/8/2k5/4g3/8/2K5/8/4G3 w - - 0 1', 33 | numMoves: 2, 34 | ), 35 | const HopperTest( 36 | name: 'Grasshopper - Simple, Mate', 37 | fen: '8/8/2kG4/4g3/8/2K5/8/4G3 w - - 0 1', 38 | numMoves: 4, 39 | checkmate: true, 40 | ), 41 | const HopperTest( 42 | name: 'Grasshopper - Complex', 43 | fen: '4g3/8/8/2k5/4g3/2K5/8/2gGG3 w - - 0 1', 44 | numMoves: 5, 45 | ), 46 | const HopperTest( 47 | name: 'Grasshopper - Complex, Mate', 48 | fen: '4g3/8/8/2kG4/4g3/2K5/8/2gGG3 w - - 0 1', 49 | numMoves: 8, 50 | checkmate: true, 51 | ), 52 | ]; 53 | Variant v = Variant( 54 | name: 'Cannon Test', 55 | startPosition: '4k3/8/8/8/8/8/8/4K3 w - - 0 1', 56 | materialConditions: MaterialConditions.none, 57 | pieceTypes: { 58 | 'K': PieceType.staticKing(), 59 | 'G': PieceType.grasshopper(), 60 | 'C': Xiangqi.cannon(), 61 | }, 62 | ); 63 | for (HopperTest t in tests) { 64 | test(t.name, () { 65 | Game g = Game(variant: v, fen: t.fen); 66 | final moves = g.generateLegalMoves(); 67 | final sanMoves = moves.map((e) => g.toSan(e)).toList(); 68 | bool checkmate = sanMoves.where((e) => e.endsWith('#')).isNotEmpty; 69 | expect(moves.length, t.numMoves); 70 | expect(checkmate, t.checkmate); 71 | }); 72 | } 73 | }); 74 | } 75 | 76 | class HopperTest { 77 | final String name; 78 | final String fen; 79 | final int numMoves; 80 | final bool checkmate; 81 | const HopperTest({ 82 | required this.name, 83 | required this.fen, 84 | required this.numMoves, 85 | this.checkmate = false, 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/pieces_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | List countMoveTests = [ 6 | const CountMovesTest('N', 8), 7 | const CountMovesTest('Q', 8), 8 | const CountMovesTest('B', 4), 9 | const CountMovesTest('fmWfceFifmnD', 4), // pawn 10 | const CountMovesTest('BN', 12), 11 | const CountMovesTest('FWDA', 16), // musketeer elephant 12 | const CountMovesTest('B2ND', 16), // musketeer spider 13 | const CountMovesTest('FWDsN', 16), // musketeer cannon 14 | const CountMovesTest('rbB', 1), 15 | const CountMovesTest('fR', 1), 16 | const CountMovesTest('bB', 2), 17 | const CountMovesTest('fsN', 2), 18 | const CountMovesTest('vZ', 4), 19 | const CountMovesTest('lfC', 1), 20 | const CountMovesTest('flrbN', 2), // works but should it? 21 | ]; 22 | List moveTests = [ 23 | const MoveTest('ffN', ['c6', 'e6']), 24 | const MoveTest('lfN', ['c6']), 25 | const MoveTest('lhN', ['c6', 'b5', 'b3', 'c2']), 26 | const MoveTest('rfZ', ['f7']), 27 | const MoveTest('bhC', ['c1', 'e1', 'g3', 'a3']), 28 | const MoveTest('rbF1', ['e3']), 29 | const MoveTest('vN', ['c2', 'c6', 'e2', 'e6']), 30 | const MoveTest('fslbC', ['a5', 'g5', 'c1']), 31 | const MoveTest('(4,1)', ['c8', 'e8', 'h3', 'h5']), 32 | const MoveTest('r(4,3)', ['h1', 'h7']), 33 | const MoveTest('frN2rfN', ['f5', 'h6', 'e6']), 34 | const MoveTest( 35 | 'N0', 36 | ['b8', 'f8', 'c6', 'e6', 'h6', 'b5', 'f5', 'b3', 'f3', 'c2', 'e2', 'h2'], 37 | ), 38 | ]; 39 | group('Pieces', () { 40 | for (CountMovesTest t in countMoveTests) { 41 | test('Count Moves - ${t.betza}', () { 42 | PieceType pt = PieceType.fromBetza(t.betza); 43 | expect(pt.moves.length, t.num); 44 | }); 45 | } 46 | for (MoveTest t in moveTests) { 47 | test('Move Test - ${t.betza}', () { 48 | final v = Variant( 49 | name: 'Test', 50 | startPosition: '1k6/8/8/8/3T4/8/8/6K1 w KQkq - 0 1', 51 | pieceTypes: { 52 | 'K': PieceType.staticKing(), 53 | 'T': PieceType.fromBetza(t.betza), 54 | }, 55 | ); 56 | final g = Game(variant: v); 57 | final moves = g 58 | .generateLegalMoves() 59 | .from(g.size.squareNumber('d4')) 60 | .map((e) => g.size.squareName(e.to)) 61 | .toList(); 62 | expect(moves, unorderedEquals(t.targets)); 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | class CountMovesTest { 69 | final String betza; 70 | final int num; 71 | const CountMovesTest(this.betza, this.num); 72 | } 73 | 74 | class MoveTest { 75 | final String betza; 76 | final List targets; 77 | const MoveTest(this.betza, this.targets); 78 | } 79 | -------------------------------------------------------------------------------- /example/json.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:bishop/bishop.dart'; 5 | import 'package:nice_json/nice_json.dart'; 6 | 7 | import 'play.dart'; 8 | 9 | Map readJson(String filename) { 10 | final data = File(filename).readAsStringSync(); 11 | return jsonDecode(data); 12 | } 13 | 14 | void writeJson(String filename, Map data) { 15 | final file = File(filename); 16 | final json = niceJson(data); 17 | file.writeAsStringSync(json); 18 | } 19 | 20 | void main(List args) { 21 | if (args.isNotEmpty && args.contains('export')) { 22 | List vlist = [...args]; 23 | vlist.remove('export'); 24 | List variants = vlist.isEmpty 25 | ? Variants.values 26 | : vlist 27 | .map( 28 | (e) => Variants.values 29 | .firstWhere((v) => e.toLowerCase() == v.name.toLowerCase()), 30 | ) 31 | .toList(); 32 | for (Variants v in variants) { 33 | String filename = 'json/${v.name}.json'; 34 | writeJson(filename, v.build().toJson(verbose: false)); 35 | printMagenta('Wrote $filename'); 36 | } 37 | return; 38 | } 39 | 40 | final variant = Variant( 41 | name: 'Example', 42 | description: 'An example variant for JSON serialisation', 43 | boardSize: const BoardSize(3, 5), 44 | startPosition: 'nkn/ppp/3/PPP/NKN w - - 0 1', 45 | castlingOptions: CastlingOptions.none, 46 | enPassant: false, 47 | pieceTypes: { 48 | 'K': PieceType.king(), 49 | 'N': PieceType.knight(), 50 | 'P': PieceType.simplePawn(), 51 | }, 52 | actions: [ActionDoesNothing(), ActionDoesNothing(something: 'pawn')], 53 | adapters: [DoesNothingAdapter()], 54 | ); 55 | writeJson('example_variant.json', variant.toJson()); 56 | 57 | final json = readJson('example_variant.json'); 58 | final v = Variant.fromJson(json, adapters: [DoesNothingAdapter()]); 59 | print(v.actions); 60 | Game g = Game(variant: v); 61 | print(g.ascii()); 62 | print(g.generateLegalMoves().map((e) => g.toSan(e)).toList()); 63 | } 64 | 65 | class ActionDoesNothing extends Action { 66 | final String? something; 67 | ActionDoesNothing({this.something}) 68 | : super(action: ActionDefinitions.pass([])); 69 | 70 | @override 71 | String toString() => 'ActionDoesNothing($something)'; 72 | } 73 | 74 | class DoesNothingAdapter extends BishopTypeAdapter { 75 | @override 76 | String get id => 'example.action.doesNothing'; 77 | 78 | @override 79 | ActionDoesNothing build(Map? params) => 80 | ActionDoesNothing(something: params?['something']); 81 | 82 | @override 83 | Map export(ActionDoesNothing e) => { 84 | if (e.something != null) 'something': e.something, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /test/region_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | import 'package:test/expect.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | import 'constants.dart'; 6 | 7 | void main() { 8 | group('Regions', () { 9 | final r1 = BoardRegion.lrbt( 10 | Bishop.fileA, 11 | Bishop.fileC, 12 | Bishop.rank1, 13 | Bishop.rank3, 14 | ); 15 | final r2 = BoardRegion.lrbt( 16 | Bishop.fileC, 17 | Bishop.fileE, 18 | Bishop.rank3, 19 | Bishop.rank5, 20 | ); 21 | final vv = Variant( 22 | name: 'Region Test Variant', 23 | boardSize: BoardSize.standard, 24 | startPosition: Positions.standardDefault, 25 | pieceTypes: {'B': PieceType.bishop()}, 26 | regions: { 27 | 'union': UnionRegion([r1, r2]), 28 | 'inter': IntersectRegion([r1, r2]), 29 | }, 30 | ); 31 | List tests = [ 32 | const InRegionTest( 33 | region: 'redPalace', 34 | square: 'd1', 35 | inRegion: true, 36 | ), 37 | const InRegionTest( 38 | region: 'redPalace', 39 | square: 'f6', 40 | inRegion: false, 41 | ), 42 | const InRegionTest( 43 | region: 'blackSide', 44 | square: 'h7', 45 | inRegion: true, 46 | ), 47 | const InRegionTest( 48 | region: 'redSide', 49 | square: 'h7', 50 | inRegion: false, 51 | ), 52 | InRegionTest( 53 | variant: vv, 54 | region: 'union', 55 | square: 'a2', 56 | inRegion: true, 57 | ), 58 | InRegionTest( 59 | variant: vv, 60 | region: 'union', 61 | square: 'b4', 62 | inRegion: false, 63 | ), 64 | InRegionTest( 65 | variant: vv, 66 | region: 'inter', 67 | square: 'a2', 68 | inRegion: false, 69 | ), 70 | InRegionTest( 71 | variant: vv, 72 | region: 'inter', 73 | square: 'c3', 74 | inRegion: true, 75 | ), 76 | ]; 77 | for (InRegionTest t in tests) { 78 | test('Region test: ${t.region}/${t.square}', () { 79 | Variant v = t.variant ?? Xiangqi.variant(); 80 | final size = v.boardSize; 81 | BoardRegion region = v.regions[t.region]!; 82 | int square = size.squareNumber(t.square); 83 | bool inRegion = size.inRegion(square, region); 84 | expect(inRegion, t.inRegion); 85 | }); 86 | } 87 | }); 88 | } 89 | 90 | class InRegionTest { 91 | final Variant? variant; 92 | final String region; 93 | final String square; 94 | final bool inRegion; 95 | const InRegionTest({ 96 | this.variant, 97 | required this.region, 98 | required this.square, 99 | required this.inRegion, 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/variant/options/output_options.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// Allows output options (FEN, PGN) to be specified for variants. 4 | /// This will become more significant when we support variants with more complex FENs. 5 | class OutputOptions { 6 | /// Define the format to be used when outputting castling rights. 7 | final CastlingFormat castlingFormat; 8 | 9 | /// If true, a tilde (~) will be placed after promoted piece symbols. 10 | final bool showPromoted; 11 | 12 | /// If true, the castling field in the FEN string will be combined with a list 13 | /// of files where the starting pieces haven't moved from them. 14 | /// For example, to be used with Seirawan gates. 15 | final bool virginFiles; 16 | 17 | const OutputOptions({ 18 | this.castlingFormat = CastlingFormat.standard, 19 | this.showPromoted = false, 20 | this.virginFiles = false, 21 | }); 22 | 23 | factory OutputOptions.fromJson(Map json) => OutputOptions( 24 | castlingFormat: CastlingFormat.values 25 | .firstWhere((e) => e.name == json['castlingFormat']), 26 | showPromoted: json['showPromoted'], 27 | virginFiles: json['virginFiles'], 28 | ); 29 | 30 | Map toJson() => { 31 | 'castlingFormat': castlingFormat.name, 32 | 'showPromoted': showPromoted, 33 | 'virginFiles': virginFiles, 34 | }; 35 | 36 | static const standard = 37 | OutputOptions(castlingFormat: CastlingFormat.standard); 38 | static const chess960 = 39 | OutputOptions(castlingFormat: CastlingFormat.shredder); 40 | static const crazyhouse = OutputOptions( 41 | castlingFormat: CastlingFormat.standard, 42 | showPromoted: true, 43 | ); 44 | static const seirawan = OutputOptions( 45 | castlingFormat: CastlingFormat.standard, 46 | virginFiles: true, 47 | ); 48 | 49 | @override 50 | int get hashCode => 51 | castlingFormat.hashCode ^ 52 | showPromoted.hashCode << 1 ^ 53 | virginFiles.hashCode << 2; 54 | 55 | @override 56 | bool operator ==(Object other) => 57 | other is OutputOptions && hashCode == other.hashCode; 58 | } 59 | 60 | /// Determines how castling rights are represented in FEN strings. 61 | /// There are certain positions in variants such as Chess960, in which the 62 | /// standard 'KQkq' format could present an ambiguity. 63 | /// See [this link](https://en.wikipedia.org/wiki/Fischer_random_chess#Coding_games_and_positions) 64 | /// for more details. 65 | enum CastlingFormat { 66 | /// The standard castling format labels rights as 'KQkq', regardless of the 67 | /// position of the rooks. 68 | standard, 69 | 70 | /// Uses the letters for the files that the rooks start on to represent 71 | /// castling rights. For example, the default chess position's castling rights 72 | /// would be rendered as 'HAha'. 73 | shredder, 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/move/move_processor.dart: -------------------------------------------------------------------------------- 1 | import 'package:bishop/bishop.dart'; 2 | 3 | class MoveProcessorParams { 4 | final BishopState state; 5 | final T move; 6 | final Zobrist zobrist; 7 | const MoveProcessorParams({ 8 | required this.state, 9 | required this.move, 10 | required this.zobrist, 11 | }); 12 | } 13 | 14 | typedef MoveProcessorFunction = BishopState? Function( 15 | MoveProcessorParams params, 16 | ); 17 | 18 | typedef EffectMoveFunction = List Function( 19 | MoveProcessorParams params, 20 | ); 21 | 22 | abstract class MoveProcessor { 23 | MoveProcessorFunction build(BuiltVariant variant); 24 | 25 | Type get type => T; 26 | } 27 | 28 | /// A move processor that modifies the state using the effects system. 29 | /// You should probably use this or [ActionMoveProcessor] unless you have 30 | /// a complex special case 31 | class EffectMoveProcessor implements MoveProcessor { 32 | final EffectMoveFunction Function(BuiltVariant variant) builder; 33 | const EffectMoveProcessor(this.builder); 34 | @override 35 | Type get type => T; 36 | 37 | @override 38 | MoveProcessorFunction build(BuiltVariant variant) => (params) { 39 | final effects = builder(variant)(params); 40 | return params.state.applyEffects( 41 | effects: effects, 42 | size: variant.boardSize, 43 | zobrist: params.zobrist, 44 | ); 45 | }; 46 | } 47 | 48 | /// A move processor that modifies the state using an [action]. 49 | class ActionMoveProcessor extends EffectMoveProcessor { 50 | final ActionDefinition action; 51 | ActionMoveProcessor(this.action) 52 | : super( 53 | (variant) => (params) => action( 54 | ActionTrigger( 55 | event: ActionEvent.beforeMove, 56 | variant: variant, 57 | state: params.state, 58 | move: params.move, 59 | piece: params.move.promoPiece ?? 60 | params.move.dropPiece ?? 61 | params.state.board[params.move.from], 62 | ), 63 | ), 64 | ); 65 | } 66 | 67 | /// A move processor that handles a `StaticMove`. 68 | /// On moving, the piece will be detonated, destroying all pieces in [area] 69 | /// around it. Essentially this is like an atomic move without moving. 70 | /// Used for Beirut Chess. 71 | class DetonateMoveProcessor extends ActionMoveProcessor { 72 | final Area area; 73 | final List? immunePieces; 74 | final bool alwaysSuicide; 75 | 76 | DetonateMoveProcessor({ 77 | this.area = Area.radius1, 78 | this.immunePieces, 79 | this.alwaysSuicide = true, 80 | }) : super( 81 | ActionDefinitions.explosion( 82 | area, 83 | immunePieces: immunePieces, 84 | alwaysSuicide: alwaysSuicide, 85 | ), 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/actions/definitions.dart: -------------------------------------------------------------------------------- 1 | part of 'actions.dart'; 2 | 3 | /// Some common actions. 4 | class ActionDefinitions { 5 | /// Merges several [actions] into a single definition. 6 | static ActionDefinition merge(List actions) => 7 | (ActionTrigger trigger) => 8 | actions.map((e) => e(trigger)).expand((e) => e).toList(); 9 | 10 | /// Tranforms a [condition] into an action definition. 11 | static ActionDefinition transformCondition( 12 | ActionCondition condition, 13 | List Function(bool result) transformer, 14 | ) => 15 | (ActionTrigger trigger) => transformer(condition(trigger)); 16 | 17 | /// An action that removes every piece in [area] around the destination 18 | /// square of a move. 19 | static ActionDefinition explosion( 20 | Area area, { 21 | List? immunePieces, 22 | bool alwaysSuicide = true, 23 | }) => 24 | (ActionTrigger trigger) => trigger.variant.boardSize 25 | .squaresForArea(trigger.move.to, area) 26 | .where((e) { 27 | int sq = trigger.state.board[e]; 28 | if (sq.isEmpty) return false; 29 | if (e == trigger.move.to && alwaysSuicide) { 30 | return true; 31 | } 32 | if (immunePieces != null) { 33 | if (immunePieces 34 | .contains(trigger.variant.pieces[sq.type].symbol)) { 35 | return false; 36 | } 37 | } 38 | return true; 39 | }) 40 | .map((e) => EffectModifySquare(e, Bishop.empty)) 41 | .toList(); 42 | 43 | /// An action that adds a piece of [type] to the moving player's hand. 44 | static ActionDefinition addToHand( 45 | String type, { 46 | bool forOpponent = false, 47 | int count = 1, 48 | }) => 49 | (ActionTrigger trigger) => [ 50 | ...List.filled( 51 | count, 52 | EffectAddToHand( 53 | forOpponent 54 | ? trigger.piece.colour.opponent 55 | : trigger.piece.colour, 56 | trigger.variant.pieceIndexLookup[type]!, 57 | ), 58 | ), 59 | ]; 60 | 61 | /// An action that removes a piece of [type] from the moving player's hand. 62 | static ActionDefinition removeFromHand( 63 | String type, { 64 | bool forOpponent = false, 65 | int count = 1, 66 | }) => 67 | (ActionTrigger trigger) => [ 68 | ...List.filled( 69 | count, 70 | EffectRemoveFromHand( 71 | forOpponent 72 | ? trigger.piece.colour.opponent 73 | : trigger.piece.colour, 74 | trigger.variant.pieceIndexLookup[type]!, 75 | ), 76 | ), 77 | ]; 78 | 79 | /// Simply output the effects regardless of the trigger. 80 | static ActionDefinition pass(List effects) => (_) => effects; 81 | 82 | /// Invalidates the move, regardless of the trigger. 83 | static ActionDefinition get invalidateMove => 84 | pass(const [EffectInvalidateMove()]); 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/variant/variants/shogi.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// This is a work in progress. 4 | /// Many rules, specifically those related to dropping, are not implemented yet. 5 | class Shogi { 6 | static const defaultFen = 7 | 'lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[] w - - 0 1'; 8 | 9 | static PieceType pawn() => 10 | PieceType.fromBetza('fW', promoOptions: promotesToGold); 11 | static PieceType silver() => 12 | PieceType.fromBetza('FfW', promoOptions: promotesToGold); 13 | static PieceType lance() => 14 | PieceType.fromBetza('fR', promoOptions: promotesToGold); 15 | static PieceType knight() => 16 | PieceType.fromBetza('fN', promoOptions: promotesToGold); 17 | static PieceType gold() => PieceType.fromBetza('WfF'); 18 | 19 | static PieceType bishop() => PieceType.fromBetza( 20 | 'B', 21 | promoOptions: PiecePromoOptions.promotesToOne('H'), 22 | ); 23 | static PieceType dragonHorse() => PieceType.fromBetza('WB'); 24 | 25 | static PieceType rook() => PieceType.fromBetza( 26 | 'R', 27 | promoOptions: PiecePromoOptions.promotesToOne('D'), 28 | ); 29 | static PieceType dragonKing() => PieceType.fromBetza('FR'); 30 | 31 | static PiecePromoOptions get promotesToGold => 32 | PiecePromoOptions.promotesToOne('G'); 33 | 34 | static Variant variant() => shogi(); 35 | 36 | static Variant shogi() => Variant( 37 | name: 'Shogi', 38 | boardSize: const BoardSize(9, 9), 39 | pieceTypes: { 40 | 'K': PieceType.king(), 41 | 'N': knight(), 42 | 'S': silver(), 43 | 'L': lance(), 44 | 'P': pawn(), 45 | 'G': gold(), 46 | 'R': rook(), 47 | 'D': dragonKing(), 48 | 'B': bishop(), 49 | 'H': dragonHorse(), 50 | }, 51 | startPosition: defaultFen, 52 | handOptions: HandOptions.captures, 53 | promotionOptions: PromotionOptions.optional( 54 | ranks: [Bishop.rank7, Bishop.rank3], 55 | ), 56 | ); 57 | } 58 | 59 | class Dobutsu { 60 | static const defaultFen = 'gle/1c1/1C1/ELG[-] w - - 0 1'; 61 | 62 | static PieceType giraffe() => 63 | PieceType.fromBetza('W', promoOptions: PiecePromoOptions.none); 64 | static PieceType elephant() => 65 | PieceType.fromBetza('F', promoOptions: PiecePromoOptions.none); 66 | static PieceType chick() => 67 | PieceType.fromBetza('fW', promoOptions: PiecePromoOptions.promotable); 68 | static PieceType hen() => Shogi.gold(); 69 | static PieceType lion() => PieceType.king(); 70 | 71 | static Variant variant() => dobutsu(); 72 | 73 | static Variant dobutsu() => Variant( 74 | name: 'Dobutsu Shogi', 75 | description: 'A simple Shogi variant aimed at children.', 76 | boardSize: const BoardSize(3, 4), 77 | startPosition: defaultFen, 78 | pieceTypes: { 79 | 'L': lion(), 80 | 'G': giraffe(), 81 | 'E': elephant(), 82 | 'C': chick(), 83 | 'H': hen(), 84 | }, 85 | handOptions: const HandOptions( 86 | enableHands: true, 87 | addCapturesToHand: true, 88 | dropBuilder: DropBuilder.unrestricted, 89 | ), 90 | ).withCampMate(); 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/serialisation/promo_adapters.dart: -------------------------------------------------------------------------------- 1 | part of 'serialisation.dart'; 2 | 3 | class NoPromotionAdapter extends BasicAdapter { 4 | const NoPromotionAdapter() : super('bishop.promo.none', NoPromotion.new); 5 | } 6 | 7 | class StandardPromotionAdapter extends BishopTypeAdapter { 8 | @override 9 | String get id => 'bishop.promo.standard'; 10 | 11 | @override 12 | StandardPromotion build(Map? params) => StandardPromotion( 13 | pieceLimits: (params?['pieceLimits'] as Map?)?.map( 14 | (k, e) => MapEntry(k, e as int), 15 | ), 16 | ranks: params?['ranks']?.cast(), 17 | optional: params?['optional'] ?? false, 18 | ); 19 | 20 | @override 21 | Map? export(StandardPromotion e) => { 22 | if (e.pieceLimits != null) 'pieceLimits': e.pieceLimits, 23 | if (e.ranks != null) 'ranks': e.ranks, 24 | if (e.optional != false) 'optional': e.optional, 25 | }; 26 | } 27 | 28 | class OptionalPromotionAdapter extends BishopTypeAdapter { 29 | @override 30 | String get id => 'bishop.promo.optional'; 31 | 32 | @override 33 | OptionalPromotion build(Map? params) => OptionalPromotion( 34 | pieceLimits: (params?['pieceLimits'] as Map?)?.map( 35 | (k, e) => MapEntry(k, e as int), 36 | ), 37 | ranks: params?['ranks']?.cast(), 38 | forced: params?['forced'] ?? true, 39 | forcedRanks: params?['forcedRanks']?.cast(), 40 | ); 41 | 42 | @override 43 | Map? export(OptionalPromotion e) => { 44 | if (e.pieceLimits != null) 'pieceLimits': e.pieceLimits, 45 | if (e.ranks != null) 'ranks': e.ranks, 46 | if (!e.forced) 'forced': e.forced, 47 | if (e.forced && e.forcedRanks != null) 'forcedRanks': e.forcedRanks, 48 | }; 49 | } 50 | 51 | class RegionPromotionAdapter extends BishopTypeAdapter { 52 | @override 53 | RegionPromotion build(Map? params) => RegionPromotion( 54 | whiteRegion: params?['wRegion'] != null || params?['region'] != null 55 | ? BoardRegion.fromJson(params?['wRegion'] ?? params?['region']) 56 | : null, 57 | blackRegion: params?['bRegion'] != null || params?['region'] != null 58 | ? BoardRegion.fromJson(params?['bRegion'] ?? params?['region']) 59 | : null, 60 | whiteId: params?['wId'], 61 | blackId: params?['bId'], 62 | optional: params?['optional'] ?? false, 63 | ); 64 | 65 | @override 66 | Map? export(RegionPromotion e) { 67 | bool same = e.whiteRegion == e.blackRegion && e.whiteRegion != null; 68 | return { 69 | if (same) 'region': e.whiteRegion!.toJson(), 70 | if (e.whiteRegion != null && !same) 'wRegion': e.whiteRegion!.toJson(), 71 | if (e.blackRegion != null && !same) 'bRegion': e.blackRegion!.toJson(), 72 | if (e.whiteId != null && e.whiteRegion == null) 'wId': e.whiteId, 73 | if (e.blackId != null && e.blackRegion == null) 'bId': e.blackId, 74 | if (e.optional) 'optional': e.optional, 75 | }; 76 | } 77 | 78 | @override 79 | String get id => 'bishop.promo.region'; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/engine.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:bishop/bishop.dart'; 4 | 5 | class Engine { 6 | final Game game; 7 | 8 | Engine({required this.game}); 9 | 10 | Future search({ 11 | int maxDepth = 50, 12 | int timeLimit = 5000, 13 | int timeBuffer = 2000, 14 | int debug = 0, 15 | int printBest = 0, 16 | }) async { 17 | if (game.gameOver) { 18 | print(game.drawn ? 'Draw' : 'Checkmate'); 19 | return const EngineResult(); 20 | } 21 | int endTime = DateTime.now().millisecondsSinceEpoch + timeLimit; 22 | int endBuffer = endTime + timeBuffer; 23 | int depthSearched = 0; 24 | List moves = game.generateLegalMoves(); 25 | Map evals = {}; 26 | for (Move m in moves) { 27 | evals[m] = 0; 28 | } 29 | for (int depth = 1; depth < maxDepth; depth++) { 30 | if (debug > 0) print('----- DEPTH $depth -----'); 31 | for (Move m in moves) { 32 | game.makeMove(m, false); 33 | int eval = -negamax( 34 | depth, 35 | -Bishop.mateUpper, 36 | Bishop.mateUpper, 37 | game.turn.opponent, 38 | debug, 39 | ); 40 | game.undo(); 41 | evals[m] = eval; 42 | int now = DateTime.now().millisecondsSinceEpoch; 43 | if (now >= endBuffer) break; 44 | } 45 | moves.sort((a, b) => evals[b]!.compareTo(evals[a]!)); 46 | depthSearched = depth; 47 | int now = DateTime.now().millisecondsSinceEpoch; 48 | if (now >= endTime) break; 49 | } 50 | if (printBest > 0) { 51 | print('-- Best Moves --'); 52 | for (Move m in moves.take(printBest)) { 53 | print('${game.toSan(m)}: ${evals[m]}'); 54 | } 55 | } 56 | 57 | return EngineResult( 58 | move: moves.first, 59 | eval: evals[moves.first], 60 | depth: depthSearched, 61 | ); 62 | } 63 | 64 | int negamax(int depth, int alpha, int beta, Colour player, [int debug = 0]) { 65 | int value = -Bishop.mateUpper; 66 | if (game.drawn) return 0; 67 | final result = game.result; 68 | if (result is WonGame) { 69 | return result.winner == player ? -Bishop.mateUpper : Bishop.mateUpper; 70 | } 71 | List moves = game.generateLegalMoves(); 72 | if (moves.isEmpty) { 73 | if (game.turn == player) { 74 | return Bishop.mateUpper; 75 | } else { 76 | return -Bishop.mateUpper; 77 | } 78 | } 79 | // -evaluate because we are currently looking at this asthe other player 80 | if (depth == 0) return -game.evaluate(player); 81 | if (moves.isNotEmpty) { 82 | int a = alpha; 83 | for (Move m in moves) { 84 | game.makeMove(m, false); 85 | int v = -negamax(depth - 1, -beta, -a, player.opponent, debug - 1); 86 | game.undo(); 87 | value = max(v, value); 88 | a = max(a, value); 89 | if (a >= beta) break; 90 | } 91 | } 92 | return value; 93 | } 94 | } 95 | 96 | class EngineResult { 97 | final Move? move; 98 | final int? eval; 99 | final int? depth; 100 | 101 | bool get hasMove => move != null; 102 | 103 | const EngineResult({this.move, this.eval, this.depth}); 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/actions/actions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:bishop/bishop.dart'; 3 | 4 | part 'conditions.dart'; 5 | part 'definitions.dart'; 6 | part 'effects.dart'; 7 | part 'events.dart'; 8 | part 'trigger.dart'; 9 | 10 | /// A function that generates a `List` based on an 11 | /// `ActionTrigger` passed to it. 12 | typedef ActionDefinition = List Function(ActionTrigger trigger); 13 | 14 | /// A function that validates an `ActionTrigger`. 15 | typedef ActionCondition = bool Function(ActionTrigger trigger); 16 | 17 | class Action { 18 | /// The type of event that will trigger this action. 19 | final ActionEvent event; 20 | 21 | /// The [precondition] is checked before any action in the group is executed. 22 | /// In other words, the [precondition] will act on the state of the game, 23 | /// ignoring any changes that happen as a result of actions executed before 24 | /// this one, that are triggered by the same event. 25 | final ActionCondition? precondition; 26 | 27 | /// The [condition] is checked before executing the action in its sequence, i.e. 28 | /// effects applied by actions occurring before this in the sequence will be 29 | /// taken into account by the [condition], in contrast with [precondition]. 30 | final ActionCondition? condition; 31 | 32 | /// A function that generates a `List` based on an 33 | /// `ActionTrigger` passed to it. 34 | final ActionDefinition action; 35 | 36 | const Action({ 37 | this.event = ActionEvent.afterMove, 38 | this.precondition, 39 | this.condition, 40 | required this.action, 41 | }); 42 | 43 | Action copyWith({ 44 | ActionEvent? event, 45 | ActionCondition? precondition, 46 | ActionCondition? condition, 47 | ActionDefinition? action, 48 | }) => 49 | Action( 50 | event: event ?? this.event, 51 | precondition: precondition ?? this.precondition, 52 | condition: condition ?? this.condition, 53 | action: action ?? this.action, 54 | ); 55 | 56 | factory Action.explodeOnCapture(Area area) => ActionExplodeOnCapture(area); 57 | factory Action.explosionRadius(int radius) => ActionExplosionRadius(radius); 58 | 59 | /// The flying generals rule from Xiangqi. If the generals/kings are facing 60 | /// each other, with no pieces between, the move will be invalidated. 61 | /// Set [activeCondition] to true if you have other actions that might modify 62 | /// the board before this. 63 | factory Action.flyingGenerals({bool activeCondition = false}) => 64 | ActionFlyingGenerals(activeCondition: activeCondition); 65 | 66 | /// Copies the Action with the added condition that the piece type is [type]. 67 | /// Used internally when building variants, to enable convenience actions on 68 | /// piece type definitions. 69 | Action forPieceType(int type) => Action( 70 | event: event, 71 | action: action, 72 | precondition: Conditions.merge([ 73 | Conditions.movingPieceType(type), 74 | if (precondition != null) precondition!, 75 | ]), 76 | condition: condition, 77 | ); 78 | 79 | /// Swaps [precondition] and [condition]. 80 | Action swapConditions() => Action( 81 | event: event, 82 | precondition: condition, 83 | condition: precondition, 84 | action: action, 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/navigator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bishop/bishop.dart'; 3 | 4 | class GameNavigator { 5 | GameNavigator({ 6 | Game? game, 7 | Variant? variant, 8 | String? startPosition, 9 | bool startAtEnd = false, 10 | }) : _game = game ?? Game(variant: variant, fen: startPosition) { 11 | NavigatorNode? cur; 12 | for (final state in _game.history) { 13 | final n = NavigatorNode(gameState: state, parent: cur); 14 | cur?.addChild(n); 15 | if (cur == null || startAtEnd) line.add(n); 16 | cur = n; 17 | } 18 | } 19 | 20 | factory GameNavigator.fromPgn(String pgn, {bool startAtEnd = false}) => 21 | GameNavigator(game: parsePgn(pgn).buildGame(), startAtEnd: startAtEnd); 22 | 23 | final Game _game; 24 | List line = []; 25 | NavigatorNode get root => line.first; 26 | NavigatorNode get current => line.last; 27 | List get branches => current.children; 28 | int get index => line.length - 1; 29 | List get mainLine => root.mainLine; 30 | 31 | late final _streamController = StreamController.broadcast(); 32 | Stream get stream => _streamController.stream; 33 | 34 | void _emitState() => _streamController.add(current); 35 | 36 | NavigatorNode? next({ 37 | int branch = 0, 38 | bool move = true, 39 | bool emit = true, 40 | }) { 41 | if (current.children.length < branch + 1) return null; 42 | final node = current.children[branch]; 43 | if (move) { 44 | line.add(node); 45 | } 46 | if (emit) _emitState(); 47 | return node; 48 | } 49 | 50 | NavigatorNode? previous({ 51 | bool move = true, 52 | bool emit = true, 53 | }) { 54 | if (line.length < 2) return null; 55 | final node = line[line.length - 2]; 56 | if (move) { 57 | line.removeLast(); 58 | } 59 | if (emit) _emitState(); 60 | return node; 61 | } 62 | 63 | NavigatorNode go(int target, {bool emit = true}) { 64 | while (index != target) { 65 | final node = index > target ? previous(emit: false) : next(emit: false); 66 | if (node == null) break; 67 | } 68 | if (emit) _emitState(); 69 | return current; 70 | } 71 | 72 | NavigatorNode goToStart() => go(0); 73 | NavigatorNode goToEnd() => go(mainLine.length); 74 | } 75 | 76 | class NavigatorNode { 77 | List children = []; 78 | NavigatorNode? parent; 79 | final BishopState gameState; 80 | 81 | MoveMeta? get moveMeta => gameState.meta?.moveMeta; 82 | String? get moveString => moveMeta != null 83 | ? '${gameState.moveNumber}. ${gameState.turn == Bishop.white ? '..' : ''}${moveMeta!.formatted}' 84 | : null; 85 | 86 | NavigatorNode({ 87 | List? children, 88 | this.parent, 89 | required this.gameState, 90 | }) : children = [...?children]; 91 | 92 | void addChild(NavigatorNode child, {int? position}) => 93 | position == null ? children.add(child) : children.insert(position, child); 94 | 95 | void addChildFirst(NavigatorNode child) => addChild(child, position: 0); 96 | 97 | void setParent(NavigatorNode parent) => this.parent = parent; 98 | 99 | List get mainLine => [ 100 | this, 101 | if (children.isNotEmpty) ...children.first.mainLine, 102 | ]; 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/variant/variants/other.dart: -------------------------------------------------------------------------------- 1 | part of '../variant.dart'; 2 | 3 | /// Games that don't really resemble Chess or any of the other main variants. 4 | abstract class OtherGames { 5 | /// Knights only. Move a knight onto the central square and off it again 6 | /// to win. 7 | /// https://en.wikipedia.org/wiki/Jeson_Mor 8 | static Variant jesonMor() => Variant( 9 | name: 'Jeson Mor', 10 | description: 11 | 'Knights only. Move a knight onto the central square and off' 12 | ' it again to win.', 13 | boardSize: const BoardSize(9, 9), 14 | startPosition: 'nnnnnnnnn/9/9/9/9/9/9/9/NNNNNNNNN w - - 0 1', 15 | pieceTypes: {'N': PieceType.knight()}, 16 | promotionOptions: PromotionOptions.none, 17 | actions: [ 18 | ActionExitRegionEnding( 19 | region: RectRegion.square(Bishop.fileE, Bishop.rank5), 20 | ), 21 | ], 22 | halfMoveDraw: 100, 23 | ); 24 | 25 | /// https://en.wikipedia.org/wiki/Clobber 26 | static Variant clobber() => Variant( 27 | name: 'Clobber', 28 | startPosition: 'PpPpP/pPpPp/PpPpP/pPpPp/PpPpP/pPpPp w - - 0 1', 29 | boardSize: const BoardSize(5, 6), 30 | pieceTypes: {'P': PieceType.fromBetza('cW')}, 31 | gameEndConditions: 32 | const GameEndConditions(stalemate: EndType.lose).symmetric(), 33 | promotionOptions: PromotionOptions.none, 34 | ); 35 | 36 | /// https://en.wikipedia.org/wiki/Clobber#Variants 37 | static Variant clobber10() => clobber().copyWith( 38 | name: 'Clobber10', 39 | boardSize: const BoardSize(10, 10), 40 | startPosition: 'PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP/PpPpPpPpPp/' 41 | 'pPpPpPpPpP/PpPpPpPpPp/pPpPpPpPpP' 42 | '/PpPpPpPpPp/pPpPpPpPpP w - - 0 1', 43 | ); 44 | 45 | /// https://en.wikipedia.org/wiki/Breakthrough_(board_game) 46 | static Variant breakthrough() => Variant( 47 | name: 'Breakthrough', 48 | startPosition: 'pppppppp/pppppppp/8/8/8/8/PPPPPPPP/PPPPPPPP w - - 0 1', 49 | pieceTypes: {'P': PieceType.fromBetza('fmWfF')}, 50 | ).withCampMate(winPieces: ['P']); 51 | 52 | /// https://en.wikipedia.org/wiki/Five_Field_Kono 53 | static Variant kono() => Variant( 54 | name: 'Five Field Kono', 55 | boardSize: const BoardSize(5, 5), 56 | startPosition: 'ppppp/p3p/5/P3P/PPPPP w - - 0 1', 57 | // startPosition: 'PPPPP/4P/1P3/5/4p w - - 0 1', 58 | pieceTypes: {'P': PieceType.fromBetza('mF')}, 59 | regions: { 60 | 'w': SetRegion(['a5', 'b5', 'c5', 'd5', 'e5', 'a4', 'e4']), 61 | 'b': SetRegion(['a1', 'b1', 'c1', 'd1', 'e1', 'a2', 'e2']), 62 | }, 63 | promotionOptions: PromotionOptions.none, 64 | ).withAction(ActionFillRegionEnding('w', 'b')); 65 | 66 | /// https://www.chessvariants.com/programs.dir/joust.html 67 | static Variant joust() => Variant( 68 | name: 'Joust', 69 | description: 'The square a piece moves from is removed from the board' 70 | 'after each move.', 71 | startPosition: '8/8/8/4n3/3N4/8/8/8 w - - 0 1', 72 | pieceTypes: {'N': PieceType.fromBetza('mN')}, 73 | gameEndConditions: 74 | const GameEndConditions(stalemate: EndType.lose).symmetric(), 75 | promotionOptions: PromotionOptions.none, 76 | ).withBlocker().withAction(ActionBlockOrigin()); 77 | } 78 | --------------------------------------------------------------------------------