-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChessLLD.java
More file actions
566 lines (469 loc) · 21.9 KB
/
ChessLLD.java
File metadata and controls
566 lines (469 loc) · 21.9 KB
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
import java.util.ArrayList;
import java.util.List;
/**
* ============================================================================
* ULTIMATE CHESS LOW-LEVEL DESIGN (LLD) CHEAT SHEET
* ============================================================================
* * DESIGN PATTERNS USED IN THIS CODE:
* * 1. COMMAND PATTERN
* - WHAT: It turns an action (like moving a piece) into a standalone object
* that holds all the details of that action.
* - WHERE: The `Move` class.
* - WHY: Without this, moving a piece just overwrites the board and the history
* is lost forever. By saving every action as a `Move` object in a list,
* we can easily build an "Undo" button by reading the list backwards.
* * 2. POLYMORPHISM (Behavioral Pattern / OOP Principle)
* - WHAT: Allowing different child classes to be treated as their parent class,
* but each child does the task in its own unique way.
* - WHERE: The `Piece` abstract class and its children (Rook, Knight, etc.),
* specifically the `canMove()` method.
* - WHY: Instead of the `Game` class having a massive, ugly `if-else` block
* (if piece == knight do this, else if piece == rook do this), the Game
* just yells "Move!" and the specific piece knows its own math.
* * 3. SINGLE RESPONSIBILITY PRINCIPLE (SOLID Principle)
* - WHAT: A class should have only one reason to change. It should do one job.
* - WHERE: The `Board` class just holds the grid. The `Game` class just runs
* the turns. The `Piece` classes just calculate math.
* - WHY: If we want to change how a Knight moves, we ONLY touch the `Knight` class.
* We don't accidentally break the Board or the Game loop.
* ============================================================================
*/
// ==========================================
// 1. GAME STATUS (The Rules of State)
// ==========================================
/*
* INTERVIEW BONUS POINT:
* "I am using an Enum for the GameStatus instead of a String. Strings are dangerous
* because a typo like 'activ' instead of 'active' will crash the game logic.
* Enums guarantee strict type safety."
*/
enum GameStatus {
ACTIVE, BLACK_WIN, WHITE_WIN, FORFEIT, STALEMATE
}
// ==========================================
// 2. THE BOARD & SQUARES (The Table)
// ==========================================
/*
* The Square class represents one single tile on the 8x8 chessboard.
* It only knows two things: Where it is (x, y), and who is sitting on it (Piece).
*/
class Square {
private int x;
private int y;
private Piece piece; // Can be null if the tile is empty
public Square(int x, int y, Piece piece) {
this.x = x;
this.y = y;
this.piece = piece;
}
// Getters and Setters
public int getX() { return x; }
public int getY() { return y; }
public Piece getPiece() { return piece; }
public void setPiece(Piece piece) { this.piece = piece; }
}
/*
* The Board class is just the physical table.
* It sets up the pieces and checks if a path is physically blocked by other pieces.
*/
class Board {
Square[][] boxes;
public Board() {
this.boxes = new Square[8][8];
this.resetBoard(); // Automatically put all pieces in their starting spots
}
public Square getBox(int x, int y) {
// Safety check to prevent crashing if a player clicks outside the 8x8 grid
if (x < 0 || x > 7 || y < 0 || y > 7) {
return null;
}
return boxes[x][y];
}
/*
* INTERVIEW BONUS POINT:
* "To prevent pieces like the Queen from teleporting through other pieces,
* I wrote this isPathClear method. It mathematically walks step-by-step from
* the start square to the end square. If it hits any piece along the way,
* it instantly returns false, meaning the path is blocked."
*/
public boolean isPathClear(int startX, int startY, int endX, int endY) {
// Integer.compare returns +1 if moving forward, -1 if moving backward, 0 if not moving on that axis
int xDirection = Integer.compare(endX, startX);
int yDirection = Integer.compare(endY, startY);
// Take the very first step away from the starting square
int currentX = startX + xDirection;
int currentY = startY + yDirection;
// Keep walking step-by-step until we reach the final destination
while (currentX != endX || currentY != endY) {
// Look at the square we are standing on right now. Is someone there?
if (boxes[currentX][currentY].getPiece() != null) {
return false; // Crash! We hit a piece. The path is NOT clear.
}
// Take the next step
currentX += xDirection;
currentY += yDirection;
}
return true; // We made it to the end without hitting anything!
}
// This method physically places all 32 pieces onto their correct starting squares.
public void resetBoard() {
// Setup White Major Pieces (Row 0)
boxes[0][0] = new Square(0, 0, new Rook(true));
boxes[0][1] = new Square(0, 1, new Knight(true));
boxes[0][2] = new Square(0, 2, new Bishop(true));
boxes[0][3] = new Square(0, 3, new Queen(true));
boxes[0][4] = new Square(0, 4, new King(true));
boxes[0][5] = new Square(0, 5, new Bishop(true));
boxes[0][6] = new Square(0, 6, new Knight(true));
boxes[0][7] = new Square(0, 7, new Rook(true));
// Setup White Pawns (Row 1)
for (int i = 0; i < 8; i++) {
boxes[1][i] = new Square(1, i, new Pawn(true));
}
// Setup Empty Middle Squares (Rows 2, 3, 4, 5)
for (int i = 2; i < 6; i++) {
for (int j = 0; j < 8; j++) {
boxes[i][j] = new Square(i, j, null);
}
}
// Setup Black Pawns (Row 6)
for (int i = 0; i < 8; i++) {
boxes[6][i] = new Square(6, i, new Pawn(false));
}
// Setup Black Major Pieces (Row 7)
boxes[7][0] = new Square(7, 0, new Rook(false));
boxes[7][1] = new Square(7, 1, new Knight(false));
boxes[7][2] = new Square(7, 2, new Bishop(false));
boxes[7][3] = new Square(7, 3, new Queen(false));
boxes[7][4] = new Square(7, 4, new King(false));
boxes[7][5] = new Square(7, 5, new Bishop(false));
boxes[7][6] = new Square(7, 6, new Knight(false));
boxes[7][7] = new Square(7, 7, new Rook(false));
}
}
// ==========================================
// 3. THE MOVE HISTORY (Implementation of COMMAND PATTERN)
// ==========================================
/*
* WHAT THIS IS: The Command Pattern.
* WHY IT MATTERS: Instead of just moving a piece and forgetting the past, we create
* a "Receipt" of every single move.
* INTERVIEW BONUS POINT: "By creating a Move object, I can save it to a database
* for game replays, or I can use it to instantly reverse an action for an Undo feature."
*/
class Move {
private Player player; // Who made the move?
private Square start; // Where did they start?
private Square end; // Where did they land?
private Piece pieceMoved; // What piece did they move?
private Piece pieceKilled; // Did they kill an enemy? (Can be null)
public Move(Player player, Square start, Square end) {
this.player = player;
this.start = start;
this.end = end;
this.pieceMoved = start.getPiece(); // The piece sitting on the start square
}
public Square getStart() { return start; }
public Square getEnd() { return end; }
public Piece getPieceMoved() { return pieceMoved; }
public Piece getPieceKilled() { return pieceKilled; }
public void setPieceKilled(Piece pieceKilled) { this.pieceKilled = pieceKilled; }
}
// ==========================================
// 4. THE PIECES (Implementation of POLYMORPHISM)
// ==========================================
/*
* The base class for every piece on the board.
* It is 'abstract' because you cannot hold a generic "Piece" in your hand,
* you can only hold a specific child piece (like a Knight).
*/
abstract class Piece {
private boolean isWhite; // True if White team, False if Black team
private boolean isKilled; // True if captured and off the board
public Piece(boolean isWhite) {
this.isWhite = isWhite;
this.isKilled = false; // Everyone starts alive
}
public boolean isWhite() { return isWhite; }
public boolean isKilled() { return isKilled; }
public void setKilled(boolean killed) { this.isKilled = killed; }
/*
* INTERVIEW BONUS POINT:
* "This abstract method forces every child piece to define its own math rules.
* This follows the Open/Closed Principle. If we invent a new piece later,
* we just add a new class. We never have to edit our existing Game loop code."
*/
public abstract boolean canMove(Board board, Square start, Square end);
}
// --- SPECIFIC PIECE IMPLEMENTATIONS ---
class Knight extends Piece {
public Knight(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: You cannot land on a piece from your own team.
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) {
return false;
}
// Calculate the physical distance moved on the X and Y axis
int x = Math.abs(start.getX() - end.getX());
int y = Math.abs(start.getY() - end.getY());
// Knight math: Moves in an "L" shape (2 steps one way, 1 step the other).
// Therefore, x * y will ALWAYS equal 2 (either 2*1 or 1*2).
// Note: Knights can jump over pieces, so we do NOT call board.isPathClear() here!
return x * y == 2;
}
}
class Rook extends Piece {
public Rook(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: Cannot land on own team
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) return false;
int xDiff = Math.abs(start.getX() - end.getX());
int yDiff = Math.abs(start.getY() - end.getY());
// Rook math: Moves straight. This means either X changes OR Y changes, never both.
if (xDiff == 0 || yDiff == 0) {
// Rooks cannot jump! We must ask the board if the road is empty.
return board.isPathClear(start.getX(), start.getY(), end.getX(), end.getY());
}
return false;
}
}
class Bishop extends Piece {
public Bishop(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: Cannot land on own team
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) return false;
int xDiff = Math.abs(start.getX() - end.getX());
int yDiff = Math.abs(start.getY() - end.getY());
// Bishop math: Moves perfectly diagonal. Steps on X must exactly equal steps on Y.
if (xDiff == yDiff) {
// Bishops cannot jump! Ask the board if the road is empty.
return board.isPathClear(start.getX(), start.getY(), end.getX(), end.getY());
}
return false;
}
}
class Queen extends Piece {
public Queen(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: Cannot land on own team
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) return false;
int xDiff = Math.abs(start.getX() - end.getX());
int yDiff = Math.abs(start.getY() - end.getY());
// Queen math: She is a combination of a Rook (straight) AND a Bishop (diagonal).
// We just combine their two rules with an "OR" statement.
if ((xDiff == 0 || yDiff == 0) || (xDiff == yDiff)) {
// Queens cannot jump! Ask the board if the road is empty.
return board.isPathClear(start.getX(), start.getY(), end.getX(), end.getY());
}
return false;
}
}
class King extends Piece {
public King(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: Cannot land on own team
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) return false;
int xDiff = Math.abs(start.getX() - end.getX());
int yDiff = Math.abs(start.getY() - end.getY());
// King math: Can move in any direction, but ONLY 1 single step.
// We do not need board.isPathClear() because he only takes one step. There is no path.
return xDiff <= 1 && yDiff <= 1;
}
}
class Pawn extends Piece {
public Pawn(boolean isWhite) { super(isWhite); }
@Override
public boolean canMove(Board board, Square start, Square end) {
// Universal Rule: Cannot land on own team
if (end.getPiece() != null && end.getPiece().isWhite() == this.isWhite()) return false;
// Pawns only move forward, so we need real direction, not absolute values for X.
int xDiff = end.getX() - start.getX();
int yDiff = Math.abs(start.getY() - end.getY());
// White pieces start at the bottom and move UP (+1).
// Black pieces start at the top and move DOWN (-1).
int direction = this.isWhite() ? 1 : -1;
// Scenario A: Normal move. Walking straight ahead 1 step.
// Rule: The destination square MUST be completely empty.
if (xDiff == direction && yDiff == 0 && end.getPiece() == null) {
return true;
}
// Scenario B: Attack move. Walking diagonal 1 step.
// Rule: The destination square MUST have an enemy piece on it.
if (xDiff == direction && yDiff == 1 && end.getPiece() != null) {
return true;
}
// Note: For advanced LLD, you would add the "move 2 steps on first turn" rule here.
return false;
}
}
// ==========================================
// 5. THE PLAYERS (Abstraction)
// ==========================================
/*
* Abstract player class. Holds shared details.
* Will be extended by HumanPlayer and BotPlayer.
*/
abstract class Player {
private boolean whiteSide;
public Player(boolean whiteSide) { this.whiteSide = whiteSide; }
public boolean isWhiteSide() { return whiteSide; }
}
class HumanPlayer extends Player {
public HumanPlayer(boolean whiteSide) { super(whiteSide); }
}
class BotPlayer extends Player {
public BotPlayer(boolean whiteSide) { super(whiteSide); }
}
// ==========================================
// 6. THE GAME CONTROLLER (The Brains / Orchestrator)
// ==========================================
/*
* WHAT THIS IS: The facade or controller.
* WHY IT MATTERS: The user interface (or API) only ever talks to this class.
* It coordinates the Board, the Players, and the Move History all in one place.
*/
class Game {
private Player[] players; // Array holding exactly 2 players
private Board board; // The chess table
private Player currentTurn; // Whose turn is it right now?
private GameStatus status; // Is the game active or over?
private List<Move> movesPlayed; // The Command Pattern history list
public Game(Player p1, Player p2) {
this.players = new Player[]{p1, p2};
this.board = new Board();
this.movesPlayed = new ArrayList<>();
// Chess rules: White always makes the very first move.
this.currentTurn = p1.isWhiteSide() ? p1 : p2;
this.status = GameStatus.ACTIVE;
}
public Board getBoard() { return board; }
public GameStatus getStatus() { return status; }
public void setStatus(GameStatus status) { this.status = status; }
/*
* INTERVIEW BONUS POINT:
* "This is the core business logic flow. I built it as a strict checklist.
* It validates the game state, validates ownership, validates piece math,
* executes the move, and updates history. If any check fails, it returns false safely."
*/
public boolean playerMove(Player player, int startX, int startY, int endX, int endY) {
// Check 1: Is the game actually still running?
if (this.status != GameStatus.ACTIVE) {
System.out.println("Move failed. The game is already over.");
return false;
}
// Grab the start and end squares from the board
Square startSquare = board.getBox(startX, startY);
Square endSquare = board.getBox(endX, endY);
// Package this action into a Command object right away
Move move = new Move(player, startSquare, endSquare);
// Check 2: Did the player click on an empty square?
Piece sourcePiece = move.getStart().getPiece();
if (sourcePiece == null) {
System.out.println("Move failed. There is no piece on the starting square.");
return false;
}
// Check 3: Is the player trying to move the enemy's piece?
if (player.isWhiteSide() != sourcePiece.isWhite()) {
System.out.println("Move failed. You cannot move your opponent's piece!");
return false;
}
// Check 4: Is it actually this player's turn?
if (player != currentTurn) {
System.out.println("Move failed. Please wait for your turn.");
return false;
}
// Check 5: POLYMORPHISM IN ACTION!
// We just ask the piece, "Hey, can you do this math?"
if (!sourcePiece.canMove(board, move.getStart(), move.getEnd())) {
System.out.println("Move failed. That piece is not allowed to move there.");
return false;
}
// Check 6: Capture Logic. Is there an enemy piece on the landing square?
Piece destPiece = move.getEnd().getPiece();
if (destPiece != null) {
destPiece.setKilled(true); // Mark enemy as dead
move.setPieceKilled(destPiece); // Save the dead enemy in our history receipt!
// If the piece we just killed was the King, the game is over!
if (destPiece instanceof King) {
this.setStatus(player.isWhiteSide() ? GameStatus.WHITE_WIN : GameStatus.BLACK_WIN);
System.out.println("CHECKMATE! Game Over.");
}
}
// EXECUTION: Physically pick up the piece and put it on the new square
move.getEnd().setPiece(move.getStart().getPiece()); // Put on destination
move.getStart().setPiece(null); // Empty the start square
// Save the receipt to our history list
movesPlayed.add(move);
// Flip the turn to the other player
this.currentTurn = (this.currentTurn == players[0]) ? players[1] : players[0];
System.out.println("Move successfully completed.");
return true;
}
/*
* INTERVIEW BONUS POINT:
* "Because I utilized the Command Pattern to store a List of Move objects,
* building an Undo feature takes just 5 lines of code. We pop the last object,
* reverse the locations, and resurrect any killed pieces."
*/
public boolean undoLastMove() {
// Check if there is actually any history to undo
if (this.movesPlayed.isEmpty()) {
System.out.println("Undo failed. No moves have been made yet.");
return false;
}
// Get the very last move from the list and remove it
Move lastMove = this.movesPlayed.remove(this.movesPlayed.size() - 1);
// 1. Put the piece that moved BACK onto its starting square
Piece pieceThatMoved = lastMove.getEnd().getPiece();
lastMove.getStart().setPiece(pieceThatMoved);
// 2. Bring the dead enemy piece back to life (if there was one)
Piece killedPiece = lastMove.getPieceKilled();
if (killedPiece != null) {
killedPiece.setKilled(false); // Resurrect
lastMove.getEnd().setPiece(killedPiece); // Put back on board
// If we just brought a King back to life, the game is no longer over!
if (killedPiece instanceof King) {
this.setStatus(GameStatus.ACTIVE);
}
} else {
// If nobody died, the ending square just becomes empty again
lastMove.getEnd().setPiece(null);
}
// 3. Flip the turn back to the person who just undid their move
this.currentTurn = (this.currentTurn == players[0]) ? players[1] : players[0];
System.out.println("Undo successfully completed. Turn reverted.");
return true;
}
}
// ==========================================
// 7. MAIN EXECUTION (Simulation)
// ==========================================
public class ChessLLD {
public static void main(String[] args) {
System.out.println("=== STARTING CHESS LLD SIMULATION ===");
// Create players
Player p1 = new HumanPlayer(true); // White Team
Player p2 = new BotPlayer(false); // Black Team (AI/Bot)
// Initialize Game
Game game = new Game(p1, p2);
// --- SIMULATING MOVES ---
System.out.println("\n[TURN 1] White moves a Pawn forward:");
// Moving White Pawn from (1,0) to (2,0)
game.playerMove(p1, 1, 0, 2, 0);
System.out.println("\n[TURN 2] Black moves a Knight (L-shape):");
// Moving Black Knight from (7,1) to (5,2)
game.playerMove(p2, 7, 1, 5, 2);
System.out.println("\n[UNDO ACTION] Player clicks Undo button to reverse Black's turn:");
// This will put the Black Knight back and make it Black's turn again
game.undoLastMove();
System.out.println("\n[INVALID MOVE CHECK] White tries to jump a Rook through a Pawn:");
// Rooks cannot jump! This should be caught by our isPathClear() method and fail safely.
game.playerMove(p1, 0, 0, 3, 0);
System.out.println("\n=== SIMULATION COMPLETE ===");
}
}