Project 03 - Chess Engine (OOP Version)
Estimated time: 8–15 hours core | Level: Engineering (hardest project in this module)
This is not a warmup. Before you write a line of code, read the full spec and draw the class diagram. Chess is complex enough that an improvised class structure will trap you in refactoring debt by the third class you implement.
The goal is not to build Deep Blue. The goal is to build a working chess engine whose design is so clean that the move validation logic falls naturally out of the class hierarchy.
Learning Objectives
By the time this project is complete, you will have practiced:
- Designing a non-trivial ABC hierarchy (
Piece) and understanding when abstract methods are the right tool - Implementing
__getitem__and__setitem__to make aBoardobject subscriptable with algebraic coordinates - Using
dataclasses.dataclassto carry structured data (Move) with zero boilerplate - Separating concerns: piece logic lives on piece classes, board logic lives on
Board, game rules live onChessGame - Understanding Method Resolution Order (MRO) when pieces share behaviour (e.g., sliding pieces share move-generation logic)
- Implementing
__repr__and__str__at multiple levels so debugging is tractable - Detecting game state (check) through object composition and method calls, not through a tangle of global variables
System Overview
You are building a chess engine with the following components:
Piece- abstract base class for all piecesPawn,Rook,Bishop,Knight,Queen,King- concrete piece classesBoard- the 8x8 grid, subscriptable via algebraic notationMove- a dataclass carrying move informationChessGame- game state, turn management, check detection, move executionAlgebraicParser- parses algebraic notation strings intoMoveobjects
The engine handles: piece movement, turn validation, check detection, and algebraic notation input. It does not need to handle checkmate (though the extension challenge covers it), en passant, or castling in the core requirements.
Requirements
R1 - Color enum
from enum import Enum, auto
class Color(Enum):
WHITE = auto()
BLACK = auto()
Every piece has a color. The engine alternates turns by color.
R2 - Position dataclass
from dataclasses import dataclass
@dataclass(frozen=True)
class Position:
row: int # 0–7, where 0 is rank 1 (white's back rank)
col: int # 0–7, where 0 is the a-file
frozen=TruemakesPositionimmutable and hashable (usable as a dict key or set member).- Add a classmethod
from_algebraic(notation: str) -> "Position"that converts"e4"toPosition(row=3, col=4). - Add a method
to_algebraic() -> strthat converts back to"e4". - Validate that row and col are in range 0–7. Raise
ValueErrorfor out-of-range positions.
R3 - Move dataclass
@dataclass
class Move:
from_pos: Position
to_pos: Position
piece: "Piece"
captured: Optional["Piece"] = None
is_promotion: bool = False
promotion_piece_type: Optional[type] = None
- A
Movecarries everything needed to execute or undo a move. __repr__should produce something useful:"Move(Pawn(WHITE) e2->e4)".
R4 - Piece Abstract Base Class
- Abstract properties:
symbol(str - the single-character piece symbol:P,R,B,N,Q,K),value(int - point value for evaluation: Pawn=1, Knight=3, Bishop=3, Rook=5, Queen=9, King=0). - Attributes:
color(Color),position(Position),has_moved(bool, initialised toFalse). - Abstract method:
valid_moves(board: "Board") -> list[Move]- returns all pseudo-legal moves (moves that are geometrically valid, ignoring whether the king would be in check after the move). Filtering for check is done inChessGame, not here. - Concrete method:
__repr__:"Pawn(WHITE)". - Concrete method:
__str__: returns the colored piece symbol - uppercase for white, lowercase for black. E.g.,"P"for a white pawn,"p"for a black pawn. - Concrete method:
is_enemy(other: "Piece") -> bool: returnsTrueif the other piece is notNoneand has the opposite color.
R5 - Concrete Piece Classes
Implement all six. Each overrides symbol, value, and valid_moves. Here are the movement rules to implement:
Pawn
- Moves forward one square (toward higher rows for white, lower rows for black).
- Can move two squares forward from its starting rank (row 1 for white, row 6 for black). Only if both squares are empty.
- Captures diagonally forward one square. Does not capture straight ahead.
has_movedis set toTrueafter the first move - this blocks the two-square advance.
Rook
- Slides any number of squares horizontally or vertically.
- Cannot jump over pieces. Stops before a friendly piece, can land on an enemy piece (capturing it).
Bishop
- Slides any number of squares diagonally. Same jump rules as Rook.
Knight
- Jumps in an L-shape: two squares in one direction, one square perpendicular (or vice versa). Eight possible destinations.
- The only piece that can jump over other pieces.
Queen
- Combines Rook and Bishop movement. Consider using composition or a shared helper method.
King
- Moves one square in any direction (horizontal, vertical, or diagonal). Eight possible destinations.
- Cannot move to a square occupied by a friendly piece.
- Cannot move to a square that would put the king in check. This constraint is enforced in
ChessGame, not inKing.valid_moves.
R6 - Board class
- Internal storage: a 8x8 2D list (
list[list[Optional[Piece]]]), initialised toNone. __getitem__(self, pos): accepts aPositionobject or an algebraic string (e.g.,"e4"). Returns thePieceat that position, orNoneif empty.__setitem__(self, pos, piece): accepts aPositionor algebraic string. Sets the position to the given piece (orNoneto clear it).setup_standard_position(): places all 32 pieces in their standard starting positions. White pieces on ranks 1–2 (rows 0–1), black pieces on ranks 7–8 (rows 6–7).get_pieces(color: Color) -> list[Piece]: returns all pieces of a given color currently on the board.find_king(color: Color) -> Piece: returns the King piece for the given color. RaisesValueErrorif no king is found (should not happen in a valid game).copy() -> "Board": returns a deep copy of the board. Required for check-detection: you simulate moves on the copy, not the real board.__str__: returns an 8x8 ASCII grid of the board, with rank numbers and file letters. Example below.__repr__:"Board(32 pieces)".
Board.__str__ format:
a b c d e f g h
8 r n b q k b n r
7 p p p p p p p p
6 . . . . . . . .
5 . . . . . . . .
4 . . . . . . . .
3 . . . . . . . .
2 P P P P P P P P
1 R N B Q K B N R
R7 - ChessGame class
- Attributes:
board(Board),current_turn(Color, starts with WHITE),move_history(list of Move),captured_pieces(dict mapping Color to list of Piece - tracks what each side has captured). get_legal_moves(piece: Piece) -> list[Move]: returnspiece.valid_moves(board)filtered to exclude moves that would leave the current player's king in check. Implement this by simulating each move on a board copy and checking for check.is_in_check(color: Color) -> bool: returnsTrueif the king of the given color is currently under attack. The king is in check if any enemy piece has a pseudo-legal move that lands on the king's position.make_move(move: Move) -> None: executes the move on the board. Updates piece positions, setshas_moved = True, records the captured piece if any, appends tomove_history, switchescurrent_turn.parse_and_move(notation: str) -> Move: parses a move string like"e2e4"or"e2-e4", finds the piece, validates it is the current player's piece, gets legal moves, executes if legal. RaisesIllegalMoveErrorfor illegal moves.undo_last_move() -> Move: reverses the last move. Restores captured pieces, resetshas_movedif it was the piece's first move, switchescurrent_turnback.__repr__:"ChessGame(move 14, WHITE to play, check=False)".
R8 - IllegalMoveError
Custom exception. __str__: "IllegalMoveError: Pawn(WHITE) cannot move from e2 to e5".
R9 - Demonstration block
Your __main__ block must produce the exact expected output shown below.
Starter Code Skeleton
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Optional
import copy
# ── Color ─────────────────────────────────────────────────────────────────────
class Color(Enum):
WHITE = auto()
BLACK = auto()
# ── Custom Exceptions ─────────────────────────────────────────────────────────
class IllegalMoveError(Exception):
def __init__(self, piece: "Piece", from_pos: "Position", to_pos: "Position"):
self.piece = piece
self.from_pos = from_pos
self.to_pos = to_pos
def __str__(self):
# TODO: "IllegalMoveError: Pawn(WHITE) cannot move from e2 to e5"
pass
# ── Position ──────────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class Position:
row: int
col: int
def __post_init__(self):
# TODO: validate row and col are 0-7, raise ValueError if not
pass
@classmethod
def from_algebraic(cls, notation: str) -> "Position":
# TODO: "e4" -> Position(row=3, col=4)
# col: a=0, b=1, ..., h=7
# row: "1"=0, "2"=1, ..., "8"=7
pass
def to_algebraic(self) -> str:
# TODO: Position(row=3, col=4) -> "e4"
pass
def __repr__(self):
return self.to_algebraic()
# ── Move ──────────────────────────────────────────────────────────────────────
@dataclass
class Move:
from_pos: Position
to_pos: Position
piece: "Piece"
captured: Optional["Piece"] = None
is_promotion: bool = False
promotion_piece_type: Optional[type] = None
def __repr__(self):
# TODO: "Move(Pawn(WHITE) e2->e4)"
pass
# ── Piece ABC ─────────────────────────────────────────────────────────────────
class Piece(ABC):
def __init__(self, color: Color, position: Position):
# TODO: store color and position
# TODO: initialise has_moved = False
pass
@property
@abstractmethod
def symbol(self) -> str:
pass
@property
@abstractmethod
def value(self) -> int:
pass
@abstractmethod
def valid_moves(self, board: "Board") -> list[Move]:
pass
def is_enemy(self, other: Optional["Piece"]) -> bool:
# TODO: return True if other is not None and other.color != self.color
pass
def __repr__(self):
# TODO: "Pawn(WHITE)"
pass
def __str__(self):
# TODO: uppercase symbol for WHITE, lowercase for BLACK
pass
# ── SlidingPieceMixin ─────────────────────────────────────────────────────────
class SlidingPieceMixin:
"""
Shared helper for pieces that slide along rays (Rook, Bishop, Queen).
Not a standalone class - used via multiple inheritance.
"""
def _slide_moves(self, board: "Board", directions: list[tuple[int, int]]) -> list[Move]:
# TODO: for each direction in directions:
# - start from self.position
# - step in the direction until off-board, blocked by friendly, or captures enemy
# - yield Move objects
pass
# ── Concrete Piece Classes ────────────────────────────────────────────────────
class Pawn(Piece):
@property
def symbol(self) -> str:
return "P"
@property
def value(self) -> int:
return 1
def valid_moves(self, board: "Board") -> list[Move]:
moves = []
direction = 1 if self.color == Color.WHITE else -1
# TODO: one-square advance (if square is empty)
# TODO: two-square advance from starting rank (if both squares empty, has_moved=False)
# TODO: diagonal captures (if enemy piece present)
return moves
class Rook(SlidingPieceMixin, Piece):
@property
def symbol(self) -> str:
return "R"
@property
def value(self) -> int:
return 5
def valid_moves(self, board: "Board") -> list[Move]:
# TODO: use _slide_moves with horizontal and vertical directions
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
return self._slide_moves(board, directions)
class Bishop(SlidingPieceMixin, Piece):
@property
def symbol(self) -> str:
return "B"
@property
def value(self) -> int:
return 3
def valid_moves(self, board: "Board") -> list[Move]:
# TODO: use _slide_moves with diagonal directions
directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)]
return self._slide_moves(board, directions)
class Knight(Piece):
@property
def symbol(self) -> str:
return "N"
@property
def value(self) -> int:
return 3
def valid_moves(self, board: "Board") -> list[Move]:
moves = []
offsets = [
(2, 1), (2, -1), (-2, 1), (-2, -1),
(1, 2), (1, -2), (-1, 2), (-1, -2),
]
# TODO: for each offset, compute target position
# TODO: skip if off-board or occupied by friendly piece
# TODO: create Move, capturing enemy if present
return moves
class Queen(SlidingPieceMixin, Piece):
@property
def symbol(self) -> str:
return "Q"
@property
def value(self) -> int:
return 9
def valid_moves(self, board: "Board") -> list[Move]:
# TODO: combine Rook and Bishop directions
directions = [(0, 1), (0, -1), (1, 0), (-1, 0),
(1, 1), (1, -1), (-1, 1), (-1, -1)]
return self._slide_moves(board, directions)
class King(Piece):
@property
def symbol(self) -> str:
return "K"
@property
def value(self) -> int:
return 0
def valid_moves(self, board: "Board") -> list[Move]:
moves = []
offsets = [
(1, 0), (-1, 0), (0, 1), (0, -1),
(1, 1), (1, -1), (-1, 1), (-1, -1),
]
# TODO: similar to Knight but only one step in each direction
return moves
# ── Board ─────────────────────────────────────────────────────────────────────
class Board:
def __init__(self):
# TODO: initialise self._grid as 8x8 list of None
pass
def _normalise(self, pos) -> Position:
"""Accept Position or algebraic string, return Position."""
if isinstance(pos, str):
return Position.from_algebraic(pos)
return pos
def __getitem__(self, pos) -> Optional[Piece]:
pos = self._normalise(pos)
# TODO: return self._grid[pos.row][pos.col]
pass
def __setitem__(self, pos, piece: Optional[Piece]):
pos = self._normalise(pos)
# TODO: set self._grid[pos.row][pos.col] = piece
pass
def setup_standard_position(self) -> None:
# TODO: place all 32 pieces in starting positions
# White pieces on rows 0-1, black on rows 6-7
# Back rank order: Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook
pass
def get_pieces(self, color: Color) -> list[Piece]:
# TODO: return all pieces of the given color
pass
def find_king(self, color: Color) -> "King":
# TODO: find and return the King for the given color
# Raise ValueError if not found
pass
def copy(self) -> "Board":
# TODO: return a deep copy of the board
# Use copy.deepcopy(self) or manually reconstruct
pass
def __str__(self) -> str:
# TODO: return ASCII board representation
# 8 rows top to bottom (rank 8 to rank 1)
# Each row: rank number + piece symbols (uppercase=white, lowercase=black, .=empty)
pass
def __repr__(self) -> str:
total = sum(1 for row in self._grid for cell in row if cell is not None)
return f"Board({total} pieces)"
# ── ChessGame ─────────────────────────────────────────────────────────────────
class ChessGame:
def __init__(self):
self.board = Board()
self.board.setup_standard_position()
self.current_turn = Color.WHITE
self.move_history: list[Move] = []
self.captured_pieces: dict[Color, list[Piece]] = {
Color.WHITE: [],
Color.BLACK: [],
}
def is_in_check(self, color: Color) -> bool:
# TODO: find the king of `color`
# TODO: for each enemy piece, get its valid_moves
# TODO: return True if any move's to_pos == king.position
pass
def get_legal_moves(self, piece: Piece) -> list[Move]:
# TODO: get pseudo-legal moves from piece.valid_moves(self.board)
# TODO: filter: for each move, apply it to a board copy, check if own king is in check
# TODO: return only moves that do NOT leave own king in check
pass
def make_move(self, move: Move) -> None:
# TODO: update board: clear from_pos, set to_pos
# TODO: update piece.position
# TODO: set piece.has_moved = True
# TODO: if captured, add to captured_pieces of current_turn
# TODO: append move to move_history
# TODO: switch current_turn
pass
def parse_and_move(self, notation: str) -> Move:
# TODO: parse notation (e.g. "e2e4" or "e2-e4") into from_pos and to_pos
# TODO: get piece at from_pos
# TODO: verify piece belongs to current_turn (raise IllegalMoveError otherwise)
# TODO: get legal moves for piece
# TODO: find the matching move (to_pos matches)
# TODO: if not found, raise IllegalMoveError
# TODO: call make_move and return the Move
pass
def undo_last_move(self) -> Move:
# TODO: pop last move from history
# TODO: reverse the board changes
# TODO: restore captured piece if any
# TODO: switch current_turn back
# TODO: return the undone move
pass
def __repr__(self) -> str:
move_num = len(self.move_history) + 1
in_check = self.is_in_check(self.current_turn)
turn = self.current_turn.name
return f"ChessGame(move {move_num}, {turn} to play, check={in_check})"
# ── Main Demonstration ────────────────────────────────────────────────────────
if __name__ == "__main__":
game = ChessGame()
print("=== Initial Board ===")
print(game.board)
print(game)
# Play the Scholar's Mate sequence (4-move checkmate attempt)
moves = ["e2e4", "e7e5", "d1h5", "b8c6", "f1c4", "a7a6", "h5f7"]
for notation in moves:
try:
move = game.parse_and_move(notation)
print(f"Played: {move}")
except IllegalMoveError as e:
print(e)
print("\n=== Board after Scholar's Mate attempt ===")
print(game.board)
print(game)
# Check detection
print(f"\nWhite in check: {game.is_in_check(Color.WHITE)}")
print(f"Black in check: {game.is_in_check(Color.BLACK)}")
# Legal moves for a specific piece
pawn_at_d7 = game.board["d7"]
if pawn_at_d7:
legal = game.get_legal_moves(pawn_at_d7)
print(f"\nLegal moves for {pawn_at_d7} at d7: {[m.to_pos for m in legal]}")
# Undo the last move
undone = game.undo_last_move()
print(f"\nUndone: {undone}")
print(game)
# Position conversion
pos = Position.from_algebraic("e4")
print(f"\nPosition e4: {pos}")
print(f"Back to algebraic: {pos.to_algebraic()}")
# Piece repr and str
rook = Rook(Color.WHITE, Position.from_algebraic("a1"))
print(f"\nRook repr: {repr(rook)}")
print(f"Rook str: {str(rook)}")
Expected Output
=== Initial Board ===
a b c d e f g h
8 r n b q k b n r
7 p p p p p p p p
6 . . . . . . . .
5 . . . . . . . .
4 . . . . . . . .
3 . . . . . . . .
2 P P P P P P P P
1 R N B Q K B N R
ChessGame(move 1, WHITE to play, check=False)
Played: Move(Pawn(WHITE) e2->e4)
Played: Move(Pawn(BLACK) e7->e5)
Played: Move(Queen(WHITE) d1->h5)
Played: Move(Knight(BLACK) b8->c6)
Played: Move(Bishop(WHITE) f1->c4)
Played: Move(Pawn(BLACK) a7->a6)
Played: Move(Queen(WHITE) h5->f7)
=== Board after Scholar's Mate attempt ===
a b c d e f g h
8 r . b q k b n r
7 p p p p . Q p p
6 p . n . . . . .
5 . . . . p . . .
4 . . B . P . . .
3 . . . . . . . .
2 P P P P . P P P
1 R N B . K . N R
ChessGame(move 8, BLACK to play, check=True)
White in check: False
Black in check: True
Legal moves for p at d7: []
Undone: Move(Queen(WHITE) h5->f7)
ChessGame(move 7, WHITE to play, check=False)
Position e4: e4
Back to algebraic: e4
Rook repr: Rook(WHITE)
Rook str: R
Step-by-Step Hints
Hint 1 - Build and test in strict order.
The dependency chain is: Color → Position → Move → Piece → concrete pieces → Board → ChessGame. Do not skip ahead. If Position.from_algebraic is broken, everything above it is broken too. Write a few test lines after each class before continuing.
Hint 2 - Position.from_algebraic("e4").
"e4"[0] is the file (column). "e4"[1] is the rank (row). Column: ord(char) - ord('a') gives 0–7. Row: int(rank) - 1 gives 0–7. So "e4" → col = 4, row = 3.
Hint 3 - Board.__getitem__ and __setitem__ make the board feel like a dictionary.
After implementing these, you can write board["e4"] to read and board["e4"] = piece to write. This is the entire point of these dunders - they make the board interface feel natural. All the piece move generation code will use this interface.
Hint 4 - SlidingPieceMixin._slide_moves is the hardest helper to get right.
For each direction (dr, dc), start at (piece.position.row + dr, piece.position.col + dc) and keep stepping. Stop when:
- Off the board (row or col out of 0–7 range) - break without adding this square.
- Friendly piece - break without adding this square.
- Enemy piece - add this square as a capture move, then break (cannot slide through).
- Empty square - add this square, continue stepping.
Hint 5 - Check detection requires a board copy.
is_in_check must not be implemented by simulating moves - it just looks at the current board state. For each enemy piece, call piece.valid_moves(board) (pseudo-legal moves) and see if any move.to_pos equals the king's position. Note: valid_moves may not be fully legal (they don't filter check) but that is intentional - you are checking if the king is threatened, and you need all geometrically reachable squares.
Hint 6 - get_legal_moves uses the board copy.
For each pseudo-legal move: create a copy of the board, apply the move to the copy, call is_in_check(color) on the copy. If the king is in check after the move, discard it. This is the standard approach for legal move generation and it is intentionally expensive (O(n * m) where n is your moves and m is enemy pieces). Performance optimisation is an extension challenge.
Hint 7 - make_move must update piece.position.
When you move a piece, you do two things to the board (board[from] = None, board[to] = piece) and one thing to the piece itself (piece.position = to_pos). Forgetting to update piece.position will cause check detection to fail because find_king returns the king object, and then you look at king.position - which is still the old position.
Hint 8 - MRO for Queen(SlidingPieceMixin, Piece).
Queen inherits from both SlidingPieceMixin and Piece. The MRO is [Queen, SlidingPieceMixin, Piece, ABC, object]. When Queen.valid_moves calls self._slide_moves(...), Python looks it up in MRO order: not on Queen, found on SlidingPieceMixin. When SlidingPieceMixin._slide_moves accesses self.position, self.color, it accesses them from the Piece part of the same object. This is the correct design - the mixin uses the piece's state without inheriting Piece itself (avoiding diamond issues).
Hint 9 - undo_last_move must know if it was the piece's first move.
The Move dataclass does not currently store was_first_move. Consider adding a field first_move_for_piece: bool = False to Move, and set it to not piece.has_moved before executing the move. During undo, if first_move_for_piece is True, reset piece.has_moved = False.
Hint 10 - The Scholar's Mate check requires Qf7 to threaten Ke8.
After Qh5-f7, the white Queen is on f7. The black King is on e8. f7 is diagonally adjacent to e8, so the Queen threatens the King. Verify your Queen move generation includes f7 and that your check detection catches it.
OOP Concepts Tested
| Concept | Where it appears |
|---|---|
| Abstract Base Classes | Piece(ABC) with @abstractmethod |
| Abstract properties | symbol, value on Piece |
| Multiple inheritance / MRO | SlidingPieceMixin, Queen(SlidingPieceMixin, Piece) |
__getitem__ / __setitem__ | Board subscriptable via algebraic notation |
__repr__ / __str__ | Every class, different purposes |
| Dataclasses | Position(frozen=True), Move |
frozen=True for hashability | Position used as dict key |
| Composition | ChessGame contains Board; Board contains Piece objects |
| Encapsulation | _grid private on Board, game state managed through methods |
| Enum | Color |
| Method overriding | All 6 concrete valid_moves implementations |
| Custom exceptions | IllegalMoveError |
Extension Challenges
Extension 1 - Checkmate and stalemate detection
Add ChessGame.is_checkmate(color) and ChessGame.is_stalemate(color). Checkmate: the player is in check and has no legal moves. Stalemate: not in check but no legal moves. Both require calling get_legal_moves for every piece of the given color - this is expensive but correct.
Extension 2 - Castling
Add castling support. Kingside castling: King moves two squares right, Rook jumps to the other side. Conditions: neither King nor Rook has moved, no pieces between them, King is not in check before, during, or after castling. This requires changes to King.valid_moves, make_move, and undo_last_move.
Extension 3 - En passant
Pawns can capture en passant when an enemy pawn moves two squares past the capturing pawn's rank. This requires tracking the last pawn double-move in game state and giving Pawn.valid_moves access to it. Implement ChessGame.en_passant_target: Optional[Position].
Extension 4 - Standard Algebraic Notation (SAN)
Extend AlgebraicParser to handle full SAN: e4, Nf3, exd5, O-O (castling), O-O-O (queenside castling), e8=Q (promotion), Nbd7 (disambiguation). This requires reverse-mapping from the target square and piece type back to the originating piece.
Extension 5 - Minimax AI with alpha-beta pruning
Add a MinimaxEngine class that implements the minimax algorithm with alpha-beta pruning. Use piece values and basic positional heuristics (pieces near the center are worth slightly more) as the evaluation function. Implement get_best_move(game, depth) that returns the best Move for the current player at the given search depth. Depth 3 should be playable on modern hardware for the middle game.
Extension 6 - FEN parsing and generation
Add Board.from_fen(fen_string) and Board.to_fen(). FEN (Forsyth-Edwards Notation) is the standard serialisation format for chess positions. Parsing FEN lets you load any published position and test your engine against known results (e.g., the starting position FEN is rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1).
