rules.gno
15.18 Kb · 624 lines
1package chess
2
3import (
4 "errors"
5)
6
7// IsPathClear checks if all squares between from and to (exclusive) are empty
8func IsPathClear(board Board, fromRow, fromCol, toRow, toCol int) bool {
9 dRow := toRow - fromRow
10 dCol := toCol - fromCol
11
12 stepRow := 0
13 if dRow > 0 {
14 stepRow = 1
15 } else if dRow < 0 {
16 stepRow = -1
17 }
18
19 stepCol := 0
20 if dCol > 0 {
21 stepCol = 1
22 } else if dCol < 0 {
23 stepCol = -1
24 }
25
26 currRow := fromRow + stepRow
27 currCol := fromCol + stepCol
28
29 for currRow != toRow || currCol != toCol {
30 if board[currRow][currCol] != "." {
31 return false
32 }
33 currRow += stepRow
34 currCol += stepCol
35 }
36 return true
37}
38
39// IsOpponentOrEmpty checks if destination square is empty or has an opponent piece
40func IsOpponentOrEmpty(board Board, toRow, toCol int, isWhite bool) bool {
41 dest := board[toRow][toCol]
42 if dest == "." {
43 return true
44 }
45 destIsWhite := IsWhitePiece(dest)
46 return isWhite != destIsWhite
47}
48
49// IsKingInCheck checks if the king of the specified color is under attack
50func IsKingInCheck(board Board, isWhite bool) bool {
51 kingRow := -1
52 kingCol := -1
53 kingChar := "k"
54 if isWhite {
55 kingChar = "K"
56 }
57
58 for r := 0; r < 8; r++ {
59 for c := 0; c < 8; c++ {
60 if board[r][c] == kingChar {
61 kingRow = r
62 kingCol = c
63 break
64 }
65 }
66 if kingRow != -1 {
67 break
68 }
69 }
70
71 if kingRow == -1 {
72 return false
73 }
74
75 for r := 0; r < 8; r++ {
76 for c := 0; c < 8; c++ {
77 piece := board[r][c]
78 if piece == "." {
79 continue
80 }
81 pieceIsWhite := IsWhitePiece(piece)
82 if pieceIsWhite == isWhite {
83 continue
84 }
85
86 canAttack := false
87 switch piece {
88 case "P", "p":
89 dRow := kingRow - r
90 dCol := kingCol - c
91 if pieceIsWhite {
92 canAttack = (dRow == -1 && (dCol == 1 || dCol == -1))
93 } else {
94 canAttack = (dRow == 1 && (dCol == 1 || dCol == -1))
95 }
96 case "N", "n":
97 canAttack = IsValidKnightMove(r, c, kingRow, kingCol)
98 case "B", "b":
99 canAttack = IsValidBishopMove(board, r, c, kingRow, kingCol)
100 case "R", "r":
101 canAttack = IsValidRookMove(board, r, c, kingRow, kingCol)
102 case "Q", "q":
103 canAttack = IsValidQueenMove(board, r, c, kingRow, kingCol)
104 case "K", "k":
105 dRow := Abs(kingRow - r)
106 dCol := Abs(kingCol - c)
107 canAttack = (dRow <= 1 && dCol <= 1)
108 }
109
110 if canAttack {
111 return true
112 }
113 }
114 }
115 return false
116}
117
118// HasAnyLegalMoves checks if the current player has at least one legal move
119func HasAnyLegalMoves(board Board, isWhite bool, rights CastlingRights, enPassantCol int) bool {
120 for r := 0; r < 8; r++ {
121 for c := 0; c < 8; c++ {
122 piece := board[r][c]
123 if piece == "." {
124 continue
125 }
126 pieceIsWhite := IsWhitePiece(piece)
127 if pieceIsWhite != isWhite {
128 continue
129 }
130
131 for tr := 0; tr < 8; tr++ {
132 for tc := 0; tc < 8; tc++ {
133 if IsValidMove(board, r, c, tr, tc, isWhite, rights, enPassantCol) {
134 // Simulate move to check if king remains in check
135 origDest := board[tr][tc]
136 board[tr][tc] = piece
137 board[r][c] = "."
138
139 inCheck := IsKingInCheck(board, isWhite)
140
141 board[r][c] = piece
142 board[tr][tc] = origDest
143
144 if !inCheck {
145 return true
146 }
147 }
148 }
149 }
150 }
151 }
152 return false
153}
154
155// IsValidPawnMove validates pawn movements including double step and en passant
156func IsValidPawnMove(board Board, fromRow, fromCol, toRow, toCol int, isWhite bool, enPassantCol int) bool {
157 dRow := toRow - fromRow
158 dCol := toCol - fromCol
159
160 if isWhite {
161 if dRow == -1 && dCol == 0 {
162 return board[toRow][toCol] == "."
163 }
164 if fromRow == 6 && dRow == -2 && dCol == 0 {
165 return board[5][fromCol] == "." && board[4][fromCol] == "."
166 }
167 if dRow == -1 && (dCol == 1 || dCol == -1) {
168 dest := board[toRow][toCol]
169 if dest == "." {
170 // En Passant capture (must be from row 3 targeting enPassantCol)
171 return fromRow == 3 && toCol == enPassantCol
172 }
173 return !IsWhitePiece(dest)
174 }
175 } else {
176 if dRow == 1 && dCol == 0 {
177 return board[toRow][toCol] == "."
178 }
179 if fromRow == 1 && dRow == 2 && dCol == 0 {
180 return board[2][fromCol] == "." && board[3][fromCol] == "."
181 }
182 if dRow == 1 && (dCol == 1 || dCol == -1) {
183 dest := board[toRow][toCol]
184 if dest == "." {
185 // En Passant capture (must be from row 4 targeting enPassantCol)
186 return fromRow == 4 && toCol == enPassantCol
187 }
188 return IsWhitePiece(dest)
189 }
190 }
191 return false
192}
193
194// IsValidKnightMove validates knight L-shaped moves
195func IsValidKnightMove(fromRow, fromCol, toRow, toCol int) bool {
196 dRow := Abs(toRow - fromRow)
197 dCol := Abs(toCol - fromCol)
198 return (dRow == 1 && dCol == 2) || (dRow == 2 && dCol == 1)
199}
200
201// IsValidBishopMove validates bishop diagonal moves
202func IsValidBishopMove(board Board, fromRow, fromCol, toRow, toCol int) bool {
203 dRow := Abs(toRow - fromRow)
204 dCol := Abs(toCol - fromCol)
205 if dRow != dCol {
206 return false
207 }
208 return IsPathClear(board, fromRow, fromCol, toRow, toCol)
209}
210
211// IsValidRookMove validates rook straight line moves
212func IsValidRookMove(board Board, fromRow, fromCol, toRow, toCol int) bool {
213 if fromRow != toRow && fromCol != toCol {
214 return false
215 }
216 return IsPathClear(board, fromRow, fromCol, toRow, toCol)
217}
218
219// IsValidQueenMove validates queen diagonal or straight moves
220func IsValidQueenMove(board Board, fromRow, fromCol, toRow, toCol int) bool {
221 dRow := Abs(toRow - fromRow)
222 dCol := Abs(toCol - fromCol)
223
224 isDiagonal := (dRow == dCol)
225 isStraight := (fromRow == toRow || fromCol == toCol)
226
227 if !isDiagonal && !isStraight {
228 return false
229 }
230 return IsPathClear(board, fromRow, fromCol, toRow, toCol)
231}
232
233// IsValidKingMove validates king standard 1-square moves and castling (O-O and O-O-O)
234func IsValidKingMove(board Board, fromRow, fromCol, toRow, toCol int, isWhite bool, rights CastlingRights) bool {
235 dRow := Abs(toRow - fromRow)
236 dCol := Abs(toCol - fromCol)
237
238 if dRow <= 1 && dCol <= 1 {
239 return true
240 }
241
242 // Castling
243 if dRow == 0 && dCol == 2 {
244 if isWhite {
245 if rights.WhiteKingMoved {
246 return false
247 }
248 // Kingside Castling (Short O-O)
249 if toCol == 6 {
250 if rights.WhiteRightRookMoved || board[7][7] != "R" {
251 return false
252 }
253 if board[7][5] != "." || board[7][6] != "." {
254 return false
255 }
256 if IsKingInCheck(board, true) {
257 return false
258 }
259 // Verify intermediate square 7, 5 is not in check
260 board[7][4] = "."
261 board[7][5] = "K"
262 inCheckStep := IsKingInCheck(board, true)
263 board[7][5] = "."
264 board[7][4] = "K"
265 if inCheckStep {
266 return false
267 }
268 return true
269 }
270 // Queenside Castling (Long O-O-O)
271 if toCol == 2 {
272 if rights.WhiteLeftRookMoved || board[7][0] != "R" {
273 return false
274 }
275 if board[7][1] != "." || board[7][2] != "." || board[7][3] != "." {
276 return false
277 }
278 if IsKingInCheck(board, true) {
279 return false
280 }
281 // Verify intermediate square 7, 3 is not in check
282 board[7][4] = "."
283 board[7][3] = "K"
284 inCheckStep := IsKingInCheck(board, true)
285 board[7][3] = "."
286 board[7][4] = "K"
287 if inCheckStep {
288 return false
289 }
290 return true
291 }
292 } else {
293 if rights.BlackKingMoved {
294 return false
295 }
296 // Kingside Castling (Short o-o)
297 if toCol == 6 {
298 if rights.BlackRightRookMoved || board[0][7] != "r" {
299 return false
300 }
301 if board[0][5] != "." || board[0][6] != "." {
302 return false
303 }
304 if IsKingInCheck(board, false) {
305 return false
306 }
307 // Verify intermediate square 0, 5 is not in check
308 board[0][4] = "."
309 board[0][5] = "k"
310 inCheckStep := IsKingInCheck(board, false)
311 board[0][5] = "."
312 board[0][4] = "k"
313 if inCheckStep {
314 return false
315 }
316 return true
317 }
318 // Queenside Castling (Long o-o-o)
319 if toCol == 2 {
320 if rights.BlackLeftRookMoved || board[0][0] != "r" {
321 return false
322 }
323 if board[0][1] != "." || board[0][2] != "." || board[0][3] != "." {
324 return false
325 }
326 if IsKingInCheck(board, false) {
327 return false
328 }
329 // Verify intermediate square 0, 3 is not in check
330 board[0][4] = "."
331 board[0][3] = "k"
332 inCheckStep := IsKingInCheck(board, false)
333 board[0][3] = "."
334 board[0][4] = "k"
335 if inCheckStep {
336 return false
337 }
338 return true
339 }
340 }
341 }
342 return false
343}
344
345// IsValidMove checks if a move from (fromRow, fromCol) to (toRow, toCol) is valid
346func IsValidMove(board Board, fromRow, fromCol, toRow, toCol int, isWhiteTurn bool, rights CastlingRights, enPassantCol int) bool {
347 piece := board[fromRow][fromCol]
348 if piece == "." {
349 return false
350 }
351
352 isWhite := IsWhitePiece(piece)
353 if isWhite != isWhiteTurn {
354 return false
355 }
356
357 if !IsOpponentOrEmpty(board, toRow, toCol, isWhite) {
358 return false
359 }
360
361 switch piece {
362 case "P", "p":
363 return IsValidPawnMove(board, fromRow, fromCol, toRow, toCol, isWhite, enPassantCol)
364 case "N", "n":
365 return IsValidKnightMove(fromRow, fromCol, toRow, toCol)
366 case "B", "b":
367 return IsValidBishopMove(board, fromRow, fromCol, toRow, toCol)
368 case "R", "r":
369 return IsValidRookMove(board, fromRow, fromCol, toRow, toCol)
370 case "Q", "q":
371 return IsValidQueenMove(board, fromRow, fromCol, toRow, toCol)
372 case "K", "k":
373 return IsValidKingMove(board, fromRow, fromCol, toRow, toCol, isWhite, rights)
374 }
375
376 return false
377}
378
379// NormalizePromotionPiece validates and normalizes the promotion piece symbol
380func NormalizePromotionPiece(promo string, isWhite bool) (string, error) {
381 switch promo {
382 case "Q", "q", "":
383 if isWhite {
384 return "Q", nil
385 }
386 return "q", nil
387 case "R", "r":
388 if isWhite {
389 return "R", nil
390 }
391 return "r", nil
392 case "B", "b":
393 if isWhite {
394 return "B", nil
395 }
396 return "b", nil
397 case "N", "n":
398 if isWhite {
399 return "N", nil
400 }
401 return "n", nil
402 default:
403 return "", errors.New("pilihan promosi tidak valid: gunakan Q (Queen), R (Rook), B (Bishop), atau N (Knight)")
404 }
405}
406
407func splitWords(s string) []string {
408 var words []string
409 var cur []byte
410 for i := 0; i < len(s); i++ {
411 c := s[i]
412 if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' {
413 if len(cur) > 0 {
414 words = append(words, string(cur))
415 cur = nil
416 }
417 } else {
418 cur = append(cur, c)
419 }
420 }
421 if len(cur) > 0 {
422 words = append(words, string(cur))
423 }
424 return words
425}
426
427// ParseUCIMove parses a standard UCI move string (e.g. "e2e4", "e7e8q")
428func ParseUCIMove(uci string) (fromRow, fromCol, toRow, toCol int, promoPiece string, err error) {
429 if len(uci) < 4 || len(uci) > 5 {
430 return 0, 0, 0, 0, "", errors.New("format langkah UCI harus 4 atau 5 karakter (contoh: e2e4 atau e7e8q)")
431 }
432
433 fromFile := uci[0]
434 fromRank := uci[1]
435 toFile := uci[2]
436 toRank := uci[3]
437
438 if fromFile < 'a' || fromFile > 'h' || toFile < 'a' || toFile > 'h' ||
439 fromRank < '1' || fromRank > '8' || toRank < '1' || toRank > '8' {
440 return 0, 0, 0, 0, "", errors.New("koordinat UCI tidak valid: " + uci)
441 }
442
443 fromCol = int(fromFile - 'a')
444 fromRow = 8 - int(fromRank - '0')
445 toCol = int(toFile - 'a')
446 toRow = 8 - int(toRank - '0')
447
448 if len(uci) == 5 {
449 promoPiece = string(uci[4])
450 }
451
452 return fromRow, fromCol, toRow, toCol, promoPiece, nil
453}
454
455// ApplyMove applies a move to the board after validating it, updating castling rights and en passant.
456func ApplyMove(board Board, fromRow, fromCol, toRow, toCol int, promoPiece string, isWhiteTurn bool, rights *CastlingRights, enPassantCol *int) (Board, error) {
457 if fromRow < 0 || fromRow > 7 || fromCol < 0 || fromCol > 7 ||
458 toRow < 0 || toRow > 7 || toCol < 0 || toCol > 7 {
459 return board, errors.New("koordinat di luar papan")
460 }
461
462 piece := board[fromRow][fromCol]
463 if piece == "." {
464 return board, errors.New("tidak ada bidak di posisi asal")
465 }
466
467 isWhite := IsWhitePiece(piece)
468 if isWhite != isWhiteTurn {
469 return board, errors.New("bukan giliran warna bidak ini")
470 }
471
472 if !IsValidMove(board, fromRow, fromCol, toRow, toCol, isWhiteTurn, *rights, *enPassantCol) {
473 return board, errors.New("langkah catur tidak valid")
474 }
475
476 isPromotion := (piece == "P" && toRow == 0) || (piece == "p" && toRow == 7)
477 finalPiece := piece
478 if isPromotion {
479 promoted, err := NormalizePromotionPiece(promoPiece, isWhite)
480 if err != nil {
481 return board, err
482 }
483 finalPiece = promoted
484 }
485
486 isCastling := (piece == "K" || piece == "k") && Abs(toCol-fromCol) == 2
487 isEnPassant := (piece == "P" || piece == "p") && toCol == *enPassantCol && fromCol != toCol && board[toRow][toCol] == "."
488
489 // Verify move does not leave own king in check
490 origDest := board[toRow][toCol]
491 board[toRow][toCol] = piece
492 board[fromRow][fromCol] = "."
493
494 var epRow int
495 var epPiece string
496 if isEnPassant {
497 if isWhite {
498 epRow = 3
499 } else {
500 epRow = 4
501 }
502 epPiece = board[epRow][toCol]
503 board[epRow][toCol] = "."
504 }
505
506 inCheck := IsKingInCheck(board, isWhite)
507
508 board[fromRow][fromCol] = piece
509 board[toRow][toCol] = origDest
510 if isEnPassant {
511 board[epRow][toCol] = epPiece
512 }
513
514 if inCheck {
515 return board, errors.New("langkah ilegal: Raja terkena skak")
516 }
517
518 // Execute move
519 board[toRow][toCol] = finalPiece
520 board[fromRow][fromCol] = "."
521
522 if isEnPassant {
523 if isWhite {
524 board[3][toCol] = "."
525 } else {
526 board[4][toCol] = "."
527 }
528 }
529
530 if isCastling {
531 if isWhite {
532 if toCol == 6 {
533 board[7][5] = "R"
534 board[7][7] = "."
535 } else if toCol == 2 {
536 board[7][3] = "R"
537 board[7][0] = "."
538 }
539 } else {
540 if toCol == 6 {
541 board[0][5] = "r"
542 board[0][7] = "."
543 } else if toCol == 2 {
544 board[0][3] = "r"
545 board[0][0] = "."
546 }
547 }
548 }
549
550 // Update castling rights
551 if piece == "K" {
552 rights.WhiteKingMoved = true
553 } else if piece == "k" {
554 rights.BlackKingMoved = true
555 } else if piece == "R" {
556 if fromRow == 7 && fromCol == 0 {
557 rights.WhiteLeftRookMoved = true
558 } else if fromRow == 7 && fromCol == 7 {
559 rights.WhiteRightRookMoved = true
560 }
561 } else if piece == "r" {
562 if fromRow == 0 && fromCol == 0 {
563 rights.BlackLeftRookMoved = true
564 } else if fromRow == 0 && fromCol == 7 {
565 rights.BlackRightRookMoved = true
566 }
567 }
568
569 // Update en passant
570 if (piece == "P" || piece == "p") && Abs(toRow-fromRow) == 2 {
571 *enPassantCol = fromCol
572 } else {
573 *enPassantCol = -1
574 }
575
576 return board, nil
577}
578
579// ReplayUCI replays the entire sequence of chess moves from the beginning using UCI notation.
580// Returns status ("PLAYING", "WHITE_WON", "BLACK_WON", "DRAW").
581func ReplayUCI(movesUCI string) (Board, bool, string, error) {
582 board := NewInitialBoard()
583 var rights CastlingRights
584 enPassantCol := -1
585 isWhiteTurn := true
586
587 moves := splitWords(movesUCI)
588 if len(moves) == 0 {
589 return board, isWhiteTurn, "PLAYING", nil
590 }
591
592 for _, uci := range moves {
593 fromRow, fromCol, toRow, toCol, promoPiece, err := ParseUCIMove(uci)
594 if err != nil {
595 return board, isWhiteTurn, "PLAYING", errors.New("langkah " + uci + " tidak valid: " + err.Error())
596 }
597
598 newBoard, err := ApplyMove(board, fromRow, fromCol, toRow, toCol, promoPiece, isWhiteTurn, &rights, &enPassantCol)
599 if err != nil {
600 return board, isWhiteTurn, "PLAYING", errors.New("langkah " + uci + " ilegal: " + err.Error())
601 }
602 board = newBoard
603
604 // Flip turn
605 isWhiteTurn = !isWhiteTurn
606
607 // Check if the current player is checkmated or stalemated
608 if !HasAnyLegalMoves(board, isWhiteTurn, rights, enPassantCol) {
609 if IsKingInCheck(board, isWhiteTurn) {
610 if isWhiteTurn {
611 // White's turn with no legal moves and in check -> Black wins
612 return board, isWhiteTurn, "BLACK_WON", nil
613 }
614 // Black's turn with no legal moves and in check -> White wins
615 return board, isWhiteTurn, "WHITE_WON", nil
616 }
617 // Stalemate -> Draw
618 return board, isWhiteTurn, "DRAW", nil
619 }
620 }
621
622 return board, isWhiteTurn, "PLAYING", nil
623}
624