Skip to main content
Code Review

Return to Answer

added 743 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with an indexer by row and column and Size.

     interface IBoard
     {
     int Size { get; }
     Player? this [int row, int column] { get; set; }
     }
    

    (really, you don't need anything else in IBoard)

  • The implementation is trivial:

     class Board : IBoard
     {
     Player? [,] _cells;
     public int Size {
     get { return _cells.GetLength (0); }
     }
     public Board (int size)
     {
     _cells = new Player? [size, size];
     }
     public Player? this [int row, int column] {
     get {
     return _cells [row, column];
     } set {
     if (_cells [row, column].HasValue)
     throw new InvalidOperationException ("The cell is already claimed.");
     _cells [row, column] = value;
     }
     }
     }
    
  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board [row, column];
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board [move.Item1, move.Item2] = player;
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with an indexer by row and column and Size.

     interface IBoard
     {
     int Size { get; }
     Player? this [int row, int column] { get; set; }
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board [row, column];
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board [move.Item1, move.Item2] = player;
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with an indexer by row and column and Size.

     interface IBoard
     {
     int Size { get; }
     Player? this [int row, int column] { get; set; }
     }
    

    (really, you don't need anything else in IBoard)

  • The implementation is trivial:

     class Board : IBoard
     {
     Player? [,] _cells;
     public int Size {
     get { return _cells.GetLength (0); }
     }
     public Board (int size)
     {
     _cells = new Player? [size, size];
     }
     public Player? this [int row, int column] {
     get {
     return _cells [row, column];
     } set {
     if (_cells [row, column].HasValue)
     throw new InvalidOperationException ("The cell is already claimed.");
     _cells [row, column] = value;
     }
     }
     }
    
  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board [row, column];
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board [move.Item1, move.Item2] = player;
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
deleted 73 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with an indexer by GetCellrow, and SetCellcolumn and Size.

     interface IBoard
     {
     int Size { get; }
     Player? GetCellthis (int[int row, int column);
     void SetCell (int row,column] int{ column,get; Playerset; player);}
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board.GetCell (row[row, column);column];
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board.GetCell (row[row, column);column];
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board.GetCell (row[row, column);column];
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board.SetCell (move[move.Item1, move.Item2,Item2] player);= player;
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with GetCell, SetCell and Size.

     interface IBoard
     {
     int Size { get; }
     Player? GetCell (int row, int column);
     void SetCell (int row, int column, Player player);
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board.GetCell (row, column);
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board.SetCell (move.Item1, move.Item2, player);
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with an indexer by row and column and Size.

     interface IBoard
     {
     int Size { get; }
     Player? this [int row, int column] { get; set; }
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board [row, column];
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board [row, column];
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board [move.Item1, move.Item2] = player;
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
added 443 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19

I re-wrotere-wrote my earlier toy Tic Tac Toe implementation specifically to address your question, with the following separation:

  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with GetCell, SetCell and Size.

     interface IBoard
     {
     int Size { get; }
     Player? GetCell (int row, int column);
     void SetCell (int row, int column, Player player);
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board.GetCell (row, column);
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board.SetCell (move.Item1, move.Item2, player);
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    

I re-wrote my earlier toy Tic Tac Toe implementation specifically to address your question, with the following separation:

  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with GetCell, SetCell and Size.

     interface IBoard
     {
     int Size { get; }
     Player? GetCell (int row, int column);
     void SetCell (int row, int column, Player player);
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board.GetCell (row, column);
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     Console.WriteLine ("Bad move.");
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board.SetCell (move.Item1, move.Item2, player);
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    

I re-wrote my earlier toy Tic Tac Toe implementation specifically to address your question, with the following separation:

  • Player is a simple enum:

     enum Player {
     X,
     O
     }
    
  • IBoard represents a board with GetCell, SetCell and Size.

     interface IBoard
     {
     int Size { get; }
     Player? GetCell (int row, int column);
     void SetCell (int row, int column, Player player);
     }
    

    (really, you don't need anything else in IBoard)

  • IBoardAnalyzer has a single method that scans the board to find the winner, similarly to yours:

     interface IBoardAnalyzer
     {
     Player? DetermineWinner (IBoard board);
     }
    
  • All the actual heavy-lifting for scanning the board lives in BoardExtensions that can enumerate board rows, columns and diagonals. There is also an über-method called SelectAllLines that returns a sequence of all "lines" (rows, columns and diagonals) on the board. Note that BoardExtensions does no analysis; it only provides convenience methods for extracting data out of Board.

     static class BoardExtensions
     {
     public enum DiagonalKind {
     Primary,
     Secondary
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllLines (this IBoard board)
     {
     return board.SelectAllDiagonals ()
     .Concat (board.SelectAllRows ())
     .Concat (board.SelectAllColumns ());
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllRows (this IBoard board)
     {
     return from row in board.SelectIndices ()
     select board.SelectRow (row);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllColumns (this IBoard board)
     {
     return from column in board.SelectIndices ()
     select board.SelectColumn (column);
     }
     public static IEnumerable<IEnumerable<Player?>> SelectAllDiagonals (this IBoard board)
     {
     return from kind in new [] { DiagonalKind.Primary, DiagonalKind.Secondary }
     select board.SelectDiagonal (kind);
     }
     public static IEnumerable<Player?> SelectRow (this IBoard board, int row)
     {
     return from column in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectColumn (this IBoard board, int column)
     {
     return from row in board.SelectIndices ()
     select board.GetCell (row, column);
     }
     public static IEnumerable<Player?> SelectDiagonal (this IBoard board, DiagonalKind kind)
     {
     return from index in board.SelectIndices ()
     let row = index
     let column = (kind == DiagonalKind.Primary)
     ? index
     : board.Size - 1 - index
     select board.GetCell (row, column);
     }
     public static IEnumerable<int> SelectIndices (this IBoard board)
     {
     return Enumerable.Range (0, board.Size);
     }
     }
    
  • The implementation for BoardAnalyzer uses BoardExtensions to find the winner, but in itself is trivial:

     class BoardAnalyzer : IBoardAnalyzer
     {
     public Player? DetermineWinner (IBoard board)
     {
     return (
     from line in board.SelectAllLines ()
     let winner = DetermineLineWinner (line)
     where winner.HasValue
     select winner
     ).FirstOrDefault ();
     }
     static Player? DetermineLineWinner (IEnumerable<Player?> line)
     {
     try {
     return line.Distinct ().Single ();
     } catch (InvalidOperationException) {
     return null;
     }
     }
     }
    
  • Finally, I implemented an IO which has a simple interface:

     interface IGameIO
     {
     Tuple<int, int> AskNextMove (Player player, IBoard board);
     void DisplayError (GameError error);
     void DisplayWinner (Player player);
     }
    
  • And just as simple implementation:

     class ConsoleGameIO : IGameIO
     {
     public Tuple<int, int> AskNextMove (Player player, IBoard board)
     {
     Console.WriteLine ("\n\n");
     Console.WriteLine ("{0}, what is your move?\n", FormatPlayer (player));
     Console.WriteLine (FormatBoard (board));
     Console.Write ("\nType A1 to C3: ", FormatPlayer (player));
     return ParseMove (Console.ReadLine ().Trim ().ToUpperInvariant ());
     }
     public void DisplayError (GameError error)
     {
     switch (error) {
     case GameError.CellAlreadyOccupied:
     Console.WriteLine ("Bad move: cell is already occupied.");
     break;
     case GameError.CouldNotParseMove:
     Console.WriteLine ("Bad move. Valid moves are A1 to C3.");
     break;
     default:
     Console.WriteLine ("Something went wrong.");
     break;
     }
     }
     public void DisplayWinner (Player player)
     {
     Console.WriteLine ("Congatulations, {0}! You won.", FormatPlayer (player));
     }
     static string FormatBoard (IBoard board)
     {
     return string.Join ("\n", from row in board.SelectAllRows () select FormatRow (row));
     }
     static string FormatRow (IEnumerable<Player?> row)
     {
     return string.Join ("|", from cell in row select FormatCell (cell));
     }
     static string FormatPlayer (Player player)
     {
     return FormatCell (player);
     }
     static string FormatCell (Player? cell)
     {
     return cell.HasValue ? cell.Value.ToString () : "_";
     }
     static Tuple<int, int> ParseMove (string input)
     {
     return Tuple.Create (
     input [0] - 'A',
     input [1] - '1'
     );
     }
     }
    
  • Finally, there goes the Game class with the run loop and Main method:

     class Game
     {
     IBoard board;
     IBoardAnalyzer analyzer;
     IGameIO io;
     public Game (IBoard board, IBoardAnalyzer analyzer, IGameIO io)
     {
     this.board = board;
     this.analyzer = analyzer;
     this.io = io;
     }
     public void Run ()
     {
     Player player = Player.X;
     Player? winner = null;
     do {
     Tuple<int, int> move;
     try {
     move = io.AskNextMove (player, board);
     } catch {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     if (!ValidateMove (move)) {
     io.DisplayError (GameError.CouldNotParseMove);
     continue;
     }
     try {
     board.SetCell (move.Item1, move.Item2, player);
     } catch (InvalidOperationException) {
     io.DisplayError (GameError.CellAlreadyOccupied);
     continue;
     }
     player = GetNextPlayer (player);
     winner = analyzer.DetermineWinner (board);
     } while (!winner.HasValue);
     io.DisplayWinner (winner.Value);
     }
     bool ValidateMove (Tuple<int, int> move)
     {
     return (move.Item1 >= 0 && move.Item1 < board.Size)
     && (move.Item2 >= 0 && move.Item2 < board.Size);
     }
     static Player GetNextPlayer (Player player)
     {
     switch (player) {
     case Player.X:
     return Player.O;
     case Player.O:
     return Player.X;
     default:
     throw new NotImplementedException ();
     }
     }
     public static void Main (string[] args)
     {
     var game = new Game (new Board (3), new BoardAnalyzer (), new ConsoleGameIO ());
     game.Run ();
     }
     }
    
added 22 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
Loading
added 22 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
Loading
added 22 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
Loading
added 4988 characters in body
Source Link
Dan
  • 1.5k
  • 9
  • 19
Loading
Source Link
Dan
  • 1.5k
  • 9
  • 19
Loading
lang-cs

AltStyle によって変換されたページ (->オリジナル) /