From 95fa8e7da37ed5a9fc21b848732decdf32232a6d Mon Sep 17 00:00:00 2001 From: pdagosta <8269326+pdagosta@users.noreply.github.com> Date: Sat, 5 Mar 2022 11:07:45 -0600 Subject: [PATCH] working version --- 23_Checkers/csharp/Program.cs | 587 ++++++++++++++++++++++------------ 1 file changed, 379 insertions(+), 208 deletions(-) diff --git a/23_Checkers/csharp/Program.cs b/23_Checkers/csharp/Program.cs index b2118267..75383c99 100644 --- a/23_Checkers/csharp/Program.cs +++ b/23_Checkers/csharp/Program.cs @@ -1,33 +1,28 @@ -const int LineLength = 80; -Dictionary Pieces = new Dictionary() -{ - { -2, "X*" }, - { -1, "X " }, - { 0, ". " }, - { 1, "O " }, - { 2, "O*" }, -}; - -void PrintBoard(int[,] state) -{ - SkipLines(3); - for (int y = 7; y >= 0; y--) - { - for (int x = 0; x < 8; x++) - { - Console.Write(Pieces[state[x, y]]); - Console.Write(" "); - } - Console.WriteLine(); - } -} - -void WriteCenter(string text) -{ - var spaces = (LineLength - text.Length) / 2; - Console.WriteLine($"{"".PadLeft(spaces)}{text}"); -} - +/********************************************************************************* + * CHECKERS + * ported from BASIC https://www.atariarchives.org/basicgames/showpage.php?page=41 + * + * Porting philosophy + * 1) Adhere to the original as much as possible + * 2) Attempt to be understandable by Novice progammers + * + * There are no classes or Object Oriented design patterns used in this implementation. + * Everything is written procedurally, using only top-level functions. Hopefully, this + * will be approachable for someone who wants to learn C# syntax without experience with + * Object Oriented concepts. Similarly, basic data structures have been chosen over more + * powerful collection types. Linq/lambda syntax is also excluded. + * + * C# Concepts contained in this example: + * Loops (for, foreach, while, and do) + * Multidimensional arrays + * Tuples + * Nullables + * IEnumerable (yield return / yield break) + * + * The original had multiple implementations of logic, like determining second jump locations. + * This has been refactored to reduce unnecessary code duplication. + *********************************************************************************/ +#region Display functions void SkipLines(int count) { for (int i = 0; i < count; i++) @@ -36,6 +31,72 @@ void SkipLines(int count) } } +void PrintBoard(int[,] state) +{ + SkipLines(3); + for (int y = 7; y >= 0; y--) + { + for (int x = 0; x < 8; x++) + { + switch(state[x,y]) + { + case -2: + Console.Write("X*"); + break; + case -1: + Console.Write("X "); + break; + case 0: + Console.Write(". "); + break; + case 1: + Console.Write("O "); + break; + case 2: + Console.Write("O*"); + break; + } + Console.Write(" "); + } + Console.WriteLine(); + } +} + +void WriteCenter(string text) +{ + const int LineLength = 80; + var spaces = (LineLength - text.Length) / 2; + Console.WriteLine($"{"".PadLeft(spaces)}{text}"); +} + +void ComputerWins() +{ + Console.WriteLine("I WIN."); +} +void PlayerWins() +{ + Console.WriteLine("YOU WIN."); +} + +void WriteIntroduction() +{ + WriteCenter("CHECKERS"); + WriteCenter("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + SkipLines(3); + Console.WriteLine("THIS IS THE GAME OF CHECKERS. THE COMPUTER IS X,"); + Console.WriteLine("AND YOU ARE O. THE COMPUTER WILL MOVE FIRST."); + Console.WriteLine("SQUARES ARE REFERRED TO BY A COORDINATE SYSTEM."); + Console.WriteLine("(0,0) IS THE LOWER LEFT CORNER"); + Console.WriteLine("(0,7) IS THE UPPER LEFT CORNER"); + Console.WriteLine("(7,0) IS THE LOWER RIGHT CORNER"); + Console.WriteLine("(7,7) IS THE UPPER RIGHT CORNER"); + Console.WriteLine("THE COMPUTER WILL TYPE '+TO' WHEN YOU HAVE ANOTHER"); + Console.WriteLine("JUMP. TYPE TWO NEGATIVE NUMBERS IF YOU CANNOT JUMP."); + SkipLines(3); +} +#endregion + +#region State validation functions bool IsPointOutOfBounds(int x) { return x < 0 || x > 7; @@ -46,15 +107,152 @@ bool IsOutOfBounds((int x, int y) position) return IsPointOutOfBounds(position.x) || IsPointOutOfBounds(position.y); } +bool IsJumpMove((int x, int y) from, (int x, int y) to) +{ + return Math.Abs(from.y - to.y) == 2; +} + +bool IsValidPlayerMove(int[,] state, (int x, int y) from, (int x, int y) to) +{ + if (state[to.x, to.y] != 0) + { + return false; + } + var deltaX = Math.Abs(to.x - from.x); + var deltaY = Math.Abs(to.y - from.y); + if (deltaX != 1 && deltaX != 2) + { + return false; + } + if (deltaX != deltaY) + { + return false; + } + if (state[from.x, from.y] == 1 && Math.Sign(to.y - from.y) <= 0) + { + // only kings can move downwards + return false; + } + if (deltaX == 2) + { + var jump = GetJumpedPiece(from, to); + if (state[jump.x, jump.y] >= 0) + { + // no valid piece to jump + return false; + } + } + return true; +} + +bool CheckForComputerWin(int[,] state) +{ + bool playerAlive = false; + foreach (var piece in state) + { + if (piece > 0) + { + playerAlive = true; + break; + } + } + return !playerAlive; +} + +bool CheckForPlayerWin(int[,] state) +{ + bool computerAlive = false; + foreach (var piece in state) + { + if (piece < 0) + { + computerAlive = true; + break; + } + } + return !computerAlive; +} +#endregion + +#region Board "arithmetic" +/// +/// Get the Coordinates of a jumped piece +/// +(int x, int y) GetJumpedPiece((int x, int y) from, (int x, int y) to) +{ + var midX = (to.x + from.x) / 2; + var midY = (to.y + from.y) / 2; + return (midX, midY); +} +/// +/// Apply a directional vector "direction" to location "from" +/// return resulting location +/// direction will contain: (-1,-1), (-1, 1), ( 1,-1), ( 1, 1) +/// /// +(int x, int y) GetLocation((int x , int y) from, (int x, int y) direction) +{ + return (x: from.x + direction.x, y: from.y + direction.y); +} +#endregion + +#region State change functions +/// +/// Alter current "state" by moving a piece from "from" to "to" +/// This method does not verify that the move being made is valid +/// This method works for both player moves and computer moves +/// +int[,] ApplyMove(int[,] state, (int x, int y) from, (int x, int y) to) +{ + state[to.x, to.y] = state[from.x, from.y]; + state[from.x, from.y] = 0; + + if (IsJumpMove(from, to)) + { + // a jump was made + // remove the jumped piece from the board + var jump = GetJumpedPiece(from, to); + state[jump.x, jump.y] = 0; + } + return state; +} +/// +/// At the end of a turn (either player or computer) check to see if any pieces +/// reached the final row. If so, change them to kings (crown) +/// +int[,] CrownKingPieces(int[,] state) +{ + for (int x = 0; x < 8; x++) + { + // check the bottom row if computer has a piece in it + if (state[x, 0] == -1) + { + state[x, 0] = -2; + } + // check the top row if the player has a piece in it + if (state[x, 7] == 1) + { + state[x, 7] = 2; + } + } + return state; +} +#endregion + +#region Computer Logic +/// +/// Given a current location "from", determine if a move exists in a given vector, "direction" +/// direction will contain: (-1,-1), (-1, 1), ( 1,-1), ( 1, 1) +/// return "null" if no move is possible in this direction +/// (int x, int y)? GetCandidateMove(int[,] state, (int x, int y) from, (int x, int y) direction) { - var to = (x: from.x + direction.x, y: from.y + direction.y); + var to = GetLocation(from, direction); if (IsOutOfBounds(to)) return null; if (state[to.x, to.y] > 0) { // potential jump - to = (x: to.x + direction.x, y: to.y + direction.y); + to = GetLocation(to, direction); if (IsOutOfBounds(to)) return null; } @@ -64,12 +262,11 @@ bool IsOutOfBounds((int x, int y) position) return to; } -bool IsJumpMove((int x, int y) from, (int x, int y) to) -{ - return Math.Abs(from.y - to.y) == 2; -} - -int AnalyzeMove(int[,] state, (int x, int y) from, (int x, int y) to) +/// +/// Calculate a rank for a given potential move +/// The higher the rank value, the better the move is considered to be +/// +int RankMove(int[,] state, (int x, int y) from, (int x, int y) to) { int rank = 0; @@ -90,12 +287,13 @@ int AnalyzeMove(int[,] state, (int x, int y) from, (int x, int y) to) } if (to.x == 0 || to.x == 7) { - // move to edge + // move to edge of board rank += 1; } - for (int c = -1; c <=1; c++) + // look to the row in front of the potential destination for + for (int c = -1; c <=1; c+=2) { - var inFront = (x: to.x + c, y: to.y - 1); + var inFront = GetLocation(to, (c, -1)); if (IsOutOfBounds(inFront)) continue; if (state[inFront.x, inFront.y] < 0) @@ -104,13 +302,13 @@ int AnalyzeMove(int[,] state, (int x, int y) from, (int x, int y) to) rank++; continue; } - var inBack = (x: to.x - c, y: to.y + 1); + var inBack = GetLocation(to, (-c, 1)); if (IsOutOfBounds(inBack)) { continue; } - if (inBack == from || - (state[inFront.x, inFront.y] > 0 && state[inBack.x, inBack.y] == 0)) + if ((state[inFront.x, inFront.y] > 0) && + (state[inBack.x, inBack.y] == 0) || (inBack == from)) { // the player can jump us rank -= 2; @@ -119,6 +317,10 @@ int AnalyzeMove(int[,] state, (int x, int y) from, (int x, int y) to) return rank; }; +/// +/// Returns an enumeration of possible moves that can be made by the given piece "from" +/// If no moves, can be made, the enumeration will be empty +/// IEnumerable<(int x, int y)> GetPossibleMoves(int[,] state, (int x, int y) from) { int maxB; @@ -156,7 +358,10 @@ IEnumerable<(int x, int y)> GetPossibleMoves(int[,] state, (int x, int y) from) } } } - +/// +/// Determine the best move from a list of candidate moves "possibleMoves" +/// Returns "null" if no move can be made +/// ((int x, int y) from, (int x, int y) to)? GetBestMove(int[,] state, IEnumerable<((int x, int y) from, (int x, int y) to)> possibleMoves) { int? bestRank = null; @@ -164,9 +369,9 @@ IEnumerable<(int x, int y)> GetPossibleMoves(int[,] state, (int x, int y) from) foreach (var move in possibleMoves) { - int rank = AnalyzeMove(state, move.from, move.to); + int rank = RankMove(state, move.from, move.to); - if (rank > bestRank) + if (bestRank == null || rank > bestRank) { bestRank = rank; bestMove = move; @@ -176,6 +381,11 @@ IEnumerable<(int x, int y)> GetPossibleMoves(int[,] state, (int x, int y) from) return bestMove; } +/// +/// Examine the entire board and record all possible moves +/// Return the best move found, if one exists +/// Returns "null" if no move found +/// ((int x, int y) from, (int x, int y) to)? CalculateMove(int[,] state) { var possibleMoves = new List<((int x, int y) from, (int x, int y) to)>(); @@ -193,118 +403,86 @@ IEnumerable<(int x, int y)> GetPossibleMoves(int[,] state, (int x, int y) from) var bestMove = GetBestMove(state, possibleMoves); return bestMove; } -(int x, int y) GetJumpedPiece((int x, int y) from, (int x, int y) to) -{ - var midX = (to.x + from.x) / 2; - var midY = (to.y + from.y) / 2; - return (midX, midY); -} -int[,] ApplyMove(int[,] state, (int x, int y) from, (int x, int y) to) -{ - state[to.x, to.y] = state[from.x, from.y]; - state[from.x, from.y] = 0; - if ( (to.y == 0 && state[to.x, to.y] == -1) - ||(to.y == 7 && state[to.x, to.y] == 1)) - { - // make the piece a king - state[to.x, to.y] *= 2; - } - - if (IsJumpMove(from, to)) - { - // a jump was made - // remove the jumped piece from the board - var jump = GetJumpedPiece(from, to); - state[jump.x, jump.y] = 0; - } - return state; -} +/// +/// The logic behind the Computer's turn +/// Look for valid moves and possible subsequent moves +/// (bool moveMade, int[,] state) ComputerTurn(int[,] state) { + // Get best move available var move = CalculateMove(state); if (move == null) { // No move can be made return (false, state); } - Console.Write($"FROM {move.Value.from.x} {move.Value.from.y} "); + var from = move.Value.from; + Console.Write($"FROM {from.x} {from.y} "); + // Continue to make moves until no more valid moves can be made while (move != null) { - Console.WriteLine($"TO {move.Value.to.x} {move.Value.to.y}"); - state = ApplyMove(state, move.Value.from, move.Value.to); - if (IsJumpMove(move.Value.from, move.Value.to)) + var to = move.Value.to; + Console.WriteLine($"TO {to.x} {to.y}"); + state = ApplyMove(state, from, to); + if (!IsJumpMove(from, to)) + break; + + // check for double / triple / etc. jump + var possibleMoves = new List<((int x, int y) from, (int x, int y) to)>(); + from = to; + foreach (var candidate in GetPossibleMoves(state, from)) { - // check for double / triple / etc. jump - var possibleMoves = new List<((int x, int y) from, (int x, int y) to)>(); - var from = move.Value.to; - foreach (var to in GetPossibleMoves(state, from)) + if (IsJumpMove(from, candidate)) { - if (IsJumpMove(from, to)) - { - possibleMoves.Add((from, to)); - } + possibleMoves.Add((from, candidate)); } - move = GetBestMove(state, possibleMoves); } + // Get best jump move + move = GetBestMove(state, possibleMoves); } + // apply crowns to any new Kings + state = CrownKingPieces(state); return (true, state); } +#endregion +#region Player Logic +/// +/// Get input from the player in the form "x,y" where x and y are integers +/// If invalid input is received, return null +/// If input is valid, return the coordinate of the location +/// (int x, int y)? GetCoordinate(string prompt) { Console.Write(prompt + "? "); var input = Console.ReadLine(); - var parts = input.Split(","); - if (parts.Length != 2) + // split the string into multiple parts + var parts = input?.Split(","); + if (parts?.Length != 2) + // must be exactly 2 parts return null; int x; if (!int.TryParse(parts[0], out x)) + // first part is not a number return null; int y; if (!int.TryParse(parts[1], out y)) + //second part is not a number return null; return (x, y); } -bool IsValidMove(int[,] state, (int x, int y) from, (int x, int y) to) -{ - if (state[to.x, to.y] != 0) - { - return false; - } - var deltaX = Math.Abs(to.x - from.x); - var deltaY = Math.Abs(to.y - from.y); - if (deltaX != 1 || deltaX != 2) - { - return false; - } - if (deltaX != deltaY) - { - return false; - } - if (state[from.x, from.y] == 1 && Math.Sign(to.y - from.y) <= 0) - { - // only kings can move downwards - return false; - } - if (deltaX == 2) - { - var jump = GetJumpedPiece(from, to); - if (state[jump.x, jump.y] >= 0) - { - // no valid piece to jump - return false; - } - } - return true; -} - -int [,] PlayerTurn(int[,] state) +/// +/// Get the move from the player. +/// return a tuple of "from" and "to" representing a valid move +/// +/// +((int x, int y) from, (int x,int y) to) GetPlayerMove(int[,] state) { // The original program has some issues regarding user input - // 1) There is minimal data sanity checks + // 1) There are minimal data sanity checks in the original: // a) FROM piece must be owned by player // b) TO location must be empty // c) the FROM and TO x's must be less than 2 squares away @@ -313,119 +491,111 @@ int [,] PlayerTurn(int[,] state) // if the piece even moves. // 2) Once a valid FROM is selected, a TO must be selected. // If there are no valid TO locations, you are soft-locked - // This approach is intentionally different + // This approach is intentionally different from the original + // but maintains the original intent as much as possible // 1) Select a FROM location // 2) If FROM is invalid, return to step 1 // 3) Select a TO location // 4) If TO is invalid or the implied move is invalid, // return to step 1 - (int x, int y)? from = null; - (int x, int y)? to = null; - var valid = false; + + // There is still currently no way for the player to indicate that no move can be made + // This matches the original logic, but is a candidate for a refactor + do { - from = GetCoordinate("FROM"); + var from = GetCoordinate("FROM"); if ((from != null) && !IsOutOfBounds(from.Value) && (state[from.Value.x, from.Value.y] > 0)) { - to = GetCoordinate("TO"); + // we have a valid "from" location + var to = GetCoordinate("TO"); if ((to != null) && !IsOutOfBounds(to.Value) - && IsValidMove(state, from.Value, to.Value)) + && IsValidPlayerMove(state, from.Value, to.Value)) { - valid = true; + // we have a valid "to" location + return (from.Value, to.Value); } } - } while (!valid); - bool jumping = false; + } while (true); +} + +/// +/// Get a subsequent jump from the player if they can / want to +/// returns a move ("from", "to") if a player jumps +/// returns null if a player does not make another move +/// The player must input negative numbers for the coordinates to indicate +/// that no more moves are to be made. This matches the original implementation +/// +((int x, int y) from, (int x, int y) to)? GetPlayerSubsequentJump(int[,] state, (int x, int y) from) +{ do { - state = ApplyMove(state, from.Value, to.Value); - jumping = IsJumpMove(from.Value, to.Value); - if (jumping) + var to = GetCoordinate("+TO"); + if ((to != null) + && !IsOutOfBounds(to.Value) + && IsValidPlayerMove(state, from, to.Value) + && IsJumpMove(from, to.Value)) { - from = to; - valid = false; - do - { - to = GetCoordinate("+TO"); - if ((to != null) - && !IsOutOfBounds(to.Value) - && IsValidMove(state, from.Value, to.Value) - && IsJumpMove(from.Value, to.Value)) - { - valid = true; - } + // we have a valid "to" location + return (from, to.Value); ; + } - if (to != null && to.Value.x < 0 && to.Value.y < 0) - { - jumping = false; - break; - } - } - while (!valid); + if (to != null && to.Value.x < 0 && to.Value.y < 0) + { + // player has indicated to not make any more moves + return null; } } - while (jumping); + while (true); +} + +/// +/// The logic behind the Player's turn +/// Get the player input for a move +/// Get subsequent jumps, if possible +/// +int [,] PlayerTurn(int[,] state) +{ + var move = GetPlayerMove(state); + do + { + state = ApplyMove(state, move.from, move.to); + if (!IsJumpMove(move.from, move.to)) + { + // If player doesn't make a jump move, no further moves are possible + break; + } + var nextMove = GetPlayerSubsequentJump(state, move.to); + if (nextMove == null) + { + // another jump is not made + break; + } + move = nextMove.Value; + } + while (true); + // check to see if any kings need crowning + state = CrownKingPieces(state); return state; } +#endregion -bool CheckForComputerWin(int[,] state) -{ - bool playerAlive = false; - foreach (var piece in state) - { - if (piece > 0) - { - playerAlive = true; - break; - } - } - return !playerAlive; -} -bool CheckForPlayerWin(int[,] state) -{ - bool computerAlive = false; - foreach (var piece in state) - { - if (piece < 0) - { - computerAlive = true; - break; - } - } - return !computerAlive; -} +/***************************************************************************** + * + * Main program starts here + * + ****************************************************************************/ -void ComputerWins() -{ - Console.WriteLine("I WIN."); -} -void PlayerWins() -{ - Console.WriteLine("YOU WIN."); -} - -// Main program starts here - -WriteCenter("CHECKERS"); -WriteCenter("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); -SkipLines(3); -Console.WriteLine("THIS IS THE GAME OF CHECKERS. THE COMPUTER IS X,"); -Console.WriteLine("AND YOU ARE O. THE COMPUTER WILL MOVE FIRST."); -Console.WriteLine("SQUARES ARE REFERRED TO BY A COORDINATE SYSTEM."); -Console.WriteLine("(0,0) IS THE LOWER LEFT CORNER"); -Console.WriteLine("(0,7) IS THE UPPER LEFT CORNER"); -Console.WriteLine("(7,0) IS THE LOWER RIGHT CORNER"); -Console.WriteLine("(7,7) IS THE UPPER RIGHT CORNER"); -Console.WriteLine("THE COMPUTER WILL TYPE '+TO' WHEN YOU HAVE ANOTHER"); -Console.WriteLine("JUMP. TYPE TWO NEGATIVE NUMBERS IF YOU CANNOT JUMP."); -SkipLines(3); +WriteIntroduction(); // initalize state - empty spots initialize to 0 // set player pieces to 1, computer pieces to -1 +// turn your head to the right to visualize the board. +// kings will be represented by -2 (for computer) and 2 (for player) int[,] state = new int[8, 8] { { 1, 0, 1, 0, 0, 0,-1, 0 }, { 0, 1, 0, 0, 0,-1, 0,-1 }, @@ -443,9 +613,10 @@ while (true) (moveMade, state) = ComputerTurn(state); if (!moveMade) { - // in the original program the computer wins if it cannot make a move - // I believe the player should win in this case, assuming the player can make a move + // In the original program the computer wins if it cannot make a move + // I believe the player should win in this case, assuming the player can make a move. // if neither player can make a move, the game should be draw. + // I have left it as the original logic for now. ComputerWins(); break; }