井字游戏(Tic-tac-toe)智能合约

很多网络游戏编程的基础课程,会以井字游戏开始。EOS官方教程也不例外,本篇将讲述如何在EOS区块链上做一个去中心化的双人互动井字游戏。 井字游戏(Tic-tac-toe)智能合约 - 图1

一、设计与准备

玩家

该游戏将采取标准的3X3井字游戏方式。玩家被定义为两个角色:host与·challenger,Host(庄)首先画。同时,每对玩家最多同时玩两局,一局先下者做庄,一局后下者坐庄。

棋盘与数据结构

Instead of using o and x as in the traditional tic tac toe game. We use 1 to denote movement by host, 2 to denote movement by challenger, and 0 to denote empty cell. Furthermore, we will use one dimensional array to store the board. Hence: 在游戏中,1代表host占的格子,2代表challenger占的格子,0代表还没被占领的格子。 井字游戏(Tic-tac-toe)智能合约 - 图2 如上图,我们设定x(叉)为host占的格子,用1表示;o(圈)为challenger占的格子,用2表示,则上图的数据结构可以表示为:[0, 2, 1, 0, 1, 0, 1, 2, 2](先横后竖)

动作

User will have the following actions to interact with this contract:

create: create a new game restart: restart an existing game, host or challenger is allowed to do this close: close an existing game, which frees up the storage used to store the game, only host is allowed to do this move: make a movement

玩家将会做出以下动作,与智能合约交互:

  • create: 创建游戏
  • restart: 重启当前已有的一局游戏,host和challenger都能执行该行为
  • close: 关闭一局游戏,将存储资源从EOS中释放,只有host允许操作该行为
  • move: 占领某一个位置的格子

创建合约

下面教程,将会基于一个命名为 tic.tac.toe 的账户创建智能合约。如果 tic.tac.toe 账户名被使用,可以创建另外一个账户名,并将代码中出现的 tic.tac.toe 改为你的账户名。 如果还没建立账户,请先按以下命令建立账户,再进行下一步操作。

  1. $ cleos create account ${creator_name} ${contract_account_name} ${contract_pub_owner_key} ${contract_pub_active_key} --permission ${creator_name}@active
  2. # e.g. $ cleos create account inita tic.tac.toe EOS4toFS3YXEQCkuuw1aqDLrtHim86Gz9u3hBdcBw5KNPZcursVHq EOS7d9A3uLe6As66jzN8j44TXJUqJSK3bFjjEEqR4oTvNAB3iM9SA --permission inita@active

请确保钱包已经解锁,并且创建者的私钥已经被导入钱包。

二、开始

本合约将创建以下三个文件:

  • tic_tac_toe.hpp => 定义合约数据结构的头文件
  • tic_tac_toe.cpp => 合约主逻辑代码
  • tic_tac_toe.abi => 合约ABI文件

三、定义数据结构

新建(或打开)tic_tac_toe.hpp 文件,加入如下代码模板:

  1. // Import necessary library
  2. #include // Generic eosio library, i.e. print, type, math, etc
  3. using namespace eosio;
  4. namespace tic_tac_toe {
  5. static const account_name games_account = N(games);
  6. static const account_name code_account = N(tic.tac.toe); //账户名称,如果需要用其他账户名,在这里改
  7. // Your code here
  8. }

游戏列表

在这个智能合约中,我们需要定义一个Table列表,用来存储所有游戏。

  1. ...
  2. namespace tic_tac_toe {
  3. ...
  4. typedef eosio::multi_index< games_account, game> games;
  5. }

其中第一个参数games_account定义了table名称。后一个参数game定义了游戏结构(Game Structure)。

游戏数据结构 Game Structure

  1. ...
  2. namespace tic_tac_toe {
  3. static const uint32_t board_len = 9; //井字游戏3x3,数据长度
  4. struct game {
  5. game() {};
  6. //构造函数
  7. game(account_name challenger, account_name host):challenger(challenger), host(host), turn(host) {
  8. // Initialize board
  9. initialize_board();
  10. };
  11. account_name challenger;
  12. account_name host;
  13. account_name turn; // = account name of host/ challenger
  14. account_name winner = N(none); // = none/ draw/ account name of host/ challenger
  15. uint8_t board[9]; //棋盘数组
  16. // 将3x3上的格子全部清零,即回到最初状态
  17. void initialize_board() {
  18. for (uint8_t i = 0; i < board_len ; i++) {
  19. board[i] = 0;
  20. }
  21. }
  22. // 重启游戏
  23. void reset_game() {
  24. initialize_board();
  25. turn = host;
  26. winner = N(none);
  27. }
  28. auto primary_key() const { return challenger; } //确定table的索引字段为 challenger
  29. EOSLIB_SERIALIZE( game, (challenger)(host)(turn)(winner)(board) )
  30. };

行为定义

create 创建游戏

  1. ...
  2. struct create {
  3. account_name challenger;
  4. account_name host;
  5. EOSLIB_SERIALIZE( create, (challenger)(host) )
  6. };
  7. ...

restart 重启游戏

  1. ...
  2. struct restart {
  3. account_name challenger;
  4. account_name host;
  5. account_name by; //由谁来发起重启游戏
  6. EOSLIB_SERIALIZE( restart, (challenger)(host)(by) )
  7. };
  8. ...

close 关闭游戏

  1. ...
  2. struct close {
  3. account_name challenger;
  4. account_name host;
  5. EOSLIB_SERIALIZE( close, (challenger)(host) )
  6. };
  7. ...

move 占格子

  1. ...
  2. struct movement {
  3. uint32_t row; //行
  4. uint32_t column; //列
  5. EOSLIB_SERIALIZE( movement, (row)(column) )
  6. };
  7. struct Move {
  8. account_name challenger;
  9. account_name host;
  10. account_name by; // the account who wants to make the move
  11. movement mvt;
  12. EOSLIB_SERIALIZE( move, (challenger)(host)(by)(mvt) )
  13. };
  14. ...

至此,hpp文件定义完毕。以上定义的头文件,可在这里获取

四、主逻辑代码

打开tic_tac_toe.cpp文件,输入以下模板代码:

  1. #include <tic_tac_toe.hpp>
  2. using namespace eosio;
  3. /**
  4. * The apply() method must have C calling convention so that the blockchain can lookup and
  5. * call these methods.
  6. */
  7. extern "C" {
  8. using namespace tic_tac_toe;
  9. /// The apply method implements the dispatch of events to this contract
  10. void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
  11. // Put your action handler here
  12. }
  13. } // extern "C"

行为响应框架

我们只想让合约相应来自于合约创建人帐号,而且不同的行为需要执行不同的命令,因此,创建impl结构:

  1. using namespace eosio;
  2. namespace tic_tac_toe {
  3. struct impl {
  4. ...
  5. /// The apply method implements the dispatch of events to this contract
  6. void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
  7. if (code == code_account) {
  8. if (action == N(create)) {
  9. //注意:eosio::unpack_action_data<T>()用于将收到的动作转换为T
  10. impl::on(eosio::unpack_action_data<tic_tac_toe::create>());
  11. } else if (action == N(restart)) {
  12. impl::on(eosio::unpack_action_data<tic_tac_toe::restart>());
  13. } else if (action == N(close)) {
  14. impl::on(eosio::unpack_action_data<tic_tac_toe::close>());
  15. } else if (action == N(move)) {
  16. impl::on(eosio::unpack_action_data<tic_tac_toe::move>());
  17. }
  18. }
  19. }
  20. };
  21. }
  22. ...
  23. extern "C" {
  24. using namespace tic_tac_toe;
  25. // apply 方法用来执行广播到该合约的事件
  26. void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
  27. //直接执行impl方法
  28. impl().apply(receiver, code, action);
  29. }
  30. } // extern "C"

在impl结构中,需要添加如下实现方法,先把框架写入,具体实现方法见下文:

  1. ...
  2. struct impl {
  3. ...
  4. /**
  5. * @param create - action to be applied
  6. */
  7. void on(const create& c) {
  8. // Put code for create action here
  9. }
  10. /**
  11. * @brief Apply restart action
  12. * @param restart - action to be applied
  13. */
  14. void on(const restart& r) {
  15. // Put code for restart action here
  16. }
  17. /**
  18. * @brief Apply close action
  19. * @param close - action to be applied
  20. */
  21. void on(const close& c) {
  22. // Put code for close action here
  23. }
  24. /**
  25. * @brief Apply move action
  26. * @param move - action to be applied
  27. */
  28. void on(const move& m) {
  29. // Put code for move action here
  30. }
  31. /// The apply method implements the dispatch of events to this contract
  32. void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
  33. ...

实现 Create 创建游戏

在创建游戏操作中,我们需要做以下几件事情:

  • 确保该动作已经host签名
  • 确保host和challenger不是同一个玩家
  • 确保没有已经存在的游戏
  • 将该新建的游戏保存到db中
  1. struct impl {
  2. ...
  3. /**
  4. * @brief Apply create action
  5. * @param create - action to be applied
  6. */
  7. void on(const create& c) {
  8. require_auth(c.host); //从c.host中获取host用户名,并且验证是否已经签名
  9. eosio_assert(c.challenger != c.host, "challenger shouldn't be the same as host"); //确保host和challenger不是一个玩家
  10. //检查是否存在相同的游戏
  11. games existing_host_games(code_account, c.host);
  12. auto itr = existing_host_games.find( c.challenger );
  13. eosio_assert(itr == existing_host_games.end(), "game already exists");
  14. existing_host_games.emplace(c.host, [&]( auto& g ) {
  15. //判断是否是相同的游戏,即challenger一致,host一致,当前庄家一致
  16. g.challenger = c.challenger;
  17. g.host = c.host;
  18. g.turn = c.host;
  19. });
  20. }
  21. ...
  22. }

实现 Close 结束游戏

结束游戏操作中,我们需要做以下事情:

  • 确保该动作已经host签名
  • 确保游戏存在
  • 从链上数据库中删除游戏
  1. struct impl {
  2. ...
  3. /**
  4. * @brief Apply close action
  5. * @param close - action to be applied
  6. */
  7. void on(const close& c) {
  8. require_auth(c.host); //验证host签名
  9. //检查游戏是否存在,实现方法在上面create中有了
  10. games existing_host_games(code_account, c.host);
  11. auto itr = existing_host_games.find( c.challenger );
  12. eosio_assert(itr != existing_host_games.end(), "game doesn't exists");
  13. //删除游戏,释放链上资源
  14. existing_host_games.erase(itr);
  15. }
  16. ...
  17. }

实现 Move 占位操作

在Move操作中,我们需要实现:

  • 确保该操作已经被host或者challenger签名
  • 确保游戏存在
  • 确保游戏尚未结束
  • 确保该操作是有host或者challenger发出的指令
  • 确保当前轮正好由正确的玩家操作
  • 确认占位操作是有效的
  • 更新棋盘数据
  • 变更玩家轮次
  • 判断是否有胜者
  • 保存游戏数据到链上数据库中
  1. struct impl {
  2. ...
  3. bool is_valid_movement(const movement& mvt, const game& game_for_movement) {
  4. // 占位是否有效
  5. }
  6. account_name get_winner(const game& current_game) {
  7. // 是否有胜者
  8. }
  9. ...
  10. /**
  11. * @brief Apply move action
  12. * @param move - action to be applied
  13. */
  14. void on(const move& m) {
  15. require_auth(m.by); //判断当前轮的用户是否已经签名
  16. // 检查游戏是否存在
  17. games existing_host_games(code_account, m.host);
  18. auto itr = existing_host_games.find( m.challenger );
  19. eosio_assert(itr != existing_host_games.end(), "game doesn't exists");
  20. // 检查游戏是否已经结束
  21. eosio_assert(itr->winner == N(none), "the game has ended!");
  22. // 检查指令是否由host或者challenger发出
  23. eosio_assert(m.by == itr->host || m.by == itr->challenger, "this is not your game!");
  24. // 检查发送指令的是不是轮到该轮
  25. eosio_assert(m.by == itr->turn, "it's not your turn yet!");
  26. // 检查占位是否有效
  27. eosio_assert(is_valid_movement(m.mvt, *itr), "not a valid movement!");
  28. // 修改占位数据,1-host,2-challenger
  29. const auto cell_value = itr->turn == itr->host ? 1 : 2;
  30. const auto turn = itr->turn == itr->host ? itr->challenger : itr->host;
  31. existing_host_games.modify(itr, itr->host, [&]( auto& g ) {
  32. g.board[m.mvt.row * 3 + m.mvt.column] = cell_value;
  33. g.turn = turn;
  34. //检查是否有胜者
  35. g.winner = get_winner(g);
  36. });
  37. }
  38. ...
  39. }

验证占位是否有效实现方法: 1- 检查占位是否有效的方法,是否超出9格长度 2- 格子是否位空,即为0

  1. struct impl {
  2. ...
  3. /**
  4. * @brief Check if cell is empty
  5. * @param cell - value of the cell (should be either 0, 1, or 2)
  6. * @return true if cell is empty
  7. */
  8. bool is_empty_cell(const uint8_t& cell) {
  9. return cell == 0;
  10. }
  11. /**
  12. * @brief Check for valid movement
  13. * @detail Movement is considered valid if it is inside the board and done on empty cell
  14. * @param movement - the movement made by the player
  15. * @param game - the game on which the movement is being made
  16. * @return true if movement is valid
  17. */
  18. bool is_valid_movement(const movement& mvt, const game& game_for_movement) {
  19. uint32_t movement_location = mvt.row * 3 + mvt.column;
  20. bool is_valid = movement_location < board_len && is_empty_cell(game_for_movement.board[movement_location]);
  21. return is_valid;
  22. }
  23. ...
  24. }

确定是否胜利的方法: 根据纵向、横向、斜向是否一致,来判断胜负

  1. struct impl {
  2. ...
  3. /**
  4. * @brief Get winner of the game
  5. * @detail Winner of the game is the first player who made three consecutive aligned movement
  6. * @param game - the game which we want to determine the winner of
  7. * @return winner of the game (can be either none/ draw/ account name of host/ account name of challenger)
  8. */
  9. account_name get_winner(const game& current_game) {
  10. if((current_game.board[0] == current_game.board[4] && current_game.board[4] == current_game.board[8]) ||
  11. (current_game.board[1] == current_game.board[4] && current_game.board[4] == current_game.board[7]) ||
  12. (current_game.board[2] == current_game.board[4] && current_game.board[4] == current_game.board[6]) ||
  13. (current_game.board[3] == current_game.board[4] && current_game.board[4] == current_game.board[5])) {
  14. // - | - | x x | - | - - | - | - - | x | -
  15. // - | x | - - | x | - x | x | x - | x | -
  16. // x | - | - - | - | x - | - | - - | x | -
  17. if (current_game.board[4] == 1) {
  18. return current_game.host;
  19. } else if (current_game.board[4] == 2) {
  20. return current_game.challenger;
  21. }
  22. } else if ((current_game.board[0] == current_game.board[1] && current_game.board[1] == current_game.board[2]) ||
  23. (current_game.board[0] == current_game.board[3] && current_game.board[3] == current_game.board[6])) {
  24. // x | x | x x | - | -
  25. // - | - | - x | - | -
  26. // - | - | - x | - | -
  27. if (current_game.board[0] == 1) {
  28. return current_game.host;
  29. } else if (current_game.board[0] == 2) {
  30. return current_game.challenger;
  31. }
  32. } else if ((current_game.board[2] == current_game.board[5] && current_game.board[5] == current_game.board[8]) ||
  33. (current_game.board[6] == current_game.board[7] && current_game.board[7] == current_game.board[8])) {
  34. // - | - | - - | - | x
  35. // - | - | - - | - | x
  36. // x | x | x - | - | x
  37. if (current_game.board[8] == 1) {
  38. return current_game.host;
  39. } else if (current_game.board[8] == 2) {
  40. return current_game.challenger;
  41. }
  42. } else {
  43. bool is_board_full = true;
  44. for (uint8_t i = 0; i < board_len; i++) {
  45. if (is_empty_cell(current_game.board[i])) {
  46. is_board_full = false;
  47. break;
  48. }
  49. }
  50. if (is_board_full) {
  51. return N(draw);
  52. }
  53. }
  54. return N(none);
  55. }
  56. ...
  57. }

以上,cpp主逻辑代码部分完成,完整代码点击查看tic_tack_toe.cpp

五、创建ABI

ABI文件(Application Binary Interface),目的是为了让合约理解发送过去的二进制指令。 ABI文件模板如下:

  1. {
  2. "types": [],
  3. "structs": [{
  4. "name": "...",
  5. "base": "...",
  6. "fields": { ... }
  7. }, ...],
  8. "actions": [{
  9. "name": "...",
  10. "type": "...",
  11. "ricardian_contract": "..."
  12. }, ...],
  13. "tables": [{
  14. "name": "...",
  15. "type": "...",
  16. "index_type": "...",
  17. "key_names" : [...],
  18. "key_types" : [...]
  19. }, ...],
  20. "clauses: [...]

ABI文件定义了:struct, action, table,三种数据类型的映射表。与hpp头文件一一对应。

数据库映射(Table)映射ABI

  1. {
  2. ...
  3. "structs": [{
  4. "name": "game",
  5. "base": "",
  6. "fields": [
  7. {"name":"challenger", "type":"account_name"},
  8. {"name":"host", "type":"account_name"},
  9. {"name":"turn", "type":"account_name"},
  10. {"name":"winner", "type":"account_name"},
  11. {"name":"board", "type":"uint8[]"}
  12. ]
  13. },...
  14. ],
  15. "tables": [{
  16. "name": "games",
  17. "type": "game",
  18. "index_type": "i64",
  19. "key_names" : ["challenger"],
  20. "key_types" : ["account_name"]
  21. }
  22. ],
  23. ...
  24. }

行为(Action)映射ABI

  1. {
  2. ...
  3. "structs": [{
  4. ...
  5. },{
  6. "name": "create",
  7. "base": "",
  8. "fields": [
  9. {"name":"challenger", "type":"account_name"},
  10. {"name":"host", "type":"account_name"}
  11. ]
  12. },{
  13. "name": "restart",
  14. "base": "",
  15. "fields": [
  16. {"name":"challenger", "type":"account_name"},
  17. {"name":"host", "type":"account_name"},
  18. {"name":"by", "type":"account_name"}
  19. ]
  20. },{
  21. "name": "close",
  22. "base": "",
  23. "fields": [
  24. {"name":"challenger", "type":"account_name"},
  25. {"name":"host", "type":"account_name"}
  26. ]
  27. },{
  28. "name": "movement",
  29. "base": "",
  30. "fields": [
  31. {"name":"row", "type":"uint32"},
  32. {"name":"column", "type":"uint32"}
  33. ]
  34. },{
  35. "name": "move",
  36. "base": "",
  37. "fields": [
  38. {"name":"challenger", "type":"account_name"},
  39. {"name":"host", "type":"account_name"},
  40. {"name":"by", "type":"account_name"},
  41. {"name":"mvt", "type":"movement"}
  42. ]
  43. }
  44. ],
  45. "actions": [{
  46. "name": "create",
  47. "type": "create",
  48. "ricardian_contract": "" //每个Action都需要对应一个李嘉图合约
  49. },{
  50. "name": "restart",
  51. "type": "restart",
  52. "ricardian_contract": ""
  53. },{
  54. "name": "close",
  55. "type": "close",
  56. "ricardian_contract": ""
  57. },{
  58. "name": "move",
  59. "type": "move",
  60. "ricardian_contract": ""
  61. }
  62. ],
  63. ...
  64. }

六、编译与部署

略。。。

七、怎么玩

创建游戏

  1. $ cleos push action tic.tac.toe create '{"challenger":"inita", "host":"initb"}' --permission initb@active

占位

  1. $ cleos push action tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"initb", "mvt":{"row":0, "column":0} }' --permission initb@active
  2. $ cleos push action tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"inita", "mvt":{"row":1, "column":1} }' --permission inita@active

重新启动现有游戏

  1. $ cleos push action tic.tac.toe restart '{"challenger":"inita", "host":"initb", "by":"initb"}' --permission initb@active

关闭游戏

  1. $ cleos push action tic.tac.toe close '{"challenger":"inita", "host":"initb"}' --permission initb@active

查看游戏状态

  1. $ cleos get table tic.tac.toe initb games

八、其他

  • 做成网页,前端通过eosjs调用智能合约执行。
  • 在合约中加入代币转移,这个游戏则可以立即变成一个有奖惩的小游戏。