前面我们介绍了元素战争这个游戏最基本的组成要素,只有一些基本的东西是无法实现整个游戏的过程的,接下来我们继续来探讨整个游戏资源,游戏规则,游戏流程。在本节内容中我们将继续来完善智能合约中的startgame和playcard两个action。
我们要知道在eos系统中保存数据的方式就是多索引表,因此为了保存每一局游戏的信息,我们需要创建一个多索引表对应的数据结构。同时每一个玩家都有其独立的游戏信息,因此我们把user_info结构体中添加游戏的具体信息:
struct game {
int8_t life_player = 5;
int8_t life_ai = 5;
vector<card_id> deck_player = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
vector<card_id> deck_ai = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
vector<card_id> hand_player = {0, 0, 0, 0};
vector<card_id> hand_ai = {0, 0, 0, 0};
card_id selected_card_player = 0;
card_id selected_card_ai = 0;
uint8_t life_lost_player = 0;
uint8_t life_lost_ai = 0;
int8_t status = ONGOING;
};
每把游戏都有三个状态,即:
游戏的初始状态我们设置为ONGING,同时为了增加可读性,我们用一个枚举来表示游戏的状态:
enum game_status: int8_t {
ONGOING = 0,
PLAYER_WON = 1,
PLAYER_LOST = -1
};
如何来判断这局游戏结束了呢,我们稍微玩过一些游戏的人都知道有个HP值,代表玩家的血条值,当HP变为0的时候,代表玩家已经死亡,游戏结束,那么这个游戏中还包含有哪些元素呢:
卡牌具有以下五种元素类型,元素兼容性到底是什么样的以及这个游戏该怎么玩呢?来和我一起玩就知道了:
每种元素的卡牌所拥有的攻击力如下:
同时为了记录卡牌的类型,我们也用枚举来示例,考虑到一些可能产生的异常情况,我们将EMPTY这种类型也添加进来:
enum card_type: uint8_t {
EMPTY = 0, // Represents empty slot in hand
FIRE = 1,
WOOD = 2,
WATER = 3,
NEUTRAL = 4,
VOID = 5
};
我们前面说过每张卡牌都有其具体的攻击力,可以简单的用一个map表来存储这17张卡牌信息:
const map<card_id, card> card_dict = {
{ 0, {EMPTY, 0} },
{ 1, {FIRE, 1} },
{ 2, {FIRE, 1} },
{ 3, {FIRE, 2} },
{ 4, {FIRE, 2} },
{ 5, {FIRE, 3} },
{ 6, {WOOD, 1} },
{ 7, {WOOD, 1} },
{ 8, {WOOD, 2} },
{ 9, {WOOD, 2} },
{ 10, {WOOD, 3} },
{ 11, {WATER, 1} },
{ 12, {WATER, 1} },
{ 13, {WATER, 2} },
{ 14, {WATER, 2} },
{ 15, {WATER, 3} },
{ 16, {NEUTRAL, 3} },
{ 17, {VOID, 0} }
};
了解了卡牌的信息我们来看每一轮游戏中都包含有哪些重要信息,注释在代码里:
struct game {
//玩家和AI拥有的血量
int8_t life_player = 5;
int8_t life_ai = 5;
//用两个vector分别来存储玩家和AI的初始化的卡牌信息(和我们刚才定义的map表对应)
vector<card_id> deck_player = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
vector<card_id> deck_ai = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
//再用两个vector分别存储游戏玩家和AI当前手中所持有的卡牌信息,刚开始的时候都是空的,因此我们用0即EMPTY来初始化这些信息
vector<card_id> hand_player = {0, 0, 0, 0};
vector<card_id> hand_ai = {0, 0, 0, 0};
//每一轮游戏玩家和AI都会选择出牌,我们用接下来两个变量来存储出牌的信息
card_id selected_card_player = 0;
card_id selected_card_ai = 0;
//每一轮游戏玩家和AI掉的血量记录在下面两个变量
uint8_t life_lost_player = 0;
uint8_t life_lost_ai = 0;
int8_t status = ONGOING;
};
上面介绍了游戏,玩家,卡牌的信息,为了增加可玩性我们添加一些随机的元素在里面,当然这个随机数的生成方法只是简单的在元素战争游戏里使用,对公平性要求较高的游戏不推荐使用该方法生成随机数,这个随机数方法主要有以下两个功能:
我们知道随机数的生成必然少不了随机数种子,在这里我们使用的是blocktime,为了让这个随机数种子在玩家之间公平传递,我们也要把随机数种子存储在多索引表中,当随机数方法被调用的时候,该表将会更新:
// @abi table seed
struct seed {
uint64_t key = 1;
uint32_t value = 1;
auto primary_key() const { return key; }
};
我们来看随机数是如何生成的,如我们前面说的,这种随机数生成的方式不建议使用在对公平性要求很高的Dapp游戏开发中,尤其是菠菜游戏:
int cardgame::random(const int range) {
auto seed_iterator = _seed.begin();
// 先查找表中是否已经存在随机数种子,如果不存在就初始化存储一个
if (seed_iterator == _seed.end()) {
seed_iterator = _seed.emplace( _self, [&]( auto& seed ) { });
}
// 生成一个新的随机数种子
int prime = 65537;
auto new_seed_value = (seed_iterator->value + now()) % prime;
// 将生成的随机数种子更新到表中
_seed.modify( seed_iterator, _self, [&]( auto& s ) {
s.value = new_seed_value;
});
// 获取一定范围内随机数的结果并返回
int random_result = new_seed_value % range;
return random_result;
}
现在让我们来开始游戏,具体做了什么也在代码的注释里:
void cardgame::startgame(account_name username) {
// 玩家权限确认,必不可少
require_auth(username);
//查看用户是不是第一次玩
auto& user = _users.get(username, "User doesn't exist");
_users.modify(user, username, [&](auto& modified_user) {
// 为用户创建一个新游戏并给玩家和AI分别分配卡牌,卡牌的分发使用了我们刚刚说的随机数的生成
game game_data;
for (uint8_t i = 0; i < 4; i++) {
draw_one_card(game_data.deck_player, game_data.hand_player);
draw_one_card(game_data.deck_ai, game_data.hand_ai);
}
modified_user.game_data = game_data;
});
}
游戏开始之后我们出牌的规则如下:
void cardgame::playcard(account_name username, uint8_t player_card_idx) {
// 1.确认玩家权限 2.检查卡牌的id是否有效(每个人手中只有四张牌)3.确认游戏正在进行
require_auth(username);
eosio_assert(player_card_idx < 4, "playcard: Invalid hand index");
auto& user = _users.get(username, "User doesn't exist");
eosio_assert(user.game_data.status == ONGOING,
"playcard: This game has ended. Please start a new one");
eosio_assert(user.game_data.selected_card_player == 0,
"playcard: The player has played his card this turn!");
_users.modify(user, username, [&](auto& modified_user) {
game& game_data = modified_user.game_data;
game_data.selected_card_player = game_data.hand_player[player_card_idx];
game_data.hand_player[player_card_idx] = 0;
});
}
现在智能合约中的action都编写完成了,我们再来看前端中分别展示了什么:
GameMat:
GameInfo:
玩家手中持有的卡牌:
OK,所有的内容都介绍结束之后我们点击前端的开始按钮:
而玩家出牌的代码流程如下:
至此一个开始游戏和出牌的过程就完成了。
本文介绍了元素战争游戏中如何编写开始游戏和出牌的逻辑,其中包含有游戏的主要元素,卡牌的属性值,一个简单的随机数的生成等,更多的内容我们接下来也会继续分析。如果对该游戏感兴趣,可以一起来玩。