Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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