Move the mailbox into the position object and get rid of update(). Minor UI tweaks
This commit is contained in:
parent
da82878ebb
commit
d4fe999567
|
@ -4,3 +4,4 @@
|
|||
--passL:"-flto"
|
||||
--passC:"-flto -march=native -mtune=native"
|
||||
--mm:atomicArc
|
||||
--deepCopy
|
||||
|
|
|
@ -35,8 +35,6 @@ type
|
|||
Chessboard* = object
|
||||
## A chessboard
|
||||
|
||||
# The actual board where pieces live
|
||||
grid*: array[64, Piece]
|
||||
# The current position
|
||||
position*: Position
|
||||
# List of all previously reached positions
|
||||
|
@ -51,14 +49,9 @@ proc hash*(self: var Chessboard)
|
|||
|
||||
proc newChessboard*: Chessboard =
|
||||
## Returns a new, empty chessboard
|
||||
for i in 0..63:
|
||||
result.grid[i] = nullPiece()
|
||||
result.position = Position(enPassantSquare: nullSquare(), sideToMove: White)
|
||||
|
||||
|
||||
# Indexing operations
|
||||
func `[]`*(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
|
||||
func `[]=`*(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece
|
||||
for i in Square(0)..Square(63):
|
||||
result.position.mailbox[i] = nullPiece()
|
||||
|
||||
|
||||
func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} =
|
||||
|
@ -102,7 +95,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard =
|
|||
let square = makeSquare(row, column)
|
||||
piece = c.fromChar()
|
||||
result.position.pieces[piece.color][piece.kind].setBit(square)
|
||||
result.grid[square] = piece
|
||||
result.position.mailbox[square] = piece
|
||||
inc(column)
|
||||
of '/':
|
||||
# Next row
|
||||
|
@ -190,7 +183,7 @@ func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.i
|
|||
|
||||
func getPiece*(self: Chessboard, square: Square): Piece {.inline.} =
|
||||
## Gets the piece at the given square
|
||||
return self.grid[square]
|
||||
return self.position.mailbox[square]
|
||||
|
||||
|
||||
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
|
||||
|
@ -218,7 +211,7 @@ proc spawnPiece*(self: var Chessboard, square: Square, piece: Piece) =
|
|||
when not defined(danger):
|
||||
doAssert self.getPiece(square).kind == Empty
|
||||
self.addPieceToBitboard(square, piece)
|
||||
self.grid[square] = piece
|
||||
self.position.mailbox[square] = piece
|
||||
|
||||
|
||||
proc removePiece*(self: var Chessboard, square: Square) =
|
||||
|
@ -228,7 +221,7 @@ proc removePiece*(self: var Chessboard, square: Square) =
|
|||
let piece = self.getPiece(square)
|
||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||
self.removePieceFromBitboard(square)
|
||||
self.grid[square] = nullPiece()
|
||||
self.position.mailbox[square] = nullPiece()
|
||||
|
||||
|
||||
proc movePiece*(self: var Chessboard, move: Move) =
|
||||
|
@ -457,46 +450,13 @@ proc canCastle*(self: Chessboard): tuple[queen, king: bool] =
|
|||
break
|
||||
|
||||
|
||||
proc update*(self: var Chessboard) =
|
||||
## Updates the internal grid representation
|
||||
## according to the positional data stored
|
||||
## in the chessboard
|
||||
for i in 0..63:
|
||||
self.grid[i] = nullPiece()
|
||||
for sq in self.position.pieces[White][Pawn]:
|
||||
self.grid[sq] = Piece(color: White, kind: Pawn)
|
||||
for sq in self.position.pieces[Black][Pawn]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Pawn)
|
||||
for sq in self.position.pieces[White][Bishop]:
|
||||
self.grid[sq] = Piece(color: White, kind: Bishop)
|
||||
for sq in self.position.pieces[Black][Bishop]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Bishop)
|
||||
for sq in self.position.pieces[White][Knight]:
|
||||
self.grid[sq] = Piece(color: White, kind: Knight)
|
||||
for sq in self.position.pieces[Black][Knight]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Knight)
|
||||
for sq in self.position.pieces[White][Rook]:
|
||||
self.grid[sq] = Piece(color: White, kind: Rook)
|
||||
for sq in self.position.pieces[Black][Rook]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Rook)
|
||||
for sq in self.position.pieces[White][Queen]:
|
||||
self.grid[sq] = Piece(color: White, kind: Queen)
|
||||
for sq in self.position.pieces[Black][Queen]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Queen)
|
||||
for sq in self.position.pieces[White][King]:
|
||||
self.grid[sq] = Piece(color: White, kind: King)
|
||||
for sq in self.position.pieces[Black][King]:
|
||||
self.grid[sq] = Piece(color: Black, kind: King)
|
||||
|
||||
|
||||
|
||||
proc `$`*(self: Chessboard): string =
|
||||
result &= "- - - - - - - -"
|
||||
var file = 8
|
||||
for i in 0..7:
|
||||
result &= "\n"
|
||||
for j in 0..7:
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
let piece = self.position.mailbox[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
result &= "x "
|
||||
continue
|
||||
|
@ -521,7 +481,7 @@ proc pretty*(self: Chessboard): string =
|
|||
result &= "\x1b[39;44;1m"
|
||||
else:
|
||||
result &= "\x1b[39;40;1m"
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
let piece = self.position.mailbox[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
result &= " \x1b[0m"
|
||||
else:
|
||||
|
@ -541,7 +501,7 @@ proc toFEN*(self: Chessboard): string =
|
|||
for i in 0..7:
|
||||
skip = 0
|
||||
for j in 0..7:
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
let piece = self.position.mailbox[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
inc(skip)
|
||||
elif skip > 0:
|
||||
|
|
|
@ -373,7 +373,8 @@ proc doMove*(self: var Chessboard, move: Move) =
|
|||
enPassantSquare: enPassantTarget,
|
||||
pieces: self.position.pieces,
|
||||
castlingAvailability: self.position.castlingAvailability,
|
||||
zobristKey: self.position.zobristKey
|
||||
zobristKey: self.position.zobristKey,
|
||||
mailbox: self.position.mailbox
|
||||
)
|
||||
if self.position.enPassantSquare != nullSquare():
|
||||
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(move.targetSquare))
|
||||
|
@ -485,7 +486,6 @@ proc unmakeMove*(self: var Chessboard) =
|
|||
if self.positions.len() == 0:
|
||||
return
|
||||
self.position = self.positions.pop()
|
||||
self.update()
|
||||
|
||||
|
||||
## Testing stuff
|
||||
|
@ -494,10 +494,12 @@ proc unmakeMove*(self: var Chessboard) =
|
|||
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
|
||||
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
|
||||
|
||||
|
||||
proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) =
|
||||
let pieces = board.countPieces(kind, color)
|
||||
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
|
||||
|
||||
|
||||
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
|
||||
var i = 0
|
||||
for square in bitboard:
|
||||
|
|
|
@ -49,8 +49,18 @@ func nullSquare*: Square {.inline.} = Square(-1'i8)
|
|||
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
|
||||
func isValid*(a: Square): bool {.inline.} = a.int8 in 0..63
|
||||
func isLightSquare*(a: Square): bool {.inline.} = (a.int8 and 2) == 0
|
||||
|
||||
# Overridden operators for our distinct type
|
||||
func `==`*(a, b: Square): bool {.inline.} = a.int8 == b.int8
|
||||
func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8
|
||||
func `<`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 < b.int8
|
||||
func `>`*(a: SomeInteger, b: Square): bool {.inline.} = a.int8 > b.int8
|
||||
func `<=`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 <= b.int8
|
||||
func `>=`*(a: SomeInteger, b: Square): bool {.inline.} = a.int8 >= b.int8
|
||||
func `<`*(a, b: Square): bool {.inline.} = a.int8 < b.int8
|
||||
func `>`*(a, b: Square): bool {.inline.} = a.int8 > b.int8
|
||||
func `<=`*(a, b: Square): bool {.inline.} = a.int8 <= b.int8
|
||||
func `>=`*(a, b: Square): bool {.inline.} = a.int8 >= b.int8
|
||||
func `-`*(a, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
func `-`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
func `-`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
|
|
|
@ -43,15 +43,18 @@ type
|
|||
sideToMove*: PieceColor
|
||||
# Positional bitboards for all pieces
|
||||
pieces*: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, Bitboard]]
|
||||
# Pieces pinned for the current side to move
|
||||
diagonalPins*: Bitboard # Pinned diagonally (by a queen or bishop)
|
||||
orthogonalPins*: Bitboard # Pinned orthogonally (by a queen or rook)
|
||||
# Pin rays for the current side to move
|
||||
diagonalPins*: Bitboard # Rays from a bishop or queen
|
||||
orthogonalPins*: Bitboard # Rays from a rook or queen
|
||||
# Pieces checking the current side to move
|
||||
checkers*: Bitboard
|
||||
# Zobrist hash of this position
|
||||
zobristKey*: ZobristKey
|
||||
# Cached result of drawByRepetition()
|
||||
repetitionDraw*: bool
|
||||
# A mailbox for fast piece lookup by
|
||||
# location
|
||||
mailbox*: array[Square(0)..Square(63), Piece]
|
||||
|
||||
|
||||
func getKingStartingSquare*(color: PieceColor): Square {.inline.} =
|
||||
|
|
|
@ -44,20 +44,8 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
|
|||
return
|
||||
elif ply == 1 and bulk:
|
||||
if divide:
|
||||
var postfix = ""
|
||||
for move in moves:
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
postfix = "b"
|
||||
of PromoteToKnight:
|
||||
postfix = "n"
|
||||
of PromoteToRook:
|
||||
postfix = "r"
|
||||
of PromoteToQueen:
|
||||
postfix = "q"
|
||||
else:
|
||||
postfix = ""
|
||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1"
|
||||
echo &"{move.toAlgebraic()}: 1"
|
||||
if verbose:
|
||||
echo ""
|
||||
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
||||
|
@ -109,20 +97,7 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
|
|||
let next = board.perft(ply - 1, verbose, bulk=bulk)
|
||||
board.unmakeMove()
|
||||
if divide and (not bulk or ply > 1):
|
||||
var postfix = ""
|
||||
if move.isPromotion():
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
postfix = "b"
|
||||
of PromoteToKnight:
|
||||
postfix = "n"
|
||||
of PromoteToRook:
|
||||
postfix = "r"
|
||||
of PromoteToQueen:
|
||||
postfix = "q"
|
||||
else:
|
||||
discard
|
||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}"
|
||||
echo &"{move.toAlgebraic()}: {next.nodes}"
|
||||
if verbose:
|
||||
echo ""
|
||||
result.nodes += next.nodes
|
||||
|
@ -134,63 +109,6 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
|
|||
result.checkmates += next.checkmates
|
||||
|
||||
|
||||
proc handleGoCommand(board: var Chessboard, command: seq[string]) =
|
||||
if len(command) < 2:
|
||||
echo &"Error: go: invalid number of arguments"
|
||||
return
|
||||
case command[1]:
|
||||
of "perft":
|
||||
if len(command) == 2:
|
||||
echo &"Error: go: perft: invalid number of arguments"
|
||||
return
|
||||
var
|
||||
args = command[2].splitWhitespace()
|
||||
bulk = false
|
||||
verbose = false
|
||||
captures = false
|
||||
if args.len() > 1:
|
||||
var ok = true
|
||||
for arg in args[1..^1]:
|
||||
case arg:
|
||||
of "bulk":
|
||||
bulk = true
|
||||
of "verbose":
|
||||
verbose = true
|
||||
of "captures":
|
||||
captures = true
|
||||
else:
|
||||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||||
ok = false
|
||||
break
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
let ply = parseInt(args[0])
|
||||
if bulk:
|
||||
let t = cpuTime()
|
||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose, capturesOnly=captures).nodes
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
||||
else:
|
||||
let t = cpuTime()
|
||||
let data = board.perft(ply, divide=true, verbose=verbose, capturesOnly=captures)
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||
echo &" - Captures: {data.captures}"
|
||||
echo &" - Checks: {data.checks}"
|
||||
echo &" - E.P: {data.enPassant}"
|
||||
echo &" - Checkmates: {data.checkmates}"
|
||||
echo &" - Castles: {data.castles}"
|
||||
echo &" - Promotions: {data.promotions}"
|
||||
echo ""
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
|
||||
except ValueError:
|
||||
echo "Error: go: perft: invalid depth"
|
||||
else:
|
||||
echo &"Error: go: unknown subcommand '{command[1]}'"
|
||||
|
||||
|
||||
proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.discardable.} =
|
||||
if len(command) != 2:
|
||||
echo &"Error: move: invalid number of arguments"
|
||||
|
@ -253,6 +171,70 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc
|
|||
echo &"Error: move: {moveString} is illegal"
|
||||
|
||||
|
||||
proc handleGoCommand(board: var Chessboard, command: seq[string]) =
|
||||
if len(command) < 2:
|
||||
echo &"Error: go: invalid number of arguments"
|
||||
return
|
||||
case command[1]:
|
||||
of "perft":
|
||||
if len(command) == 2:
|
||||
echo &"Error: go: perft: invalid number of arguments"
|
||||
return
|
||||
var
|
||||
args = command[2].splitWhitespace()
|
||||
bulk = false
|
||||
verbose = false
|
||||
captures = false
|
||||
divide = true
|
||||
if args.len() > 1:
|
||||
var ok = true
|
||||
for arg in args[1..^1]:
|
||||
case arg:
|
||||
of "bulk":
|
||||
bulk = true
|
||||
of "verbose":
|
||||
verbose = true
|
||||
of "captures":
|
||||
captures = true
|
||||
of "nosplit":
|
||||
divide = false
|
||||
else:
|
||||
echo &"Error: go: {command[1]}: invalid argument '{args[1]}'"
|
||||
ok = false
|
||||
break
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
let ply = parseInt(args[0])
|
||||
if bulk:
|
||||
let t = cpuTime()
|
||||
let nodes = board.perft(ply, divide=divide, bulk=true, verbose=verbose, capturesOnly=captures).nodes
|
||||
let tot = cpuTime() - t
|
||||
if divide:
|
||||
echo ""
|
||||
echo &"Nodes searched (bulk-counting: on): {nodes}"
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
||||
else:
|
||||
let t = cpuTime()
|
||||
let data = board.perft(ply, divide=divide, verbose=verbose, capturesOnly=captures)
|
||||
let tot = cpuTime() - t
|
||||
if divide:
|
||||
echo ""
|
||||
echo &"Nodes searched (bulk-counting: off): {data.nodes}"
|
||||
echo &" - Captures: {data.captures}"
|
||||
echo &" - Checks: {data.checks}"
|
||||
echo &" - E.P: {data.enPassant}"
|
||||
echo &" - Checkmates: {data.checkmates}"
|
||||
echo &" - Castles: {data.castles}"
|
||||
echo &" - Promotions: {data.promotions}"
|
||||
echo ""
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
|
||||
except ValueError:
|
||||
echo &"error: go: {command[1]}: invalid depth"
|
||||
else:
|
||||
echo &"error: go: unknown subcommand '{command[1]}'"
|
||||
|
||||
|
||||
proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
|
||||
if len(command) < 2:
|
||||
echo "Error: position: invalid number of arguments"
|
||||
|
@ -322,9 +304,9 @@ proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
|
|||
return
|
||||
|
||||
|
||||
|
||||
const HELP_TEXT = """Nimfish help menu:
|
||||
- go: Begin a search
|
||||
- go: Begin a search. Currently does not implement UCI search features (simply
|
||||
switch to UCI mode for that)
|
||||
Subcommands:
|
||||
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
|
||||
print the results
|
||||
|
@ -332,22 +314,23 @@ const HELP_TEXT = """Nimfish help menu:
|
|||
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
||||
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
||||
- captures: Only generate capture moves
|
||||
- nosplit: Do not print the number of legal moves after each root move
|
||||
Example: go perft 5 bulk
|
||||
- position: Get/set board position
|
||||
Subcommands:
|
||||
- fen [string]: Set the board to the given fen string if one is provided, or print
|
||||
the current position as a FEN string if no arguments are given
|
||||
- startpos: Set the board to the starting position
|
||||
- kiwipete: Set the board to famous kiwipete position
|
||||
- kiwipete: Set the board to the famous kiwipete position
|
||||
- pretty: Pretty-print the current position
|
||||
- print: Print the current position using ASCII characters only
|
||||
Options:
|
||||
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
|
||||
in algebraic notation after the position is loaded. This option only applies
|
||||
to the subcommands that set a position, it is ignored otherwise
|
||||
- moves {moveList}: Perform the given moves in algebraic notation
|
||||
after the position is loaded. This option only applies to the
|
||||
subcommands that set a position, it is ignored otherwise
|
||||
Examples:
|
||||
- position startpos
|
||||
- position fen "..." moves a2a3 a7a6
|
||||
- position fen ... moves a2a3 a7a6
|
||||
- clear: Clear the screen
|
||||
- move <move>: Perform the given move in algebraic notation
|
||||
- castle: Print castling rights for the side to move
|
||||
|
@ -361,15 +344,14 @@ const HELP_TEXT = """Nimfish help menu:
|
|||
- pos <args>: Shorthand for "position <args>"
|
||||
- get <square>: Get the piece on the given square
|
||||
- atk <square>: Print which pieces are currently attacking the given square
|
||||
- pins: Print the current pin mask
|
||||
- checks: Print the current checks mask
|
||||
- skip: Swap the side to move
|
||||
- pins: Print the current pin masks, if any
|
||||
- checks: Print the current check mask, if in check
|
||||
- skip: Make a null move (i.e. pass your turn). Useful for debugging. Very much illegal
|
||||
- uci: enter UCI mode
|
||||
- quit: exit
|
||||
- zobrist: Print the zobrist key for the current position
|
||||
- quit: exit nimfish
|
||||
- zobrist: Print the zobrist hash for the current position
|
||||
- eval: Evaluate the current position
|
||||
- rep: Show whether this position is a draw by repetition
|
||||
"""
|
||||
- rep: Show whether this position is a draw by repetition"""
|
||||
|
||||
|
||||
proc commandLoop*: int =
|
||||
|
@ -440,8 +422,10 @@ proc commandLoop*: int =
|
|||
echo "error: get: invalid square"
|
||||
continue
|
||||
of "castle":
|
||||
let castleRights = board.position.castlingAvailability[board.position.sideToMove]
|
||||
let canCastle = board.canCastle()
|
||||
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if castleRights.king: \"yes\" else: \"no\")}\n - Queen side: {(if castleRights.queen: \"yes\" else: \"no\")}"
|
||||
echo &"{($board.position.sideToMove)} can currently castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
of "check":
|
||||
echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||||
of "pins":
|
||||
|
@ -450,13 +434,14 @@ proc commandLoop*: int =
|
|||
if board.position.diagonalPins != 0:
|
||||
echo &"Diagonal pins:\n{board.position.diagonalPins}"
|
||||
of "checks":
|
||||
echo board.position.checkers
|
||||
if board.position.checkers != 0:
|
||||
echo board.position.checkers
|
||||
of "quit":
|
||||
return 0
|
||||
of "zobrist":
|
||||
echo board.position.zobristKey.uint64
|
||||
of "rep":
|
||||
echo board.position.repetitionDraw
|
||||
echo "Position is drawn by repetition: ", if board.position.repetitionDraw: "yes" else: "no"
|
||||
of "eval":
|
||||
echo &"Eval: {board.evaluate()}"
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue