井字棋项目开发:从功能实现到测试与集成
1. 项目起步与初始功能实现
在项目开始阶段,我们创建了
ConsoleGameView
类,其代码如下:
private TicTacToeGamePresenter _presenter;
public ConsoleGameView()
{
_presenter =
new TicTacToeGamePresenter(this, new TicTacToe(),
new PlainTextGameBoardRenderer());
_presenter.start();
}
public void write_line(string message)
{
System.Console.WriteLine(message);
}
public void get_coordinates_for_next_move()
{
System.Console.ReadLine();
}
public void write(string message)
{
System.Console.Write(message);
}
构建并运行测试用例,它们应该都能通过。此时,虽然已经能显示网格,但场景仍会失败,原因是缺少提示下一个玩家移动的信息。
2. 显示下一个玩家
显示下一个玩家移动的功能由游戏类负责。目前,
TicTacToe
实例中硬编码为总是返回令牌
X
。为了更好地进行测试,我们创建了一个抽象基类
with_a_tictactoe_game
:
using Wrox.BDD.Domain;
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
public abstract class with_a_tictactoe_game
{
public with_a_tictactoe_game()
{
SUT = new TicTacToe();
}
protected static Game SUT;
}
}
接着,在
Domain_Specs
文件夹下创建了一个新的规范类
when_placing_a_token_on_the_board
:
using Machine.Specifications;
using Rhino.Mocks;
using Wrox.BDD.Domain;
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
[Subject(typeof(TicTacToe))]
public class when_placing_a_token_on_the_board : with_a_tictactoe_game
{
Establish context = () =>
{
coordinate_text = “1,1”;
coordinate = Coordinate.parse(coordinate_text);
};
private Because of = () =>
{
SUT.place_token_for_current_player_at(coordinate);
};
private It should_alternate_the_player = () =>
{
player_tracker.AssertWasCalled(x => x.finish_players_move());
};
private static Coordinate coordinate;
private static string coordinate_text;
}
}
这个规范期望在玩家移动后调用
player_tracker
实例。
player_tracker
的职责是记录当前轮到谁,并在
finish_players_move
方法被调用后切换玩家。我们更新了
with_a_tictactoe_game
基类,将
player_tracker
作为构造函数的依赖项:
using Wrox.BDD.Domain;
using Rhino.Mocks;
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
public abstract class with_a_tictactoe_game
{
public with_a_tictactoe_game()
{
player_tracker = MockRepository.GenerateStub<PlayerTracker>();
SUT = new TicTacToe(player_tracker);
}
protected static Game SUT;
protected static PlayerTracker player_tracker;
}
}
同时,创建了
PlayerTracker
接口:
namespace Wrox.BDD.Domain
{
public interface PlayerTracker
{
Token current_player();
void finish_players_move();
}
}
修改
TicTacToe
类,使其接受
PlayerTracker
实例,并将
current_token
方法委托给
PlayerTracker
:
namespace Wrox.BDD.Domain
{
public class TicTacToe : Game
{
private PlayerTracker _player_tracker;
public TicTacToe(PlayerTracker player_tracker)
{
_player_tracker = player_tracker;
}
public Token current_token()
{
return _player_tracker.current_player();
}
public void place_token_for_current_player_at(Coordinate coordinate)
{
_player_tracker.finish_players_move();
}
}
}
创建
TokenTracker
类来实现
PlayerTracker
接口:
namespace Wrox.BDD.Domain
{
public class TokenTracker : PlayerTracker
{
private Token _current_playing_token = Tokens.x_token;
public Token current_player()
{
return _current_playing_token;
}
public void finish_players_move()
{
_current_playing_token =
(_current_playing_token.Equals(Tokens.x_token))
? Tokens.o_token :
Tokens.x_token;
}
}
}
最后,更新
FakeGameView
和
ConsoleGameView
,为
TicTacToe
实例提供
TokenTracker
:
namespace Wrox.BDD.Specs.UAT
{
public class FakeGameView : GameView
{
private TicTacToeGamePresenter _presenter;
private StringBuilder _display = new StringBuilder();
public FakeGameView()
{
_presenter = new TicTacToeGamePresenter(this,
new TicTacToe(new TokenTracker()),
new PlainTextGameBoardRenderer());
GameStorage.presenter = _presenter;
_presenter.start();
}
}
}
namespace Wrox.BDD.Ui.Console
{
public class ConsoleGameView : GameView
{
private TicTacToeGamePresenter _presenter;
public ConsoleGameView()
{
_presenter =
new TicTacToeGamePresenter(this, new TicTacToe(new
TokenTracker()),
new PlainTextGameBoardRenderer());
_presenter.start();
}
}
}
再次构建并运行所有测试用例,如果都通过,说明我们完成了这一阶段的功能开发。
3. 集成交替玩家场景
要集成交替玩家场景,只需更新
ConsoleGameView
类的
get_coordinates_for_next_move
方法,将用户输入传递给游戏:
namespace Wrox.BDD.Ui.Console
{
public class ConsoleGameView : GameView
{
public void get_coordinates_for_next_move()
{
var coordinates = System.Console.ReadLine();
_presenter.update_game_with_move(coordinates);
}
}
}
现在可以运行代码并输入坐标,但需要注意输入的语法,因为还没有处理异常情况。
4. 显示游戏场景
接下来要处理的场景是在一系列移动后显示游戏。需要在
GamePlay.feature
文件中添加新的场景,并在
GamePlaySteps
类中添加新的步骤定义:
namespace Wrox.BDD.Specs.UAT.Steps
{
[Binding]
public class GamePlaySteps
{
[Given(@”the following moves are played:”)]
public void GivenTheFollowingMovesArePlayed(Table table)
{
foreach (var row in table.Rows)
{
GameStorage.presenter.update_game_with_move(
string.Format(“{0},{1}”, row[“row”], row[“column”]));
}
GameStorage.game_view.clean_display();
}
}
}
为了支持游戏显示,需要添加以下几个行为:
1. 游戏应该将令牌放置在网格的方块上。
2. 游戏应该能够确认令牌的放置。
3. 游戏渲染器应该能够获取网格的只读视图进行渲染。
下面是这些行为的具体实现步骤:
4.1 放置令牌在网格上
更新
when_placing_a_token_on_the_board
规范:
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
[Subject(typeof(TicTacToe))]
public class when_placing_a_token_on_the_board : with_a_tictactoe_game
{
Establish context = () =>
{
coordinate_text = “1,1”;
coordinate = Coordinate.parse(coordinate_text);
player_tracker.Stub(x => x.current_player()).Return(Tokens.x_token);
};
private Because of = () =>
{
SUT.place_token_for_current_player_at(coordinate);
};
private It should_place_a_token_on_the_grid = () =>
{
tic_tac_toe_grid.AssertWasCalled(x => x.place_token_at(
Arg<Coordinate>.Matches(c => c.Equals(coordinate)),
Arg<Token>.Matches(c => c.Equals(Tokens.x_token))));
};
private static Coordinate coordinate;
private static string coordinate_text;
}
}
修改
with_a_tictactoe_game
基类,将
tic_tac_toe_grid
作为构造函数的依赖项:
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
public abstract class with_a_tictactoe_game
{
public with_a_tictactoe_game()
{
player_tracker = MockRepository.GenerateStub<PlayerTracker>();
tic_tac_toe_grid = MockRepository.GenerateStub<Grid>();
SUT = new TicTacToe(player_tracker, tic_tac_toe_grid);
}
protected static Game SUT;
protected static PlayerTracker player_tracker;
protected static Grid tic_tac_toe_grid;
}
}
创建
Grid
接口:
namespace Wrox.BDD.Domain
{
public interface Grid
{
void place_token_at(Coordinate coordinate, Token token);
}
}
更新
TicTacToe
类,添加
Grid
作为构造函数的参数,并实现放置令牌的行为:
namespace Wrox.BDD.Domain
{
public class TicTacToe : Game
{
private PlayerTracker _player_tracker;
private Grid _grid;
public TicTacToe(PlayerTracker player_tracker, Grid grid)
{
_player_tracker = player_tracker;
_grid = grid;
}
public Token current_token()
{
return _player_tracker.current_player();
}
public void place_token_for_current_player_at(Coordinate coordinate)
{
_grid.place_token_at(coordinate, current_token());
_player_tracker.finish_players_move();
}
}
}
创建
NineSquareGrid
类来实现
Grid
接口:
namespace Wrox.BDD.Domain
{
public class NineSquareGrid : Grid
{
public void place_token_at(Coordinate coordinate, Token token)
{
}
}
}
更新
FakeGameView
和
ConsoleGameView
,为
TicTacToe
实例提供
NineSquareGrid
:
namespace Wrox.BDD.Specs.UAT
{
public class FakeGameView : GameView
{
private TicTacToeGamePresenter _presenter;
private StringBuilder _display = new StringBuilder();
public FakeGameView()
{
_presenter = new TicTacToeGamePresenter(this,
new TicTacToe(new TokenTracker(),
new NineSquareGrid()),
new PlainTextGameBoardRenderer());
GameStorage.presenter = _presenter;
_presenter.start();
}
}
}
namespace Wrox.BDD.Ui.Console
{
public class ConsoleGameView : GameView
{
private TicTacToeGamePresenter _presenter;
public ConsoleGameView()
{
_presenter =
new TicTacToeGamePresenter(this,
new TicTacToe(new TokenTracker(),
new NineSquareGrid()),
new PlainTextGameBoardRenderer());
_presenter.start();
}
}
}
4.2 确认令牌的放置
创建
when_checking_for_a_token_on_the_grid
规范:
using Machine.Specifications;
using Wrox.BDD.Domain;
using NUnit.Framework;
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
[Subject(typeof(NineSquareGrid))]
public class when_checking_for_a_token_on_the_grid
{
Establish context = () =>
{
coordinate_with_token = Coordinate.parse(“1,1”);
coordinate_without_token = Coordinate.parse(“2,2”);
SUT = new NineSquareGrid();
};
private Because of = () =>
{
SUT.place_token_at(coordinate_with_token, Tokens.x_token);
};
private It should_contain_a_token = () =>
{
Assert.That(SUT.contains_token_at(coordinate_with_token), Is.True);
};
private It should_not_contain_a_token = () =>
{
Assert.That(SUT.contains_token_at(coordinate_without_token), Is.False);
};
private static NineSquareGrid SUT;
private static Coordinate coordinate_with_token;
private static Coordinate coordinate_without_token;
}
}
修改
Grid
接口,添加
contains_token_at
方法:
namespace Wrox.BDD.Domain
{
public interface Grid
{
void place_token_at(Coordinate coordinate, Token token);
bool contains_token_at(Coordinate coordinate);
}
}
更新
NineSquareGrid
类,实现
contains_token_at
方法:
namespace Wrox.BDD.Domain
{
public class NineSquareGrid : Grid
{
protected readonly Token[,] _squares;
public NineSquareGrid()
{
_squares = new Token[3, 3];
}
public void place_token_at(Coordinate coordinate, Token token)
{
_squares[coordinate.X, coordinate.Y] = token;
}
public bool contains_token_at(Coordinate coordinate)
{
return _squares[coordinate.X, coordinate.Y] != null;
}
}
}
4.3 获取只读视图进行渲染
创建
when_rendering_the_game
规范:
using Machine.Specifications;
using Wrox.BDD.Domain;
using Wrox.BDD.Ui.Console.Presentation;
using Rhino.Mocks;
namespace Wrox.BDD.Specs.Core.Presentation_Specs
{
[Subject(typeof(PlainTextGameBoardRenderer))]
public class when_rendering_the_game
{
Establish context = () =>
{
var square_array = new string[3, 3];
game = MockRepository.GenerateStub<Game>();
game.Stub(x => x.get_game_view())
.Return(new GridView() { squares = square_array });
SUT = new PlainTextGameBoardRenderer();
};
private Because of = () =>
{
SUT.render(game);
};
private It should_ask_the_game_for_a_readonly_view = () =>
{
game.AssertWasCalled(x => x.get_game_view());
};
private static PlainTextGameBoardRenderer SUT;
private static Game game;
}
}
更新
Game
接口,添加
get_game_view
方法:
namespace Wrox.BDD.Domain
{
public interface Game
{
Token current_token();
void place_token_for_current_player_at(Coordinate coordinate);
GridView get_game_view();
}
}
创建
GridView
类:
namespace Wrox.BDD.Domain
{
public class GridView
{
public string[,] squares { get; set; }
}
}
更新
TicTacToe
类,实现
get_game_view
方法:
namespace Wrox.BDD.Domain
{
public class TicTacToe : Game
{
public GridView get_game_view()
{
return new GridView(){ squares = new string[3,3]};
}
}
}
更新
PlainTextGameBoardRenderer
类,实现渲染逻辑:
namespace Wrox.BDD.Ui.Console.Presentation
{
public class PlainTextGameBoardRenderer : BoardRenderer
{
public string render(Game game)
{
var squares = game.get_game_view().squares;
var grid_display = new StringBuilder();
var row_seperator = “”;
for (int row = 0; row <= 2; row++)
{
if (!String.IsNullOrEmpty(row_seperator))
grid_display.AppendLine(row_seperator);
var pipe = “”;
var row_display = new StringBuilder();
for (int column = 0; column <= 2; column++)
{
row_display.Append(string.Format(“{0} {1} “, pipe,
pad_if_square_empty(squares[row, column])));
pipe = “|”;
}
grid_display.AppendLine(row_display.ToString());
row_seperator = “-----------”;
}
return grid_display.ToString();
}
private string pad_if_square_empty(string value)
{
return String.IsNullOrEmpty(value) ? “ “ : value;
}
}
}
总结
通过以上步骤,我们逐步实现了井字棋游戏的多个功能,包括交替玩家、放置令牌、确认令牌放置和渲染游戏视图等。每个功能的实现都遵循了测试驱动开发的原则,通过编写规范和测试用例来确保代码的正确性。在开发过程中,我们还注重了代码的可维护性和可扩展性,将不同的功能模块分离,如游戏类、网格类和渲染器类等。
下面是整个开发过程的流程图:
graph TD;
A[项目起步] --> B[显示下一个玩家];
B --> C[集成交替玩家场景];
C --> D[显示游戏场景];
D --> D1[放置令牌在网格上];
D --> D2[确认令牌的放置];
D --> D3[获取只读视图进行渲染];
通过这个流程图,我们可以清晰地看到整个开发过程的步骤和顺序。每个步骤都有明确的目标和实现方法,确保了项目的顺利进行。
井字棋项目开发:从功能实现到测试与集成
5. 优化只读视图生成
虽然已经实现了获取只读视图进行渲染的功能,但生成只读视图的责任应该由网格实例承担。因此,我们创建了一个新的规范
when_generating_a_readonly_view_for_rendering
:
using Machine.Specifications;
using Wrox.BDD.Domain;
using NUnit.Framework;
namespace Wrox.BDD.Specs.Core.Domain_Specs
{
[Subject(typeof(NineSquareGrid))]
public class when_generating_a_readonly_view_for_rendering
{
Establish context = () =>
{
coordinate_with_X_token = Coordinate.parse(“1,1”);
coordinate_with_O_token = Coordinate.parse(“2,2”);
SUT = new NineSquareGrid();
};
private Because of = () =>
{
SUT.place_token_at(coordinate_with_X_token, Tokens.x_token);
SUT.place_token_at(coordinate_with_O_token, Tokens.o_token);
result = SUT.generate_grid_view();
};
private It should_have_the_X_token_in_the_correct_position = () =>
{
Assert.That(result.squares[coordinate_with_X_token.X,
coordinate_with_X_token.Y],
Is.EqualTo(Tokens.x_token.value));
};
private It should_have_the_O_token_in_the_correct_position = () =>
{
Assert.That(result.squares[coordinate_with_O_token.X,
coordinate_with_O_token.Y],
Is.EqualTo(Tokens.o_token.value));
};
private static GridView result;
private static NineSquareGrid SUT;
private static Coordinate coordinate_with_X_token;
private static Coordinate coordinate_with_O_token;
}
}
修改
Grid
接口,添加
generate_grid_view
方法:
namespace Wrox.BDD.Domain
{
public interface Grid
{
void place_token_at(Coordinate coordinate, Token token);
bool contains_token_at(Coordinate coordinate);
GridView generate_grid_view();
}
}
更新
NineSquareGrid
类,实现
generate_grid_view
方法:
namespace Wrox.BDD.Domain
{
public class NineSquareGrid : Grid
{
protected readonly Token[,] _squares;
public NineSquareGrid()
{
_squares = new Token[3, 3];
}
public void place_token_at(Coordinate coordinate, Token token)
{
_squares[coordinate.X, coordinate.Y] = token;
}
public bool contains_token_at(Coordinate coordinate)
{
return _squares[coordinate.X, coordinate.Y] != null;
}
public GridView generate_grid_view()
{
var readonly_squares = new string[3,3];
for (int row = 0; row < 3; row++)
for (int column = 0; column < 3; column++)
readonly_squares[row, column] =
_squares[row, column] == null ?
“” : _squares[row, column].value;
return new GridView() {squares = readonly_squares};
}
}
}
最后,更新
TicTacToe
类,将生成只读视图的操作委托给网格:
namespace Wrox.BDD.Domain
{
public class TicTacToe : Game
{
private PlayerTracker _player_tracker;
private Grid _grid;
public TicTacToe(PlayerTracker player_tracker, Grid grid)
{
_player_tracker = player_tracker;
_grid = grid;
}
public Token current_token()
{
return _player_tracker.current_player();
}
public void place_token_for_current_player_at(Coordinate coordinate)
{
_grid.place_token_at(coordinate, current_token());
_player_tracker.finish_players_move();
}
public GridView get_game_view()
{
return _grid.generate_grid_view();
}
}
}
6. 开发进度总结与展望
目前,我们已经完成了井字棋游戏的多个核心功能,包括交替玩家、放置令牌、确认令牌放置和渲染游戏视图等。以下是对已实现功能的总结表格:
| 功能模块 | 实现内容 |
| ---- | ---- |
| 交替玩家 | 创建
PlayerTracker
接口和
TokenTracker
类,实现玩家交替逻辑 |
| 放置令牌 | 创建
Grid
接口和
NineSquareGrid
类,实现令牌放置功能 |
| 确认令牌放置 | 在
Grid
接口和
NineSquareGrid
类中添加
contains_token_at
方法,确认令牌放置 |
| 渲染游戏视图 | 创建
GridView
类,在
Game
接口和
TicTacToe
类中添加
get_game_view
方法,在
PlainTextGameBoardRenderer
类中实现渲染逻辑 |
在开发过程中,我们遵循了测试驱动开发(TDD)的原则,通过编写规范和测试用例来确保代码的正确性。同时,我们注重代码的可维护性和可扩展性,将不同的功能模块分离,如游戏类、网格类和渲染器类等。
然而,项目仍有一些待完善的地方。例如,目前还没有处理输入坐标的异常情况,也没有实现游戏结束的判定逻辑,如判断是否有玩家获胜或平局。以下是后续开发的计划列表:
1.
异常处理
:添加对输入坐标的验证,处理无效输入的情况。
2.
游戏结束判定
:在
TicTacToe
类中添加方法,判断是否有玩家获胜或平局。
3.
用户界面优化
:可以考虑使用更美观的界面库来替换现有的纯文本渲染器。
下面是包含后续开发计划的流程图:
graph TD;
A[已完成功能] --> B[异常处理];
A --> C[游戏结束判定];
A --> D[用户界面优化];
通过这个流程图,我们可以清晰地看到后续开发的方向。每个待完成的任务都有明确的目标,有助于我们有条不紊地推进项目的进一步完善。
7. 代码复用与扩展建议
在整个开发过程中,我们将不同的功能模块进行了分离,这为代码的复用和扩展提供了便利。以下是一些代码复用和扩展的建议:
7.1 代码复用
-
Grid接口和NineSquareGrid类 :如果要开发其他基于网格的游戏,如五子棋或围棋,可以复用Grid接口和NineSquareGrid类的基本逻辑,只需根据具体游戏规则进行适当修改。 -
PlayerTracker接口和TokenTracker类 :对于需要交替玩家的游戏,都可以复用这部分代码来实现玩家交替的功能。
7.2 代码扩展
-
添加新的渲染器
:如果需要支持不同的显示方式,如图形界面或网页显示,可以创建新的渲染器类,实现
BoardRenderer接口。 -
扩展游戏规则
:可以在
TicTacToe类中添加新的方法,实现更复杂的游戏规则,如增加棋盘大小或改变获胜条件。
通过合理的代码复用和扩展,我们可以在现有代码的基础上快速开发新的游戏或功能,提高开发效率。
8. 总结
通过本次井字棋项目的开发,我们深入实践了测试驱动开发的方法,从项目起步到各个功能的逐步实现,再到后续的优化和扩展规划,每一步都有明确的目标和实现方法。我们不仅实现了一个完整的井字棋游戏,还注重了代码的质量和可维护性。
在未来的开发中,我们可以借鉴本次项目的经验,将功能模块分离、编写规范和测试用例等方法应用到其他项目中,提高项目的开发效率和代码质量。同时,不断完善和扩展现有项目,为用户提供更好的游戏体验。
439



被折叠的 条评论
为什么被折叠?



