我最近开始学习MonoGame和C#,所以为了对两者都更加自信,并做一些不那么简单的事情,我构建了一个Naughts和十字架游戏。我使用7.1版本的C# (这里和那里的值元组.)
游戏内容如下:
整个项目本身可以在github上找到。这是游戏的截图。
我有几个问题。整件事看上去太大,太臃肿了,尽管我确实尽量不重复太多。有什么惯用的方法来缩小它的尺寸吗?
关于评论--我的大部分功能都很短,考虑到我糟糕的语言技巧,大部分代码仍然是不言而喻的。但是,我确实尝试为所有函数提供XML文档--这是否被认为是良好的实践?还是应该只为我预期在将来使用的功能而做?
关于访问级别-什么时候我应该使类公开,以及应该在什么地方将它的可见性保留为默认状态?如果我做得对--公共类将在项目之外可用--然而,将公共类和非公共类混合在一起有时不会编译。
静态函数/类-我应该拥有它们吗?
属性与setters/getter,属性和属性--何时使用?当我让对象公开一个对象时,我尝试将它公开为一个属性--这样做正确吗?
一般来说,我想知道通常情况下,如何在保持功能的同时简化/重组/改进这些代码。
这是代码本身。假设所有类都位于名称空间TicTacToe中,并在每个文件的开头有以下行:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
我的游戏分为以下几类:
TicTacToeGame.cs (取代MonoGame模板中的Game1 --注释是由模板自动生成的)--我保留了它们,因为我现在没有什么可添加的,而且我已经将它主要用作包装类)。
///
/// A class to play tic tac toe
///
public class TicTacToeGame : Game
{
GraphicsDeviceManager graphics;
public SpriteBatch SpriteBatch;
GameManager gameManager;
int windowWidth = 1280;
int windowHeight = 780;
public TicTacToeGame()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferWidth = windowWidth;
graphics.PreferredBackBufferHeight = windowHeight;
}
///
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
///
protected override void Initialize()
{
this.IsMouseVisible = true;
base.Initialize();
}
///
/// LoadContent will be called once per game and is the place to load
/// all of your content.
///
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
SpriteBatch = new SpriteBatch(GraphicsDevice);
gameManager = new GameManager(this);
// TODO: use this.Content to load your game content here
}
///
/// UnloadContent will be called once per game and is the place to unload
/// game-specific content.
///
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
///
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
///
/// Provides a snapshot of timing values.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
if (Keyboard.GetState().IsKeyDown(Keys.F10))
{
if (!graphics.IsFullScreen)
graphics.IsFullScreen = true;
else
graphics.IsFullScreen = false;
graphics.ApplyChanges();
}
gameManager.Update();
base.Update(gameTime);
}
///
/// This is called when the game should draw itself.
///
/// Provides a snapshot of timing values.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
SpriteBatch.Begin();
gameManager.Draw();
SpriteBatch.End();
base.Draw(gameTime);
}
}
GameManager.cs -负责连接游戏部件
///
/// An object repsonsible for controlling the game
///
public class GameManager
{
public TicTacToeGame TheGame { get; private set; }
public GameLogic Logic { get; private set; }
public GameIO IO { get; private set; }
public GameBoard Board { get; private set; }
private IPlayer player1;
private IPlayer player2;
///
/// Allocates memory for object - used to avoid null reference errors
/// while initializing the fields of the the object
///
private GameManager()
{
TheGame = null;
Logic = null;
IO = null;
Board = null;
}
///
/// The game to which the given Manager belongs to.
///
///
public GameManager(TicTacToeGame ticTacToeGame) : this()
{
this.TheGame = ticTacToeGame;
Board = new GameBoard();
Logic = new GameLogic(this);
IO = new GameIO(this);
player1 = new HumanPlayer("Player", CrossesOrNoughts.Crosses, this, new ScoreCalculator(10, 1, -10));
player2 = new ComputerPlayer("Computer", CrossesOrNoughts.Naughts, this, new ScoreCalculator(10, 0, -10));
Logic.AddPlayers(player1, player2);
}
public void Reset()
{
Board = new GameBoard();
Logic = new GameLogic(this);
CrossesOrNoughts tempSymbol = player1.Symbol();
player1.SetSymbol(player2.Symbol());
player2.SetSymbol(tempSymbol);
IPlayer tempPlayer = player1;
player1 = player2;
player2 = tempPlayer;
Logic.AddPlayers(player1, player2);
}
///
/// Update the game state
///
public void Update()
{
IO.Update();
}
///
/// Display the board on the screen.
///
public void Draw()
{
IO.Draw();
}
}
GameIO.cs -负责与游戏的交互-即图形输出和用户输入
///
/// The class repsonsible for the graphics part of the game
///
public class GameIO
{
GameManager gameManager;
TicTacToeGame TheGame => gameManager.TheGame;
GameLogic Logic => gameManager.Logic;
GameBoard Board => gameManager.Board;
Vector2 TopLeft => WindowSize * 0.05f; // top left position of the board
Vector2 SquareSize => WindowSize / 5;
Vector2 BoardSize => SquareSize * 3f;
Vector2 WindowSize => new Vector2(TheGame.GraphicsDevice.Viewport.Width, TheGame.GraphicsDevice.Viewport.Height);
Texture2D background; // background texture
Texture2D tableBorders; // borders between the squares
Texture2D xImage; // Crosses image
Texture2D oImage; // Naughts imaage
Texture2D horizontalLine; // Horizontal line image
Texture2D verticalLine; // vertical line image
Texture2D westEastDiagonal; // an image of diagonal from topleft to buttom right
Texture2D eastWestDiagonal; // an image of diagonal from topright to butttom left
GameMessage gameMessage;
private GameIO() { }
public GameIO(GameManager gameManager)
{
this.gameManager = gameManager;
background = TheGame.Content.Load("Background");
tableBorders = TheGame.Content.Load("TableBorders");
xImage = TheGame.Content.Load("X");
oImage = TheGame.Content.Load("O");
horizontalLine = TheGame.Content.Load("HorizontalLine");
verticalLine = TheGame.Content.Load("VerticalLine");
westEastDiagonal = TheGame.Content.Load("WestEastDiagonal");
eastWestDiagonal = TheGame.Content.Load("EastWestDiagonal");
gameMessage = new GameMessage(gameManager);
}
///
/// Draws a square image on the screen
///
/// Texture name
/// Upper border of the image position
/// Left border of the image
/// Height of the image
/// Widht of the image
void DrawSquare(Texture2D image, float topPosition, float leftPosition, float width, float height)
{
Rectangle destination = new Rectangle((int)topPosition, (int)leftPosition, (int)width, (int)height);
TheGame.SpriteBatch.Draw(image, destination, Color.White);
}
///
/// Draws the back ground of the table
///
void DrawBackground()
{
DrawSquare(background, TopLeft.X, TopLeft.Y, BoardSize.X, BoardSize.Y);
DrawSquare(tableBorders, TopLeft.X, TopLeft.Y, BoardSize.X, BoardSize.Y);
}
///
/// Fills the squares in the game table
///
void DrawSquares()
{
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
{
Texture2D filling;
if (Board[i, j] == CrossesOrNoughts.Crosses)
filling = TheGame.Content.Load("X");
else if (Board[i, j] == CrossesOrNoughts.Naughts)
filling = TheGame.Content.Load("O");
else filling = null;
if (filling != null)
DrawSquare(filling, TopLeft.X + i * SquareSize.X, TopLeft.Y + j * SquareSize.Y,
SquareSize.X, SquareSize.Y);
}
}
///
/// Marks with a line the rows that are all either noughts or crosses
///
void MarkRows()
{
for (int i = 0; i < 3; ++i)
{
if (Board.IsRowTaken(i))
{
DrawSquare(horizontalLine, TopLeft.X, TopLeft.Y + SquareSize.Y * i, BoardSize.X, SquareSize.Y);
}
}
}
///
/// Marks the collumns that are all either noughts or crosses
///
void MarkColumns()
{
for (int i = 0; i < 3; ++i)
{
if (Board.IsColumnTaken(i))
{
DrawSquare(verticalLine, TopLeft.X + SquareSize.X * i, TopLeft.Y, SquareSize.X, BoardSize.Y);
}
}
}
///
/// Marks the main if it contains all noughts or crosses
///
void MarkDiagonals()
{
if (Board.IsMainDiagonalTaken())
DrawSquare(westEastDiagonal, TopLeft.X, TopLeft.Y, BoardSize.X, BoardSize.Y);
if (Board.IsSecondaryDiagonalTaken())
DrawSquare(eastWestDiagonal, TopLeft.X, TopLeft.Y, BoardSize.X, BoardSize.Y);
}
///
/// Draws the game board
///
public void Draw()
{
DrawBackground();
DrawSquares();
MarkRows();
MarkColumns();
MarkDiagonals();
PrintScores();
if (Logic.State == GameLogic.GameState.Over)
{
DeclareWinner();
RestartMessage();
}
}
///
/// Translates 2 dimensional vector to position on the board
///
///
///
public (int row, int column) PositionOnBoard(Vector2 clickPosition)
{
return ((int)((clickPosition.X - TopLeft.X) / SquareSize.X),
(int)((clickPosition.Y - TopLeft.Y) / SquareSize.Y));
}
///
/// Processes mouse input from the user
///
public void ProcessMouseInput()
{
MouseState mouseState = Mouse.GetState();
if (mouseState.LeftButton == ButtonState.Pressed)
{
(int row, int column) = PositionOnBoard(new Vector2(mouseState.X, mouseState.Y));
Logic.Update(row, column);
}
}
///
/// Processes move that was entered as a pair of numbers
///
/// Row number
/// Column number
public void ProcessDigitalInput(int row, int column)
{
Logic.Update(row, column);
}
///
/// Get input from player and update the state of the game
///
public void Update()
{
if (Logic.State == GameLogic.GameState.Continue) Logic.CurrentPlayer.MakeMove();
if (Logic.State == GameLogic.GameState.Over)
{
if (Keyboard.GetState().IsKeyDown(Keys.Space))
gameManager.Reset();
}
}
///
/// Print player scores
///
private void PrintScores()
{
gameMessage.PrintMessageAt(new Vector2(TopLeft.X, TopLeft.Y + BoardSize.Y + 20),
$"{Logic.player1.Name()}: {Logic.player1.Score()}");
gameMessage.PrintMessageAt(new Vector2(TopLeft.X, TopLeft.Y + BoardSize.Y + 70),
$"{Logic.player2.Name()}: {Logic.player2.Score()}");
}
private void DeclareWinner()
{
gameMessage.PrintMessageAt(new Vector2(TopLeft.X, TopLeft.Y + BoardSize.Y + 120),
$"The winner is {Logic.Winner}");
}
private void RestartMessage()
{
gameMessage.PrintMessageAt(new Vector2(TopLeft.X, TopLeft.Y + BoardSize.Y + 170),
"Press space to continue");
}
}
GameLogic.cs -负责处理游戏状态-确定胜利者和处理用户输入。
///
/// A class respsonsible to the logic of the game. Used to determine winner and control the turns.
///
public class GameLogic
{
///
/// The state of the game - whether it is over or not.
///
public enum GameState { Continue, Over};
private GameManager gameManager;
GameBoard board => gameManager.Board;
public IPlayer player1 { get; private set; }
public IPlayer player2 { get; private set; }
public IPlayer CurrentPlayer { get; private set; }
public CrossesOrNoughts Winner { get; private set; }
public GameState State { get; private set; }
///
/// Creates a new game logic object and associates it with the gameManager object
///
/// Game manager object to associate with
public GameLogic(GameManager gameManager)
{
this.gameManager = gameManager;
this.State = GameState.Continue;
player1 = null;
player2 = null;
CurrentPlayer = player1;
}
///
/// Adds a player to the game
///
///
///
public void AddPlayers(IPlayer player1, IPlayer player2)
{
if (this.player1 == null) this.player1 = player1;
if (this.player2 == null) this.player2 = player2;
CurrentPlayer = player1;
}
///
/// Determines result of the game state determined by internal board object
///
/// Whehter the game is over and the winner symbol
private (GameState, CrossesOrNoughts) DetermineResult() => DetermineResult(this.board);
///
/// Calculates the state and the result of the game at the moment represented by the board.
///
///
/// Wheher the game is over and who is the winner in case it i. I
/// f it is not over - Niether is retunred.
public static (GameState state, CrossesOrNoughts winner) DetermineResult(GameBoard board)
{
// go over rows colums and diagonals to che k whether the game is over and we have a winner.
// After that - check if the board is full
for(int i = 0; i < 3; ++i)
{
if (board.IsRowTaken(i))
return (GameState.Over, board[0, i]);
if (board.IsColumnTaken(i))
return (GameState.Over, board[i, 0]);
}
if (board.IsMainDiagonalTaken())
return (GameState.Over, board[0, 0]);
if (board.IsSecondaryDiagonalTaken())
return (GameState.Over, board[2, 0]);
if (board.IsFull())
return (GameState.Over, CrossesOrNoughts.Neither);
return (GameState.Continue, CrossesOrNoughts.Neither);
}
///
/// Change the player
///
void UpdatePlayer()
{
CurrentPlayer = (CurrentPlayer == player1) ? player2 : player1;
}
///
/// Checks whether position is legal or if it is taken and puts appropriate player sign on it if the game is not over.
/// After performing player move, updates the player if it the game is not over.
///
///
///
public void Update(int row, int column)
{
if (board.ShouldUpdate(row, column) && State == GameState.Continue)
{
board[row, column] = CurrentPlayer.Symbol();
(State, Winner) = DetermineResult();
if (State == GameState.Continue) UpdatePlayer();
else
{
player1.UpdateScore(Winner);
player2.UpdateScore(Winner);
}
}
}
///
/// Calculates the symbol used by opponent player
///
/// Returns the symbol used by the opponent
///
public static CrossesOrNoughts OpponentSymbol(CrossesOrNoughts playerSymbol)
{
if (playerSymbol == CrossesOrNoughts.Crosses) return CrossesOrNoughts.Naughts;
if (playerSymbol == CrossesOrNoughts.Naughts) return CrossesOrNoughts.Crosses;
else return CrossesOrNoughts.Neither;
}
}
Board.cs -表示游戏中的棋盘,处理“几何”和“物理”部分--即我们可以在棋盘上放一块,还是有同类型的线条或对角线,等等.
///
/// Board to represent board in Noughts and crosses game
///
public class GameBoard
{
private CrossesOrNoughts[,] entries;
///
/// Checks whether the position is within bounds
///
/// Position row
/// Position column
/// True if position is within bounds, false otherwise
public bool IsValidPosition(int row, int column) => !(row < 0 || row >= 3 || column < 0 || column >= 3);
public bool IsFree(int row, int column) => !(entries[row, column] == CrossesOrNoughts.Neither);
public bool ShouldUpdate(int row, int column) =>
IsValidPosition(row, column) && (entries[row, column] == CrossesOrNoughts.Neither);
///
/// The construtor - accepts no arguments and creates 3 on 3 empty board
///
public GameBoard()
{
entries = new CrossesOrNoughts[3, 3];
}
///
/// Indexer - returns the square at the given position
///
/// Position row
/// Position column
/// Position entry
public CrossesOrNoughts this[int row, int column]
{
get
{
if (IsValidPosition(row, column))
return entries[row, column];
else throw new IndexOutOfRangeException();
}
set
{
if (IsValidPosition(row, column))
entries[row, column] = value;
else throw new IndexOutOfRangeException();
}
}
///
/// Returns whether the entries in the array are same are either noughts or crosses
///
/// The array os crosses or nouhgts
/// True if they are all same or false otherwise
private bool HaveSameSign(params CrossesOrNoughts[] crossesOrNoughts)
{
for (int i = 0; i < crossesOrNoughts.Length - 1; ++i)
{
if (crossesOrNoughts[i] == CrossesOrNoughts.Neither) return false;
if (crossesOrNoughts[i] != crossesOrNoughts[i + 1]) return false;
}
return true;
}
///
/// Returns the entries in the given row.
///
/// Row numbers
/// The row entries in array form
private CrossesOrNoughts[] TableRow(int row)
{
CrossesOrNoughts[] result = new CrossesOrNoughts[entries.GetLength(1)];
for(int i = 0; i < result.Length; ++i)
{
result[i] = entries[i, row];
}
return result;
}
///
/// Returns the entries in the given column
///
/// Column number
/// The column entries in array form
private CrossesOrNoughts[] TableColumn(int column)
{
CrossesOrNoughts[] result = new CrossesOrNoughts[entries.GetLength(0)];
for(int i = 0; i < result.Length; ++i)
{
result[i] = entries[column, i];
}
return result;
}
///
/// Returns the entries in diagonal from top left corner to buttom right corner
///
/// Entries of the main diagonal in array form
private CrossesOrNoughts[] MainDiagonal()
{
CrossesOrNoughts[] result = new CrossesOrNoughts[entries.GetLength(0)];
for (int i = 0; i < result.Length; ++i)
result[i] = entries[i, i];
return result;
}
///
/// Return the entries in the diagonal from buttom left to upper right corner
///
/// The entries of the secondary diagonal in array form
private CrossesOrNoughts[] SecondaryDiagonal()
{
CrossesOrNoughts[] result = new CrossesOrNoughts[entries.GetLength(0)];
for (int i = 0; i < result.Length; ++i)
result[i] = entries[result.Length - 1 - i, i];
return result;
}
///
/// Checks whether the board is full
///
///
public bool IsFull()
{
for(int i = 0; i < 3; ++i)
{
for(int j = 0; j < 3; ++j)
{
if (entries[i, j] == CrossesOrNoughts.Neither) return false;
}
}
return true;
}
///
/// Checks whether the given row is full and contains same signs
///
///
/// True if the column entries are either all noughts or all rosses, false otherwise
public bool IsRowTaken(int row) => HaveSameSign(TableRow(row));
///
/// Checks whether the given column is marked and contains same signs
///
///
/// True if the column entries are either all noughts or all crosses, false otherwise
public bool IsColumnTaken(int column) => HaveSameSign(TableColumn(column));
///
/// Checks whether the main diagonal is marked and contains same signs
///
/// True if all the entries in the main diagonal are either all noughts or all crosses, false otherwise
public bool IsMainDiagonalTaken() => HaveSameSign(MainDiagonal());
///
/// Checks whther the secondary diagonal is marked and contains same signs
///
/// True if all the entries in the main diagonal are either all noughts or all crosses, false otherwise
public bool IsSecondaryDiagonalTaken() => HaveSameSign(SecondaryDiagonal());
}
代表游戏玩家的IPlayer界面
///
/// An interface representing the player in the game.
///
public interface IPlayer
{
CrossesOrNoughts Symbol();
void SetSymbol(CrossesOrNoughts symbol);
void MakeMove();
string Name();
int Score();
void UpdateScore(CrossesOrNoughts crossesOrNoughts);
}
该接口由两个类HumanPlayer和ComputerPlayer实现。
HumanPlayer.cs
///
/// A class to represent human controlled player in the game
///
public class HumanPlayer : IPlayer
{
private string name;
private CrossesOrNoughts symbol;
private GameManager gameManager;
private int score;
private ScoreCalculator scoreCalculator;
///
/// Creats an instance of player
///
/// Player's name
/// What player puts on the board - crosses or nauughts
/// Interface to the game
public HumanPlayer(String name, CrossesOrNoughts symbol, GameManager gameManager, ScoreCalculator scoreCalculator)
{
this.name = name;
this.symbol = symbol;
this.gameManager = gameManager;
this.score = 0;
this.scoreCalculator = scoreCalculator;
}
///
/// make a Move in the game
///
public void MakeMove() => gameManager.IO.ProcessMouseInput();
///
/// The symbol used by player
///
/// Returns whether the player puts crosses or naughts
public CrossesOrNoughts Symbol() => symbol;
///
/// Player's name
///
/// Player's name
public string Name() => name;
///
/// Score of the player
///
/// The score of the player
public int Score() => score;
public void SetSymbol(CrossesOrNoughts symbol)
{
this.symbol = symbol;
}
///
/// Update the player's score
///
/// Current winner of the game
public void UpdateScore(CrossesOrNoughts winner)
{
if (winner == symbol) score += scoreCalculator.WinScore;
if (winner == GameLogic.OpponentSymbol(symbol)) score += scoreCalculator.LoseScore;
else score += scoreCalculator.DrawScore;
}
}
ComputerPlayer.cs
///
/// Represents computer controlled player - designed to always pick the best move
///
public class ComputerPlayer : IPlayer
{
private string name;
private CrossesOrNoughts symbol;
private GameManager gameManager;
private int score;
private ScoreCalculator scoreCalculator;
public ComputerPlayer(string name, CrossesOrNoughts symbol, GameManager gameManager, ScoreCalculator scoreCalculator)
{
this.name = name;
this.symbol = symbol;
this.gameManager = gameManager;
this.score = 0;
this.scoreCalculator = scoreCalculator;
}
///
/// Symbol used by the player - crosses or noughts
///
/// The symbol used by the player
public CrossesOrNoughts Symbol() => symbol;
public string Name() => name;
///
/// Calculates the best possible move and passes it to the IO
///
public void MakeMove()
{
MoveAnalysis move = MoveCalculator.BestMove(symbol, gameManager.Board);
gameManager.IO.ProcessDigitalInput(move.Row, move.Column);
}
///
/// The score of the player
///
///
public int Score() => score;
public void SetSymbol(CrossesOrNoughts symbol)
{
this.symbol = symbol;
}
///
/// Update the player's score
///
/// Current winner of the game
public void UpdateScore(CrossesOrNoughts winner)
{
if (winner == symbol) score += scoreCalculator.WinScore;
if (winner == GameLogic.OpponentSymbol(symbol)) score += scoreCalculator.LoseScore;
else score += scoreCalculator.DrawScore;
}
}
helper类MoveCalculator用于ComputerPlayer,它允许使用helper类MoveAnalysis计算最佳移动,以跟踪移动统计信息。
///
/// Statistaics for the move - position and who's the winner if we make it
///
class MoveAnalysis
{
public int Row { get; set; }
public int Column { get; set; }
public CrossesOrNoughts Winner { get; set; }
public MoveAnalysis(int row, int column, CrossesOrNoughts winner)
{
Row = row;
Column = column;
Winner = winner;
}
}
///
/// Static class used to calculate the optimal move
///
static class MoveCalculator
{
private static Random random = new Random();
///
/// Calculate the result to which leads outting playerSymbol at gven position on a board
/// assuming that both players play optimally.
///
/// Row position
/// Column position
/// The game board
/// Symbol - either naughts or crosses.
///
private static CrossesOrNoughts EvaluateMove(int row, int column, GameBoard board, CrossesOrNoughts playerSymbol)
{
// Sanity check - checks whether the the position is legal
if (playerSymbol == CrossesOrNoughts.Neither || !board.IsValidPosition(row, column))
throw new ArgumentOutOfRangeException("Player can be either Crosses or Naughts.");
if (board[row, column] != CrossesOrNoughts.Neither)
throw new IndexOutOfRangeException("Square already occupied.");
/* Calculates the score recursively.
* We put the current player's sign on the board and check the result.
* If the game is over we return the result of the game.
* Otherwise, we go over all available positions and pick return the best score
* achivable by the opponent
*/
board[row, column] = playerSymbol;
(GameLogic.GameState state, CrossesOrNoughts winner) = GameLogic.DetermineResult(board);
if (state == GameLogic.GameState.Over)
{
board[row, column] = CrossesOrNoughts.Neither;
return winner;
}
CrossesOrNoughts bestOponentResult = playerSymbol;
CrossesOrNoughts opponentSymbol = GameLogic.OpponentSymbol(playerSymbol);
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
if (board[i, j] == CrossesOrNoughts.Neither)
{
CrossesOrNoughts result = EvaluateMove(i, j, board, opponentSymbol);
if (result == opponentSymbol)
bestOponentResult = opponentSymbol;
else if (result == CrossesOrNoughts.Neither && bestOponentResult == playerSymbol)
bestOponentResult = CrossesOrNoughts.Neither;
}
}
}
board[row, column] = CrossesOrNoughts.Neither;
return bestOponentResult;
}
///
/// Calculates the best move that can be maid by the player
///
/// Players symbol - either crosses or noughtss
/// the game board
/// Best move that player can make
public static MoveAnalysis BestMove(CrossesOrNoughts playerSymbol, GameBoard board)
{
// list of possible moves
List moves = new List();
// go over all empty positions and them as possible moves
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
if (board[i, j] == CrossesOrNoughts.Neither)
{
CrossesOrNoughts winner = EvaluateMove(i, j, board, playerSymbol);
moves.Add(new MoveAnalysis(i, j, winner));
}
}
}
// determine the best possible move and result
MoveAnalysis bestMove = moves[0];
for (int i = 1; i < moves.Count; ++i)
{
if (moves[i].Winner == playerSymbol)
{
bestMove = moves[i];
}
else if (moves[i].Winner == CrossesOrNoughts.Neither && bestMove.Winner == GameLogic.OpponentSymbol(playerSymbol))
{
bestMove = moves[i];
}
}
// randomize - make a list of best moves and chose the best one
List bestMoves = new List();
for(int i = 0; i < moves.Count; ++i)
{
if (moves[i].Winner == bestMove.Winner)
bestMoves.Add(moves[i]);
}
return bestMoves[random.Next(bestMoves.Count)];
}
}
///
/// Statistaics for the move - position and who's the winner if we make it
///
class MoveAnalysis
{
public int Row { get; set; }
public int Column { get; set; }
public CrossesOrNoughts Winner { get; set; }
public MoveAnalysis(int row, int column, CrossesOrNoughts winner)
{
Row = row;
Column = column;
Winner = winner;
}
}
///
/// Static class used to calculate the optimal move
///
static class MoveCalculator
{
private static Random random = new Random();
///
/// Calculate the result to which leads outting playerSymbol at gven position on a board
/// assuming that both players play optimally.
///
/// Row position
/// Column position
/// The game board
/// Symbol - either naughts or crosses.
///
private static CrossesOrNoughts EvaluateMove(int row, int column, GameBoard board, CrossesOrNoughts playerSymbol)
{
// Sanity check - checks whether the the position is legal
if (playerSymbol == CrossesOrNoughts.Neither || !board.IsValidPosition(row, column))
throw new ArgumentOutOfRangeException("Player can be either Crosses or Naughts.");
if (board[row, column] != CrossesOrNoughts.Neither)
throw new IndexOutOfRangeException("Square already occupied.");
/* Calculates the score recursively.
* We put the current player's sign on the board and check the result.
* If the game is over we return the result of the game.
* Otherwise, we go over all available positions and pick return the best score
* achivable by the opponent
*/
board[row, column] = playerSymbol;
(GameLogic.GameState state, CrossesOrNoughts winner) = GameLogic.DetermineResult(board);
if (state == GameLogic.GameState.Over)
{
board[row, column] = CrossesOrNoughts.Neither;
return winner;
}
CrossesOrNoughts bestOponentResult = playerSymbol;
CrossesOrNoughts opponentSymbol = GameLogic.OpponentSymbol(playerSymbol);
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
if (board[i, j] == CrossesOrNoughts.Neither)
{
CrossesOrNoughts result = EvaluateMove(i, j, board, opponentSymbol);
if (result == opponentSymbol)
bestOponentResult = opponentSymbol;
else if (result == CrossesOrNoughts.Neither && bestOponentResult == playerSymbol)
bestOponentResult = CrossesOrNoughts.Neither;
}
}
}
board[row, column] = CrossesOrNoughts.Neither;
return bestOponentResult;
}
///
/// Calculates the best move that can be maid by the player
///
/// Players symbol - either crosses or noughtss
/// the game board
/// Best move that player can make
public static MoveAnalysis BestMove(CrossesOrNoughts playerSymbol, GameBoard board)
{
// list of possible moves
List moves = new List();
// go over all empty positions and them as possible moves
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
if (board[i, j] == CrossesOrNoughts.Neither)
{
CrossesOrNoughts winner = EvaluateMove(i, j, board, playerSymbol);
moves.Add(new MoveAnalysis(i, j, winner));
}
}
}
// determine the best possible move and result
MoveAnalysis bestMove = moves[0];
for (int i = 1; i < moves.Count; ++i)
{
if (moves[i].Winner == playerSymbol)
{
bestMove = moves[i];
}
else if (moves[i].Winner == CrossesOrNoughts.Neither && bestMove.Winner == GameLogic.OpponentSymbol(playerSymbol))
{
bestMove = moves[i];
}
}
// randomize - make a list of best moves and chose the best one
List bestMoves = new List();
for(int i = 0; i < moves.Count; ++i)
{
if (moves[i].Winner == bestMove.Winner)
bestMoves.Add(moves[i]);
}
return bestMoves[random.Next(bestMoves.Count)];
}
}
一个用来帮助计算游戏ScoreCalculator.cs中分数的类
///
/// A class to help to determine the score obtained by the player as a result of the game
///
public class ScoreCalculator
{
public int WinScore { get; private set; }
public int DrawScore { get; private set; }
public int LoseScore { get; private set; }
///
/// Constructor
///
/// Score for the win
/// Score for the draw
/// Score for the loss
public ScoreCalculator(int win, int draw, int lose)
{
WinScore = win;
DrawScore = draw;
LoseScore = lose;
}
}
发布于 2018-05-01 06:54:07
https://codereview.stackexchange.com/questions/193211
复制相似问题