1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/// Position on the board e.g. a4
///
/// Both row and col should be in the range 0-13.
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
pub struct Position {
    pub row: usize,
    pub col: usize,
}

/// Simple enum for used to denote a turn / player.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum TurnColor {
    Red,
    Blue,
    Yellow,
    Green,
}

impl TurnColor {
    /// Rotates through colors in a clockwise direction
    ///
    /// ```
    /// # use fen4::TurnColor;
    /// assert_eq!(TurnColor::Blue, TurnColor::Red.next());
    /// assert_eq!(TurnColor::Red, TurnColor::Green.next());
    /// ```
    pub fn next(&self) -> Self {
        use TurnColor::*;
        match self {
            Red => Blue,
            Blue => Yellow,
            Yellow => Green,
            Green => Red,
        }
    }
}

/// Color modifier for pieces
///
/// Includes normal pieces, dead pieces, and dead pieces that also track which player they came from.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Color {
    Turn(TurnColor),
    Dead(Option<TurnColor>),
}

impl Color {
    pub fn is_dead(self) -> bool {
        match self {
            Self::Dead(_) => true,
            _ => false,
        }
    }
}

/// Simple representation of pieces that allows all types of fairy pieces
/// The trick is to just use the character that the notation uses to represent that piece.
///
/// Walls and empty cells are special, but everything else has a color as well.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum Piece {
    Empty,
    Wall,
    Normal(Color, char),
}

impl Default for Piece {
    fn default() -> Self {
        Piece::Empty
    }
}

impl Piece {
    pub fn is_piece(&self) -> bool {
        match self {
            Piece::Normal(_, _) => true,
            _ => false,
        }
    }
    pub fn is_empty(&self) -> bool {
        match self {
            Piece::Empty => true,
            _ => false,
        }
    }
}

/// The board representation of a 4 player chess game.
/// Board can be converted to and from a String in the fen4 format
///     
/// The fen4 file format is very similar to the [fen](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
/// format for normal chess, but it uses a larger board and uses prefixes for the
/// color of pieces instead of capitalization.
///
/// It also includes the metadata about castling rights at the beginning rather
/// than the end and has some differences in what metadata is stored.
///
/// The default position (with no whitespace) is:
/// ```text
///     R-0,0,0,0-1,1,1,1-1,1,1,1-0,0,0,0-0-
///     3,yR,yN,yB,yK,yQ,yB,yN,yR,3/
///     3,yP,yP,yP,yP,yP,yP,yP,yP,3/
///     14/
///     bR,bP,10,gP,gR/
///     bN,bP,10,gP,gN/
///     bB,bP,10,gP,gB/
///     bK,bP,10,gP,gQ/
///     bQ,bP,10,gP,gK/
///     bB,bP,10,gP,gB/
///     bN,bP,10,gP,gN/
///     bR,bP,10,gP,gR/
///     14/
///     3,rP,rP,rP,rP,rP,rP,rP,rP,3/
///     3,rR,rN,rB,rQ,rK,rB,rN,rR,3
/// ```
/// This format is shared for the Free-for-all and Teams mode so the dead and point
/// metadata is useless in Teams, but is there because the format is shared with the FFA mode.
///
/// The metadata format is parsed into the struct as shown here:
/// ```text
///     v turn    v king          v points  v extra
///     R-1,0,0,0-1,1,1,0-0,1,0,0-1,2,3,4-0-{'lifes':(2,2,2,2)}-
///       ^ dead          ^ queen         ^ ply since last pawn move or capture
/// ```
/// It starts with a Color, followed by 4 integer arrays of length 4, followed by
/// 0, followed by an optional extra data section. Each of these is separated by a
/// '-'. All of the arrays are information about the players with the leftmost data
/// about Red and proceding clockwise.
#[derive(PartialEq, Eq, Clone, Hash)]
pub struct Board {
    pub turn: TurnColor,
    pub dead: [bool; 4],
    pub castling_king: [bool; 4],
    pub castling_queen: [bool; 4],
    pub points: [u16; 4],
    pub draw_ply: usize,
    pub extra_options: Extra,
    pub board: [[Piece; 14]; 14],
}

const DEFAULT_FEN: &str = "R-0,0,0,0-1,1,1,1-1,1,1,1-0,0,0,0-0-
3,yR,yN,yB,yK,yQ,yB,yN,yR,3/
3,yP,yP,yP,yP,yP,yP,yP,yP,3/
14/
bR,bP,10,gP,gR/
bN,bP,10,gP,gN/
bB,bP,10,gP,gB/
bK,bP,10,gP,gQ/
bQ,bP,10,gP,gK/
bB,bP,10,gP,gB/
bN,bP,10,gP,gN/
bR,bP,10,gP,gR/
14/
3,rP,rP,rP,rP,rP,rP,rP,rP,3/
3,rR,rN,rB,rQ,rK,rB,rN,rR,3";

impl Default for Board {
    fn default() -> Self {
        DEFAULT_FEN.parse::<Self>().unwrap()
    }
}

impl Board {
    pub fn chess960(mut n: u16) -> Board {
        n -= 1; // chess.com used 1-960
                // Mapping derived from
                // https://en.wikipedia.org/wiki/Fischer_Random_Chess_numbering_scheme#Direct_derivation
        let b1 = 2 * (n % 4) + 1;
        let n2 = n / 4;
        let b2 = 2 * (n2 % 4);
        let n3 = n2 / 4;
        let q = n3 % 6;
        let n4 = n3 / 6;
        let knights = n4 % 10; // Instead of making N > 960 invalid, just work on N % 960
        let mut back_row = ['P'; 8];
        back_row[b1 as usize] = 'B';
        back_row[b2 as usize] = 'B';
        fn place(mut empties: u16, to_place: char, buffer: &mut [char]) {
            let mut i = 0;
            loop {
                if empties == 0 && buffer[i] == 'P' {
                    break;
                }
                if buffer[i] == 'P' {
                    empties -= 1;
                }
                i += 1;
            }
            buffer[i] = to_place;
        }
        place(q, 'Q', &mut back_row);
        let (knight1, knight2) = [
            (0, 0),
            (0, 1),
            (0, 2),
            (0, 3),
            (1, 1),
            (1, 2),
            (1, 3),
            (2, 2),
            (2, 3),
            (3, 3),
        ][knights as usize];
        place(knight1, 'N', &mut back_row);
        place(knight2, 'N', &mut back_row);
        place(0, 'R', &mut back_row);
        place(0, 'K', &mut back_row);
        place(0, 'R', &mut back_row);
        let mut output = Board::default();
        for i in 0..8 {
            output.board[0][i + 3] = Piece::Normal(Color::Turn(TurnColor::Red), back_row[i]);
            output.board[i + 3][0] = Piece::Normal(Color::Turn(TurnColor::Blue), back_row[i]);
            output.board[13][10 - i] = Piece::Normal(Color::Turn(TurnColor::Yellow), back_row[i]);
            output.board[10 - i][13] = Piece::Normal(Color::Turn(TurnColor::Green), back_row[i]);
        }
        output
    }
}
/// Additional options in the FEN4 format stored as a list of key value pairs.
///
/// In addition to the always present options in a FEN4, there are also options
/// that are only present if rule variants are enabled. Notable examples would
/// be "en passant" (because its not considered standard) and "N-Check"
///
/// The format uses a series of labeled elements separated by commas. The highlevel
/// structure looks like `{'label':value,'label2':value2}`.
///
/// Values seem to be able to take several types. But the ones that are confirmed are:
///   - Strings `'string_value'`
///   - Numbers `65900`
///   - Booleans `true` and `false`
///   - Arrays `(valueRed, valueBlue, valueYellow, valueGreen)`
///
/// Examples of known labels used are:
///   - `'enPassant':('j3:j4','','','')`
///     - The first position is where a pawn can capture and the second is where the passing pawn should be removed in the event of a capture.
///     - This is neccessary notation because some types of fairy pawns move diagonally. Without the extra information it could be ambiguous.
///   - `'lives':(9,6,4,0)`
///     - Indicates number of lives left for the N-check variant
///   - `'pawnsBaseRank':8`
///     - The rank on which pawns can jump forward 2 square (default:2)
///     - a value of `0` indicates pawns never move more than 1
///   - `'royal':('a4','b5','c6','d7')`
///     - Used to indicate which piece should be considered the king for purposes of checks
///     - Neccessary in cases of multiple kings (of the same color) or for making a different piece act as the leader
///     - This seems to have previously been called 'kingSquares'
///   - `'uniquify':94403`
///     - It is not clear what this actually does
///   - `'resigned':(true,true,false,false)` and `'flagged':(false,true,false,false)`
///     - Both seem to be neccesssary for the "DeadKingWalking" feature.
///   - `'stalemated':(true,true,false,false)`
///     - It is not clear this has any effect, but it might affect the "DeadKingWalking" feature.
///   - `'zombieImmune':(true,false,false,true)`
///     - Makes zombie pieces impossible to capture
///   - `'zombieType':('','','','muncher')`
///     - Used to change the behaviour of zombies
///     - Possible types include muncher, comfuter, checker, ranter, and possibly more
///
/// The labels have a preferred order. The preferred order is the order of the fields of the struct.
///
/// gameOver is an additional option, but only seems to appear in internal messages.
/// It would fit somwhere between flagged and enPassant in terms of preferred order and stores
/// the message that shows up at the end of the game.
///
/// This will also use gameOver to represent final messages, but will not specifically try to be
/// compatable with chess.com's internal messages.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct Extra {
    pub royal: [Option<Position>; 4],
    pub lives: Option<[usize; 4]>,
    pub resigned: [bool; 4],
    pub flagged: [bool; 4],
    pub stalemated: [bool; 4],
    pub game_over: String,
    pub zombie_immune: [bool; 4],
    pub zombie_type: [String; 4],
    pub enpassant: [Option<(Position, Position)>; 4],
    pub pawnbaserank: usize,
    pub uniquify: usize,
    pub std2pc: bool,
}

impl Default for Extra {
    fn default() -> Self {
        Self {
            royal: Default::default(),
            lives: None,
            resigned: Default::default(),
            flagged: Default::default(),
            stalemated: Default::default(),
            game_over: "".into(),
            zombie_immune: Default::default(),
            zombie_type: Default::default(),
            enpassant: Default::default(),
            pawnbaserank: 2,
            uniquify: 0,
            std2pc: false,
        }
    }
}