feen 5.0.0.beta2 → 5.0.0.beta3
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.
- checksums.yaml +4 -4
- data/README.md +107 -86
- data/lib/feen/dumper/games_turn.rb +40 -65
- data/lib/feen/dumper/piece_placement.rb +102 -89
- data/lib/feen/dumper/pieces_in_hand/errors.rb +12 -0
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/dumper/pieces_in_hand.rb +57 -41
- data/lib/feen/dumper.rb +63 -32
- data/lib/feen/parser/games_turn/errors.rb +14 -0
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +24 -0
- data/lib/feen/parser/games_turn.rb +32 -110
- data/lib/feen/parser/piece_placement.rb +490 -77
- data/lib/feen/parser/pieces_in_hand/errors.rb +14 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +15 -0
- data/lib/feen/parser/pieces_in_hand.rb +53 -44
- data/lib/feen/parser.rb +67 -30
- data/lib/feen.rb +42 -76
- metadata +9 -6
- data/lib/feen/converter/from_fen.rb +0 -170
- data/lib/feen/converter/to_fen.rb +0 -153
- data/lib/feen/converter.rb +0 -16
- data/lib/feen/sanitizer.rb +0 -119
@@ -1,170 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Converter
|
5
|
-
module FromFen
|
6
|
-
# Constants
|
7
|
-
WHITE_TURN = "CHESS/chess"
|
8
|
-
BLACK_TURN = "chess/CHESS"
|
9
|
-
NO_PIECES_IN_HAND = "-"
|
10
|
-
EN_PASSANT_NONE = "-"
|
11
|
-
|
12
|
-
# Castling-related constants
|
13
|
-
CASTLING_SUFFIX_BOTH = "="
|
14
|
-
CASTLING_SUFFIX_KINGSIDE = ">"
|
15
|
-
CASTLING_SUFFIX_QUEENSIDE = "<"
|
16
|
-
CASTLING_SUFFIX_NONE = ""
|
17
|
-
|
18
|
-
# Piece identifiers
|
19
|
-
WHITE_KING = "K"
|
20
|
-
BLACK_KING = "k"
|
21
|
-
WHITE_PAWN = "P"
|
22
|
-
BLACK_PAWN = "p"
|
23
|
-
|
24
|
-
# Board indices
|
25
|
-
WHITE_KING_STARTING_ROW = 7
|
26
|
-
BLACK_KING_STARTING_ROW = 0
|
27
|
-
|
28
|
-
# Converts a FEN string to a FEEN string for chess positions
|
29
|
-
# @param fen_string [String] Standard FEN notation string for chess
|
30
|
-
# @return [String] Equivalent FEEN notation string
|
31
|
-
# @raise [ArgumentError] If the FEN string is invalid
|
32
|
-
def self.call(fen_string)
|
33
|
-
unless fen_string.is_a?(String) && !fen_string.strip.empty?
|
34
|
-
raise ArgumentError, "FEN must be a non-empty string"
|
35
|
-
end
|
36
|
-
|
37
|
-
parts = fen_string.strip.split
|
38
|
-
raise ArgumentError, "Invalid FEN format: expected at least 4 fields" unless parts.size >= 4
|
39
|
-
|
40
|
-
placement_fen, active_color, castling, en_passant = parts[0..3]
|
41
|
-
board = parse_board(placement_fen)
|
42
|
-
|
43
|
-
apply_castling!(board, castling)
|
44
|
-
apply_en_passant!(board, en_passant)
|
45
|
-
|
46
|
-
piece_placement = format_board(board)
|
47
|
-
games_turn = active_color == "w" ? WHITE_TURN : BLACK_TURN
|
48
|
-
|
49
|
-
"#{piece_placement} #{games_turn} #{NO_PIECES_IN_HAND}"
|
50
|
-
end
|
51
|
-
|
52
|
-
# Parses the FEN piece placement into a 2D board array
|
53
|
-
# @param fen_placement [String] FEN piece placement string
|
54
|
-
# @return [Array<Array<String, nil>>] 2D array representing the board
|
55
|
-
def self.parse_board(fen_placement)
|
56
|
-
fen_placement.split("/").map do |row|
|
57
|
-
cells = []
|
58
|
-
row.each_char do |char|
|
59
|
-
if /[1-8]/.match?(char)
|
60
|
-
cells.concat([nil] * char.to_i)
|
61
|
-
else
|
62
|
-
cells << char
|
63
|
-
end
|
64
|
-
end
|
65
|
-
cells
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Applies castling rights to kings on the board
|
70
|
-
# @param board [Array<Array<String, nil>>] 2D board array
|
71
|
-
# @param castling [String] FEN castling rights string
|
72
|
-
# @return [void]
|
73
|
-
def self.apply_castling!(board, castling)
|
74
|
-
return if castling == EN_PASSANT_NONE
|
75
|
-
|
76
|
-
# Determine suffix for each king
|
77
|
-
wk_suffix = determine_castling_suffix(castling.include?("K"), castling.include?("Q"))
|
78
|
-
bk_suffix = determine_castling_suffix(castling.include?("k"), castling.include?("q"))
|
79
|
-
|
80
|
-
# Apply the suffixes to the kings
|
81
|
-
apply_king_suffix!(board, WHITE_KING, wk_suffix)
|
82
|
-
apply_king_suffix!(board, BLACK_KING, bk_suffix)
|
83
|
-
end
|
84
|
-
|
85
|
-
# Applies a castling suffix to a king on the board
|
86
|
-
# @param board [Array<Array<String, nil>>] 2D board array
|
87
|
-
# @param king_char [String] King character ('K' or 'k')
|
88
|
-
# @param suffix [String] Castling suffix to apply
|
89
|
-
# @return [void]
|
90
|
-
def self.apply_king_suffix!(board, king_char, suffix)
|
91
|
-
return if suffix.empty?
|
92
|
-
|
93
|
-
board.each_with_index do |row, r|
|
94
|
-
row.each_with_index do |cell, c|
|
95
|
-
board[r][c] = "#{king_char}#{suffix}" if cell == king_char
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# Determines the appropriate castling suffix based on kingside and queenside rights
|
101
|
-
# @param kingside [Boolean] Whether kingside castling is allowed
|
102
|
-
# @param queenside [Boolean] Whether queenside castling is allowed
|
103
|
-
# @return [String] The castling suffix
|
104
|
-
def self.determine_castling_suffix(kingside, queenside)
|
105
|
-
if kingside && queenside
|
106
|
-
CASTLING_SUFFIX_BOTH
|
107
|
-
elsif kingside
|
108
|
-
CASTLING_SUFFIX_KINGSIDE
|
109
|
-
elsif queenside
|
110
|
-
CASTLING_SUFFIX_QUEENSIDE
|
111
|
-
else
|
112
|
-
CASTLING_SUFFIX_NONE
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# Applies en passant rights to pawns on the board
|
117
|
-
# @param board [Array<Array<String, nil>>] 2D board array
|
118
|
-
# @param en_passant [String] FEN en passant target square
|
119
|
-
# @return [void]
|
120
|
-
def self.apply_en_passant!(board, en_passant)
|
121
|
-
return if en_passant == EN_PASSANT_NONE
|
122
|
-
|
123
|
-
col = en_passant[0].ord - "a".ord
|
124
|
-
row = 8 - en_passant[1].to_i
|
125
|
-
|
126
|
-
if row == 2 # White just moved: check black pawns on row 3
|
127
|
-
apply_en_passant_for_pawn!(board, 3, col, BLACK_PAWN)
|
128
|
-
elsif row == 5 # Black just moved: check white pawns on row 4
|
129
|
-
apply_en_passant_for_pawn!(board, 4, col, WHITE_PAWN)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# Applies en passant rights to pawns at a specific row and column
|
134
|
-
# @param board [Array<Array<String, nil>>] 2D board array
|
135
|
-
# @param pawn_row [Integer] Row where pawns can capture en passant
|
136
|
-
# @param target_col [Integer] Column of the pawn that moved two squares
|
137
|
-
# @param pawn_char [String] Pawn character ('P' or 'p')
|
138
|
-
# @return [void]
|
139
|
-
def self.apply_en_passant_for_pawn!(board, pawn_row, target_col, pawn_char)
|
140
|
-
[-1, 1].each do |dx|
|
141
|
-
x = target_col + dx
|
142
|
-
|
143
|
-
next unless x.between?(0, 7) && board[pawn_row][x] == pawn_char
|
144
|
-
|
145
|
-
# Determine the en passant suffix based on relative position
|
146
|
-
suffix = dx == -1 ? ">" : "<"
|
147
|
-
board[pawn_row][x] = "#{pawn_char}#{suffix}"
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
# Formats the board array back into FEN piece placement
|
152
|
-
# @param board [Array<Array<String, nil>>] 2D board array
|
153
|
-
# @return [String] FEN piece placement string
|
154
|
-
def self.format_board(board)
|
155
|
-
board.map do |row|
|
156
|
-
format_row(row)
|
157
|
-
end.join("/")
|
158
|
-
end
|
159
|
-
|
160
|
-
# Formats a row of the board into FEN notation
|
161
|
-
# @param row [Array<String, nil>] Row of the board
|
162
|
-
# @return [String] FEN notation for this row
|
163
|
-
def self.format_row(row)
|
164
|
-
row.chunk_while { |a, b| a.nil? == b.nil? }.map do |chunk|
|
165
|
-
chunk.first.nil? ? chunk.size.to_s : chunk.join
|
166
|
-
end.join
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
@@ -1,153 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Converter
|
5
|
-
module ToFen
|
6
|
-
# Constants for game types and formats
|
7
|
-
WHITE_ACTIVE = "CHESS/chess"
|
8
|
-
BLACK_ACTIVE = "chess/CHESS"
|
9
|
-
|
10
|
-
# Constants for FEN components
|
11
|
-
NO_EN_PASSANT = "-"
|
12
|
-
NO_CASTLING = "-"
|
13
|
-
HALF_MOVE_DEFAULT = "0"
|
14
|
-
FULL_MOVE_DEFAULT = "1"
|
15
|
-
|
16
|
-
# Castling-related constants
|
17
|
-
WHITE_KINGSIDE = "K"
|
18
|
-
WHITE_QUEENSIDE = "Q"
|
19
|
-
BLACK_KINGSIDE = "k"
|
20
|
-
BLACK_QUEENSIDE = "q"
|
21
|
-
|
22
|
-
# En passant constants
|
23
|
-
EN_PASSANT_WHITE_RANK = "3"
|
24
|
-
EN_PASSANT_BLACK_RANK = "6"
|
25
|
-
|
26
|
-
# Converts a FEEN string to a standard FEN string for chess positions
|
27
|
-
# @param feen_string [String] FEEN notation string
|
28
|
-
# @return [String] Standard FEN notation string
|
29
|
-
# @raise [ArgumentError] If the FEEN string is invalid or not supported
|
30
|
-
def self.call(feen_string)
|
31
|
-
# Validate input
|
32
|
-
unless feen_string.is_a?(String) && !feen_string.strip.empty?
|
33
|
-
raise ArgumentError, "FEEN must be a non-empty string"
|
34
|
-
end
|
35
|
-
|
36
|
-
# Split into components
|
37
|
-
parts = feen_string.strip.split
|
38
|
-
raise ArgumentError, "Invalid FEEN format: expected 3 fields" unless parts.size == 3
|
39
|
-
|
40
|
-
piece_placement, games_turn, = parts
|
41
|
-
|
42
|
-
# Verify game type is supported
|
43
|
-
unless [WHITE_ACTIVE, BLACK_ACTIVE].include?(games_turn)
|
44
|
-
raise ArgumentError, "Only CHESS/chess FEEN formats are supported"
|
45
|
-
end
|
46
|
-
|
47
|
-
# Extract FEN components
|
48
|
-
castling_rights = extract_castling_rights(piece_placement)
|
49
|
-
en_passant = extract_en_passant(piece_placement)
|
50
|
-
fen_board = convert_board_to_fen(piece_placement)
|
51
|
-
active_color = games_turn.start_with?("CHESS") ? "w" : "b"
|
52
|
-
|
53
|
-
# Construct FEN string
|
54
|
-
"#{fen_board} #{active_color} #{castling_rights} #{en_passant} #{HALF_MOVE_DEFAULT} #{FULL_MOVE_DEFAULT}"
|
55
|
-
end
|
56
|
-
|
57
|
-
# Converts the FEEN board representation to FEN format by removing special markings
|
58
|
-
# @param piece_placement [String] FEEN piece placement string
|
59
|
-
# @return [String] FEN piece placement string
|
60
|
-
def self.convert_board_to_fen(piece_placement)
|
61
|
-
piece_placement.split("/").map do |row|
|
62
|
-
# Remove castling suffixes from kings
|
63
|
-
row_without_king_suffix = row.gsub(/([Kk])([=<>])/) { Regexp.last_match(1) }
|
64
|
-
# Remove en passant suffixes from pawns
|
65
|
-
row_without_suffixes = row_without_king_suffix.gsub(/([Pp])([<>])/) { Regexp.last_match(1) }
|
66
|
-
# Ensure only single character pieces (in case of promoted pieces with prefixes)
|
67
|
-
row_without_suffixes.gsub(/\d+|\w/) { |s| /\d/.match?(s) ? s : s[0] }
|
68
|
-
end.join("/")
|
69
|
-
end
|
70
|
-
|
71
|
-
# Extracts castling rights from FEEN piece placement
|
72
|
-
# @param piece_placement [String] FEEN piece placement string
|
73
|
-
# @return [String] FEN castling rights component
|
74
|
-
def self.extract_castling_rights(piece_placement)
|
75
|
-
castling = ""
|
76
|
-
|
77
|
-
# White castling rights - always in uppercase and first
|
78
|
-
piece_placement.split("/").each do |row|
|
79
|
-
castling += WHITE_KINGSIDE if row.include?("K>") || row.include?("K=")
|
80
|
-
castling += WHITE_QUEENSIDE if row.include?("K<") || row.include?("K=")
|
81
|
-
end
|
82
|
-
|
83
|
-
# Black castling rights - always in lowercase and after white
|
84
|
-
piece_placement.split("/").each do |row|
|
85
|
-
castling += BLACK_KINGSIDE if row.include?("k>") || row.include?("k=")
|
86
|
-
castling += BLACK_QUEENSIDE if row.include?("k<") || row.include?("k=")
|
87
|
-
end
|
88
|
-
|
89
|
-
castling.empty? ? NO_CASTLING : castling
|
90
|
-
end
|
91
|
-
|
92
|
-
# Extracts en passant target square from FEEN piece placement
|
93
|
-
# @param piece_placement [String] FEEN piece placement string
|
94
|
-
# @return [String] FEN en passant target square or "-" if none
|
95
|
-
# @raise [ArgumentError] If multiple en passant markers are detected
|
96
|
-
def self.extract_en_passant(piece_placement)
|
97
|
-
rows = piece_placement.split("/")
|
98
|
-
en_passant_targets = []
|
99
|
-
|
100
|
-
rows.each_with_index do |row, _rank|
|
101
|
-
file_index = 0
|
102
|
-
char_index = 0
|
103
|
-
|
104
|
-
while char_index < row.length
|
105
|
-
current_char = row[char_index]
|
106
|
-
|
107
|
-
if /[1-8]/.match?(current_char)
|
108
|
-
# Skip empty squares
|
109
|
-
file_index += current_char.to_i
|
110
|
-
char_index += 1
|
111
|
-
elsif char_index + 1 < row.length && row[char_index..(char_index + 1)] =~ /([Pp])([<>])/
|
112
|
-
# Found a pawn with en passant marker
|
113
|
-
pawn_type = ::Regexp.last_match(1) # 'P' or 'p'
|
114
|
-
::Regexp.last_match(2) # '<' or '>'
|
115
|
-
|
116
|
-
# Calculate the file (column) letter
|
117
|
-
file_char = ("a".ord + file_index).chr
|
118
|
-
|
119
|
-
# Calculate the rank (row) number based on pawn type
|
120
|
-
# For white pawn (P) on rank 4 (index 3), the target is rank 3
|
121
|
-
# For black pawn (p) on rank 5 (index 2), the target is rank 6
|
122
|
-
rank_num = if pawn_type == "P"
|
123
|
-
"3" # White pawn's en passant target is always on rank 3
|
124
|
-
else
|
125
|
-
"6" # Black pawn's en passant target is always on rank 6
|
126
|
-
end
|
127
|
-
|
128
|
-
en_passant_targets << "#{file_char}#{rank_num}"
|
129
|
-
|
130
|
-
# Move past this pawn and its suffix
|
131
|
-
file_index += 1
|
132
|
-
char_index += 2
|
133
|
-
else
|
134
|
-
# Regular piece
|
135
|
-
file_index += 1
|
136
|
-
char_index += 1
|
137
|
-
|
138
|
-
# Skip any suffix attached to this piece
|
139
|
-
char_index += 1 if char_index < row.length && row[char_index] =~ /[<>=]/
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# Only one en passant target should be present in a valid position
|
145
|
-
if en_passant_targets.size > 1
|
146
|
-
raise ArgumentError, "Multiple en passant markers detected: #{en_passant_targets.join(', ')}"
|
147
|
-
end
|
148
|
-
|
149
|
-
en_passant_targets.first || "-"
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
data/lib/feen/converter.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative File.join("converter", "from_fen")
|
4
|
-
require_relative File.join("converter", "to_fen")
|
5
|
-
|
6
|
-
module Feen
|
7
|
-
module Converter
|
8
|
-
def self.from_fen(fen_string)
|
9
|
-
FromFen.call(fen_string)
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.to_fen(feen_string)
|
13
|
-
ToFen.call(feen_string)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
data/lib/feen/sanitizer.rb
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
# Provides methods for sanitizing and validating chess-related notation strings
|
5
|
-
module Sanitizer
|
6
|
-
# Cleans a FEN (Forsyth-Edwards Notation) string by removing invalid castling rights
|
7
|
-
# and en passant targets based on the current position.
|
8
|
-
#
|
9
|
-
# The method performs the following validations:
|
10
|
-
# - Verifies that kings and rooks are in correct positions for castling rights
|
11
|
-
# - Verifies that en passant captures are actually possible
|
12
|
-
#
|
13
|
-
# @param fen_string [String] The FEN string to clean
|
14
|
-
# @return [String] A sanitized FEN string with invalid castling rights and en passant targets removed
|
15
|
-
# @raise [ArgumentError] If the FEN string is malformed (less than 4 parts)
|
16
|
-
#
|
17
|
-
# @example Clean a valid FEN string (unchanged)
|
18
|
-
# fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
19
|
-
# Feen::Sanitizer.clean_fen(fen) # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
20
|
-
#
|
21
|
-
# @example Remove invalid castling rights when king has moved
|
22
|
-
# fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w KQkq - 0 1"
|
23
|
-
# Feen::Sanitizer.clean_fen(fen) # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQ1BNR w kq - 0 1"
|
24
|
-
#
|
25
|
-
# @example Remove invalid castling rights when rook has moved
|
26
|
-
# fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBNR w KQkq - 0 1"
|
27
|
-
# Feen::Sanitizer.clean_fen(fen) # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBNR w Kkq - 0 1"
|
28
|
-
#
|
29
|
-
# @example Remove invalid en passant target when no capturing pawn exists
|
30
|
-
# fen = "rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w KQkq e6 0 2"
|
31
|
-
# Feen::Sanitizer.clean_fen(fen) # => "rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 2"
|
32
|
-
#
|
33
|
-
# @example Keep valid en passant target when capturing is possible
|
34
|
-
# fen = "rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 2"
|
35
|
-
# Feen::Sanitizer.clean_fen(fen) # => "rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 2"
|
36
|
-
def self.clean_fen(fen_string)
|
37
|
-
parts = fen_string.strip.split
|
38
|
-
return fen_string unless parts.size >= 4
|
39
|
-
|
40
|
-
board, active_color, castling, en_passant, *rest = parts
|
41
|
-
|
42
|
-
# Parse board into a 2D array for easier access
|
43
|
-
board_matrix = []
|
44
|
-
board.split("/").each do |row|
|
45
|
-
current_row = []
|
46
|
-
row.each_char do |c|
|
47
|
-
if /[1-8]/.match?(c)
|
48
|
-
c.to_i.times { current_row << nil }
|
49
|
-
else
|
50
|
-
current_row << c
|
51
|
-
end
|
52
|
-
end
|
53
|
-
board_matrix << current_row
|
54
|
-
end
|
55
|
-
|
56
|
-
# Clean castling rights
|
57
|
-
new_castling = castling.dup
|
58
|
-
new_castling = clean_castling_rights(new_castling, board_matrix)
|
59
|
-
|
60
|
-
# Clean en passant target
|
61
|
-
new_en_passant = clean_en_passant_target(en_passant, board_matrix, active_color)
|
62
|
-
|
63
|
-
([board, active_color, new_castling, new_en_passant] + rest).join(" ")
|
64
|
-
end
|
65
|
-
|
66
|
-
# Validates and cleans castling rights based on the position of kings and rooks
|
67
|
-
#
|
68
|
-
# @param castling [String] The castling rights string from FEN
|
69
|
-
# @param board [Array<Array<String, nil>>] 2D array representing the board
|
70
|
-
# @return [String] Cleaned castling rights or "-" if none are valid
|
71
|
-
# @api private
|
72
|
-
private_class_method def self.clean_castling_rights(castling, board)
|
73
|
-
return "-" if castling == "-"
|
74
|
-
|
75
|
-
new_castling = castling.dup
|
76
|
-
|
77
|
-
# White castling rights
|
78
|
-
new_castling.gsub!(/[KQ]/, "") unless board[7][4] == "K"
|
79
|
-
new_castling.delete!("K") unless board[7][7] == "R"
|
80
|
-
new_castling.delete!("Q") unless board[7][0] == "R"
|
81
|
-
|
82
|
-
# Black castling rights
|
83
|
-
new_castling.gsub!(/[kq]/, "") unless board[0][4] == "k"
|
84
|
-
new_castling.delete!("k") unless board[0][7] == "r"
|
85
|
-
new_castling.delete!("q") unless board[0][0] == "r"
|
86
|
-
|
87
|
-
new_castling.empty? ? "-" : new_castling
|
88
|
-
end
|
89
|
-
|
90
|
-
# Validates and cleans en passant target based on the position of pawns
|
91
|
-
#
|
92
|
-
# @param en_passant [String] The en passant target square from FEN
|
93
|
-
# @param board [Array<Array<String, nil>>] 2D array representing the board
|
94
|
-
# @param active_color [String] The active color ("w" or "b")
|
95
|
-
# @return [String] Cleaned en passant target or "-" if invalid
|
96
|
-
# @api private
|
97
|
-
private_class_method def self.clean_en_passant_target(en_passant, board, active_color)
|
98
|
-
return "-" if en_passant == "-"
|
99
|
-
|
100
|
-
file = en_passant[0].ord - "a".ord
|
101
|
-
rank = en_passant[1].to_i
|
102
|
-
|
103
|
-
# Validate en passant square coordinates
|
104
|
-
return "-" unless file.between?(0, 7) && [3, 6].include?(rank)
|
105
|
-
|
106
|
-
# For white's move (after black pawn double advance)
|
107
|
-
if active_color == "w" && rank == 6
|
108
|
-
# Check for white pawns on the 5th rank (index 3) that can capture
|
109
|
-
return en_passant if [file - 1, file + 1].any? { |f| f.between?(0, 7) && board[3][f] == "P" }
|
110
|
-
# For black's move (after white pawn double advance)
|
111
|
-
elsif active_color == "b" && rank == 3
|
112
|
-
# Check for black pawns on the 4th rank (index 4) that can capture
|
113
|
-
return en_passant if [file - 1, file + 1].any? { |f| f.between?(0, 7) && board[4][f] == "p" }
|
114
|
-
end
|
115
|
-
|
116
|
-
"-" # Invalid en passant square
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|