rb_chess 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry-byebug'
4
+ require_relative 'pieces/piece_constants'
5
+ require_relative 'position'
6
+ require_relative 'castling_rights'
7
+ require_relative 'errors'
8
+
9
+ # A class to parse Chess FEN notation
10
+ module RbChess
11
+ class FENParser
12
+ include PieceConstants
13
+
14
+ BOARD_HEIGHT = 8
15
+ BOARD_WIDTH = 8
16
+
17
+ DEFAULT_NOTATION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0'
18
+ FEN_TOKEN_REGEX = /[pkqrbn\d]/i.freeze
19
+
20
+ ERROR_MESSAGES = {
21
+ invalid_token: 'Invalid FEN notation, token not valid',
22
+ invalid_board_width: "Invalid FEN notation, Board width is not equal to #{BOARD_WIDTH}",
23
+ invalid_board_height: "Invalid FEN notation, Board height not equal to #{BOARD_HEIGHT}"
24
+ }.freeze
25
+
26
+ def self.board_to_fen(board)
27
+ pieces = pieces_to_fen(board.pieces)
28
+ active = board.active_color == :white ? 'w' : 'b'
29
+ castling_rights = board.castling_rights.to_s
30
+ en_passant_pos = board.en_passant_square.nil? ? '-' : board.en_passant_square.to_s
31
+ halfmove = board.halfmove_clock.to_s
32
+ fullmove = board.fullmove_no.to_s
33
+
34
+ [pieces, active, castling_rights, en_passant_pos, halfmove, fullmove].join(' ')
35
+ end
36
+
37
+ def self.pieces_to_fen(pieces)
38
+ (0..7).map { |y| build_rank_fen(pieces, y) }.join('/')
39
+ end
40
+
41
+ def self.build_rank_fen(pieces, y)
42
+ rank = ''
43
+ no_of_consecutive_empty_cells = 0
44
+
45
+ 0.upto(7) do |x|
46
+ piece = pieces.find { |piece| piece.position.y == y && piece.position.x == x }
47
+
48
+ if piece.nil?
49
+ no_of_consecutive_empty_cells += 1
50
+ rank += no_of_consecutive_empty_cells.to_s if x == (BOARD_WIDTH - 1)
51
+ else
52
+ rank += get_piece_letter(piece, no_of_consecutive_empty_cells)
53
+ no_of_consecutive_empty_cells = 0
54
+ end
55
+ end
56
+
57
+ rank
58
+ end
59
+
60
+ def self.get_piece_letter(piece, no_of_consecutive_empty_cells)
61
+ result = ''
62
+ result += no_of_consecutive_empty_cells.to_s if no_of_consecutive_empty_cells != 0
63
+
64
+ key = piece.class.name.split('::').last.to_sym
65
+ letter = PIECE_CLASS_TO_LETTER[key]
66
+ letter = piece.color == :white ? letter.upcase : letter
67
+ result + letter
68
+ end
69
+
70
+ def initialize(fen_notation = DEFAULT_NOTATION)
71
+ segments = fen_notation.split(' ')
72
+ raise ChessError unless segments.size == 6
73
+
74
+ @pieces = segments[0]
75
+ @active = segments[1]
76
+ @castling_rights = segments[2]
77
+ @en_passant_pos = segments[3]
78
+ @halfmove = segments[4]
79
+ @fullmove = segments[5]
80
+ end
81
+
82
+ def parse
83
+ {
84
+ pieces: parse_pieces,
85
+ active_color: active == 'w' ? :white : :black,
86
+ castling_rights: parse_castling_rights,
87
+ en_passant_square: en_passant_pos == '-' ? nil : Position.parse(en_passant_pos),
88
+ halfmove_clock: halfmove.to_i,
89
+ fullmove_no: fullmove.to_i
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :pieces, :active, :castling_rights, :en_passant_pos, :halfmove, :fullmove
96
+
97
+ def parse_castling_rights
98
+ res = CastlingRights.new
99
+
100
+ res.kingside[:white] = true if castling_rights.include?('K')
101
+ res.kingside[:black] = true if castling_rights.include?('k')
102
+ res.queenside[:white] = true if castling_rights.include?('Q')
103
+ res.queenside[:black] = true if castling_rights.include?('q')
104
+
105
+ res
106
+ end
107
+
108
+ def parse_pieces
109
+ ranks = pieces.split('/')
110
+ raise ChessError, ERROR_MESSAGES[:invalid_board_height] if ranks.length != BOARD_HEIGHT
111
+
112
+ ranks.each_with_index.reduce([]) { |res, (rank, y)| res.concat parse_rank_fen(rank, y) }
113
+ end
114
+
115
+ def parse_rank_fen(rank_fen, y)
116
+ tokens = rank_fen.split('')
117
+ pos = Position.new(y: y, x: -1)
118
+
119
+ pieces = tokens.reduce([]) do |res, token|
120
+ piece, pos = parse_token(token, pos)
121
+ res.push(piece)
122
+ end
123
+
124
+ raise ChessError, ERROR_MESSAGES[:invalid_board_width] if pos.x + 1 != BOARD_WIDTH
125
+
126
+ pieces.reject(&:nil?)
127
+ end
128
+
129
+ def parse_token(token, pos)
130
+ raise ChessError, ERROR_MESSAGES[:invalid_token] unless FEN_TOKEN_REGEX.match(token)
131
+
132
+ if is_integer? token
133
+ pos = pos.increment(x: token.to_i)
134
+ piece = nil
135
+ else
136
+ color = token.upcase == token ? :white : :black
137
+ piece_class = LETTER_TO_PIECE_CLASS[token.downcase.to_sym]
138
+
139
+ pos = pos.increment(x: 1)
140
+ piece = piece_class.new(color, pos)
141
+ end
142
+
143
+ [piece, pos]
144
+ end
145
+
146
+ def is_integer?(string)
147
+ /^\d+$/.match string
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'require_all'
5
+
6
+ require_relative 'board'
7
+ require_relative 'errors'
8
+ require_rel 'game_modules'
9
+
10
+ # A class to handle a chess game
11
+ module RbChess
12
+ class Game
13
+ include MoveGenerator
14
+ include MoveParser
15
+ include MoveValidator
16
+
17
+ attr_reader :board
18
+
19
+ def initialize(fen: nil)
20
+ @board = Board.new(fen_notation: fen)
21
+ @starting_board = @board
22
+ @history = [].freeze
23
+ @repetitions_count = Hash.new(0) # for threefold repetition rule
24
+ end
25
+
26
+ def make_move(move)
27
+ raise ChessError, 'Game Over' if game_over?
28
+
29
+ move = parse_move move
30
+ raise ChessError, 'Cannot make move king in check' unless legal_move? move
31
+
32
+ @board = board.make_move move
33
+ @history = [*@history, move].freeze
34
+ update_repetitions_count
35
+ end
36
+
37
+ def winner
38
+ return unless checkmate?
39
+
40
+ current_player == :white ? :black : :white
41
+ end
42
+
43
+ def valid_move?(move)
44
+ move = parse_move move
45
+ legal_move? move
46
+ rescue ChessError
47
+ false
48
+ end
49
+
50
+ def all_moves
51
+ moves = board.player_pieces(current_player).reduce([]) do |res, piece|
52
+ res.concat moves_for_piece(piece, board)
53
+ end
54
+ moves.filter { |m| legal_move?(m) }.map(&:to_s)
55
+ end
56
+
57
+ def make_moves(moves)
58
+ moves.each { |m| make_move(m) }
59
+ end
60
+
61
+ def moves_at(pos)
62
+ moves_for_pos(pos, board).filter { |m| legal_move?(m) }.map(&:to_s)
63
+ end
64
+
65
+ def current_player
66
+ board.active_color
67
+ end
68
+
69
+ def stalemate?
70
+ no_moves?
71
+ end
72
+
73
+ def threefold?
74
+ repetitions_count.values.any? { |e| e >= 3 }
75
+ end
76
+
77
+ def fivefold?
78
+ repetitions_count.values.any? { |e| e >= 5 }
79
+ end
80
+
81
+ def seventy_five_moves?
82
+ board.halfmove_clock >= 150
83
+ end
84
+
85
+ def history
86
+ @history.map(&:to_s)
87
+ end
88
+
89
+ def fifty_moves?
90
+ board.halfmove_clock >= 100
91
+ end
92
+
93
+ def checkmate?
94
+ stalemate? && check?
95
+ end
96
+
97
+ def insufficient_material?
98
+ return true if board.pieces.size == 2 # only kings
99
+
100
+ return true if board.pieces.size == 3 &&
101
+ board.pieces.any? { |p| p.is_a?(Knight) || p.is_a?(Bishop) }
102
+
103
+ if board.pieces.size == 4
104
+ bishops = board.pieces.select { |p| p.is_a? Bishop }
105
+ return bishops.length == 2 &&
106
+ bishops[0].position.square_type == bishops[1].position.square_type
107
+ end
108
+
109
+ false
110
+ end
111
+
112
+ def draw?
113
+ !checkmate? && (
114
+ stalemate? || fivefold? || insufficient_material? ||
115
+ seventy_five_moves?
116
+ )
117
+ end
118
+
119
+ def game_over?
120
+ checkmate? || draw?
121
+ end
122
+
123
+ def serialize
124
+ Marshal.dump(self)
125
+ end
126
+
127
+ def self.deserialize(data)
128
+ Marshal.load(data)
129
+ end
130
+
131
+ def as_json
132
+ data = {
133
+ moves: history,
134
+ starting_fen: starting_board.to_fen,
135
+ fen: board.to_fen
136
+ }
137
+ JSON.generate(data)
138
+ end
139
+
140
+ def self.from_json(json_data, with_history: false)
141
+ data = JSON.parse(json_data)
142
+ return new(fen: data['fen']) unless with_history
143
+
144
+ game = new(fen: data['starting_fen'])
145
+ game.make_moves data['moves']
146
+ game
147
+ end
148
+
149
+ private
150
+
151
+ def update_repetitions_count
152
+ # removes halfmove and fullmove since they are needed
153
+ board_fen = board.to_fen.split(' ').first(4).join(' ')
154
+ repetitions_count[board_fen] = repetitions_count[board_fen] + 1
155
+ end
156
+
157
+ def no_moves?
158
+ moves = board.player_pieces(current_player).reduce([]) do |res, piece|
159
+ res.concat moves_for_piece(piece, board)
160
+ end
161
+ moves.none? { |m| legal_move?(m) }
162
+ end
163
+
164
+ attr_reader :repetitions_count, :starting_board
165
+ end
166
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../position'
4
+ require_relative '../pieces/piece_constants'
5
+ require_relative '../move'
6
+
7
+ # a module to generate moves for a piece
8
+ module RbChess
9
+ module MoveGenerator
10
+ include PieceConstants
11
+
12
+ SPECIAL_MOVES = {
13
+ castle: :castle_moves,
14
+ en_passant: :en_passant_moves
15
+ }.freeze
16
+
17
+ def moves_for_pos(pos, board)
18
+ piece = board.piece_at pos
19
+ return [] unless piece&.color == board.active_color
20
+
21
+ moves_for_piece(piece, board)
22
+ end
23
+
24
+ def moves_for_piece(piece, board)
25
+ piece.move_sets.reduce([]) do |result, move_set|
26
+ move_set.increments.each do |increment|
27
+ result.concat(
28
+ gen_moves_for_increment(board, move_set, increment, piece)
29
+ )
30
+ end
31
+
32
+ result.concat gen_special_moves(board, move_set, piece)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def gen_moves_for_increment(board, move_set, increment, piece)
39
+ x, y = increment.values_at(:x, :y)
40
+ result = []
41
+
42
+ 1.upto(move_set.repeat) do |factor|
43
+ new_pos = piece.position.increment(y: y * factor, x: x * factor)
44
+ break if new_pos.out_of_bounds? || blocked?(board, move_set, piece, new_pos)
45
+
46
+ removed = board.piece_at(new_pos).nil? ? nil : new_pos.to_s
47
+ from = piece.position.to_s
48
+ to = new_pos.to_s
49
+
50
+ if move_set.promotable && piece.can_promote?(new_pos)
51
+ result.concat promotion_moves(piece, from: from, to: to, removed: removed)
52
+ else
53
+ result << Move.new(from: from, to: to, removed: removed)
54
+ end
55
+
56
+ break unless removed.nil?
57
+ end
58
+
59
+ result
60
+ end
61
+
62
+ def promotion_moves(piece, from:, to:, removed:)
63
+ piece.promotion_pieces.map do |letter|
64
+ Move.new(from: from, to: to, removed: removed, promotion: letter)
65
+ end
66
+ end
67
+
68
+ def blocked?(board, move_set, piece, new_pos)
69
+ other_piece = board.piece_at new_pos
70
+ return true if other_piece.nil? && move_set.blocked_by.include?(:empty)
71
+
72
+ return true if other_piece && move_set.blocked_by.include?(:piece)
73
+
74
+ return true if other_piece&.color == piece.color && move_set.blocked_by.include?(:same)
75
+
76
+ return true if other_piece&.color != piece.color && move_set.blocked_by.include?(:enemy)
77
+
78
+ false
79
+ end
80
+
81
+ def gen_special_moves(board, move_set, piece)
82
+ result = []
83
+ move_set.special_moves.each do |sym|
84
+ result.concat send(SPECIAL_MOVES[sym], board, piece)
85
+ end
86
+
87
+ result.reject(&:nil?)
88
+ end
89
+
90
+ def castle_moves(board, piece)
91
+ [
92
+ kingside_castle_move(board, piece),
93
+ queenside_castle_move(board, piece)
94
+ ].reject(&:nil?)
95
+ end
96
+
97
+ def en_passant_moves(board, piece)
98
+ moves = [-1, 1].map do |x|
99
+ from = piece.position
100
+ to = piece.position.increment(y: piece.direction, x: x)
101
+ removed = piece.position.increment(x: x)
102
+ next nil unless board.en_passant_square == to
103
+
104
+ Move.new(from: from.to_s, to: to.to_s, removed: removed.to_s)
105
+ end
106
+
107
+ moves.reject(&:nil?)
108
+ end
109
+
110
+ def kingside_castle_move(board, piece)
111
+ return unless board.can_castle_kingside? piece.color
112
+
113
+ return unless kingside_castle_squares_available?(board, piece)
114
+
115
+ king_from = piece.position.to_s
116
+ king_to = piece.position.increment(x: 2).to_s
117
+ rook_from = piece.color == :white ? 'h1' : 'h8'
118
+ rook_to = piece.color == :white ? 'f1' : 'f8'
119
+
120
+ move = Move.new(from: king_from, to: king_to, castle: :kingside)
121
+ move.add_move(from: rook_from, to: rook_to)
122
+ move
123
+ end
124
+
125
+ def queenside_castle_move(board, piece)
126
+ return unless board.can_castle_queenside? piece.color
127
+
128
+ return unless queenside_castle_squares_available?(board, piece)
129
+
130
+ king_from = piece.position.to_s
131
+ king_to = piece.position.increment(x: -2).to_s
132
+ rook_from = piece.color == :white ? 'a1' : 'a8'
133
+ rook_to = piece.color == :white ? 'd1' : 'd8'
134
+
135
+ move = Move.new(from: king_from, to: king_to, castle: :queenside)
136
+ move.add_move(from: rook_from, to: rook_to)
137
+ move
138
+ end
139
+
140
+ def kingside_castle_squares_available?(board, piece)
141
+ positions = piece.color == :white ? %w[f1 g1] : %w[f8 g8]
142
+ positions.all? do |pos|
143
+ board.piece_at(pos).nil?
144
+ end
145
+ end
146
+
147
+ def queenside_castle_squares_available?(board, piece)
148
+ positions = piece.color == :white ? %w[b1 c1 d1] : %w[b8 c8 g8]
149
+ positions.all? do |pos|
150
+ board.piece_at(pos).nil?
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'move_generator'
4
+ require_relative '../errors'
5
+
6
+ module RbChess
7
+ module MoveParser
8
+ include MoveGenerator
9
+
10
+ MOVE_REGEX = /^(0-0)|(0-0-0)|(([a-h][1-8]){2}[qrbn]?)$/i.freeze
11
+
12
+ def parse_move(move)
13
+ raise ChessError, "Invalid move format cannot parse: #{move}" unless MOVE_REGEX.match move
14
+
15
+ castling_move?(move) ? castling_move(move) : normal_move(move)
16
+ end
17
+
18
+ def castling_move?(move)
19
+ /(0-0)|(0-0-0)/.match move
20
+ end
21
+
22
+ def castling_move(move)
23
+ pos = current_player == :white ? 'e1' : 'e8'
24
+ type = move == '0-0' ? :kingside : :queenside
25
+
26
+ moves = moves_for_pos(pos, board)
27
+ move = moves.find { |m| m.castle == type }
28
+
29
+ raise ChessError, "Invalid move, cannot castle #{type}" unless move
30
+
31
+ move
32
+ end
33
+
34
+ def normal_move(move_str)
35
+ _, pos, to, promotion = * /^(\w{2})(\w{2})(\w?)$/.match(move_str)
36
+ moves = moves_for_pos(pos, board)
37
+ move = moves.find { |m| m.to_s.downcase == move_str.downcase }
38
+ raise ChessError, "Invalid move: #{move}" unless move
39
+
40
+ move
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'move_generator'
4
+
5
+ module RbChess
6
+ module MoveValidator
7
+ include MoveGenerator
8
+
9
+ def legal_move?(move)
10
+ new_board = board.make_move move
11
+ is_check = check?(boardn: new_board, color: current_player)
12
+
13
+ return !is_check && legal_castle_move?(move) if move.castle
14
+
15
+ !is_check
16
+ end
17
+
18
+ def check?(boardn: board, color: current_player)
19
+ king = boardn.pieces.find { |piece| piece.is_a?(King) && piece.color == color }
20
+ king ? pos_attacked?(boardn, king.position, color) : false
21
+ end
22
+
23
+ def legal_castle_move?(move)
24
+ can_castle = move.castle == :kingside ? legal_kingside_castle_move? : legal_queenside_castle_move?
25
+ can_castle && !check?
26
+ end
27
+
28
+ def legal_kingside_castle_move?
29
+ positions = current_player == :white ? %w[f1 g1] : %w[f8 g8]
30
+ positions.none? { |pos| pos_attacked?(board, Position.parse(pos), current_player) }
31
+ end
32
+
33
+ def legal_queenside_castle_move?
34
+ positions = current_player == :white ? %w[b1 c1 d1] : %w[b8 c8 d8]
35
+ positions.none? { |pos| pos_attacked?(board, Position.parse(pos), current_player) }
36
+ end
37
+
38
+ def pos_attacked?(board, pos, color)
39
+ opponent_color = color == :white ? :black : :white
40
+ pieces = board.player_pieces(opponent_color)
41
+ moves = pieces.reduce([]) { |res, piece| res.concat moves_for_piece(piece, board) }
42
+ moves.any? do |move|
43
+ move.moved.any? { |hash| hash[:to] == pos }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'position'
4
+ require_relative 'pieces/piece_constants'
5
+
6
+ # A module to display chess board on console
7
+ module RbChess
8
+ module LetterDisplay
9
+ include PieceConstants
10
+
11
+ COLUMN_LABELS = ' a b c d e f g h'
12
+ SEPARATOR = ' _________________'
13
+
14
+ def ascii
15
+ ranks = (0..7).map { |rank_no| rank_ascii(rank_no) }
16
+ result = [COLUMN_LABELS, SEPARATOR] + ranks + [SEPARATOR, COLUMN_LABELS]
17
+ result.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ def rank_ascii(rank_no)
23
+ result = (0..7).map do |file_no|
24
+ piece_ascii(rank_no, file_no)
25
+ end
26
+
27
+ result.unshift("#{8 - rank_no} ")
28
+ result.push(" #{8 - rank_no}")
29
+ result.join(' ')
30
+ end
31
+
32
+ def piece_ascii(rank_no, file_no)
33
+ pos = Position.new(y: rank_no, x: file_no)
34
+ piece = piece_at(pos)
35
+ return '-' if piece.nil?
36
+
37
+ key = piece.class.name.split('::').last.to_sym
38
+ result = PIECE_CLASS_TO_LETTER[key]
39
+ piece.color == :black ? result.downcase : result.upcase
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A class to represent a chess move
4
+ module RbChess
5
+ class Move
6
+ attr_reader :moved, :removed, :promotion, :castle
7
+
8
+ def initialize(from:, to:, removed: nil, promotion: nil, castle: nil)
9
+ @promotion = promotion
10
+ @castle = castle
11
+ @removed = Position.parse(removed) if removed
12
+ @moved = [{ from: Position.parse(from), to: Position.parse(to) }].freeze
13
+ end
14
+
15
+ def add_move(from:, to:)
16
+ @moved = [*moved, { from: Position.parse(from), to: Position.parse(to) }].freeze
17
+ end
18
+
19
+ def destination_for(from)
20
+ hash = moved.find { |hash| hash[:from] == from }
21
+ hash[:to]
22
+ end
23
+
24
+ def to_s
25
+ return '0-0' if castle == :kingside
26
+
27
+ return '0-0-0' if castle == :queenside
28
+
29
+ moves = moved.map { |hash| "#{hash[:from]}#{hash[:to]}#{promotion.to_s.upcase}" }
30
+ moves.join(',')
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal: true|
4
+
5
+ # A class to represent info about how a piece moves on the body
6
+ module RbChess
7
+ class MoveSet
8
+ attr_accessor :increments, :repeat, :blocked_by, :promotable, :special_moves
9
+
10
+ def initialize(increments:, repeat:, blocked_by: [], special_moves: [], promotable: false)
11
+ @increments = increments
12
+ @repeat = repeat
13
+ @blocked_by = blocked_by
14
+ @special_moves = special_moves
15
+ @promotable = promotable
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../move_set'
4
+ require_relative 'piece'
5
+
6
+ # A class to represent a bishop in a chess game
7
+ module RbChess
8
+ class Bishop < Piece
9
+ attr_reader :move_sets
10
+
11
+ def initialize(color, position)
12
+ super
13
+ increments = [{ y: 1, x: 1 }, { y: -1, x: 1 }, { y: -1, x: -1 },
14
+ { y: 1, x: -1 }]
15
+ @move_sets = [
16
+ MoveSet.new(
17
+ increments: increments,
18
+ repeat: Float::INFINITY,
19
+ blocked_by: [:same]
20
+ )
21
+ ]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../move_set'
4
+ require_relative 'piece'
5
+
6
+ # A class to represent a kong in chess
7
+ module RbChess
8
+ class King < Piece
9
+ attr_reader :move_sets
10
+
11
+ def initialize(color, position)
12
+ super
13
+ increments = [{ y: -1, x: -1 }, { y: 1, x: 1 }, { y: 1, x: -1 },
14
+ { y: -1, x: 1 }, { y: 0, x: -1 }, { y: 0, x: 1 },
15
+ { y: -1, x: 0 }, { y: 1, x: 0 }]
16
+ @move_sets = [
17
+ MoveSet.new(
18
+ increments: increments,
19
+ repeat: 1,
20
+ blocked_by: [:same],
21
+ special_moves: %i[castle]
22
+ )
23
+ ]
24
+ end
25
+ end
26
+ end