双手同时给出选择(石头剪刀布),对方同样也给出自己的选择,这个时候出拳情况是透明的(玩家可见),经过分析后选择自己的左手或者右手作为最终的选择,和对方的抉择进行比较,如果玩家赢了,那么押金......
双手同时给出选择(石头剪刀布),对方同样也给出自己的选择,这个时候出拳情况是透明的(玩家可见),经过分析后选择自己的左手或者右手作为最终的选择,和对方的抉择进行比较,如果玩家赢了,那么押金就原封不动退回,如果玩家输了(平局也算输),押金就归对方(也就是此游戏的发布者)所有。
首先考虑的是双方出手情况该如何存储,当然可以 $\mathit {user_left},\ \mathit {user_right},\ \mathit {robot_left},\ \mathit {robot_right}$ 这样子存储多个 $\mathit {String}$,但如果后面遇到了更多的选择呢?也一个一个列举吗?答案是否定的,这里就引入一个动态数组 $\mathit {Vector}$ 的概念。
它是一个泛型类型,这意味着它可以保存任何类型的数据,但一个 $\mathit {Vector}$ 当中的数据类型是统一。你不需要像某些编程语言一样,在一开始声明数组容量,因为它是动态的,可以根据情况增加或减少,只是,当存储数据过多的时候,可能会导致各项相关的操作速度变慢。
也就是说,我们只需要一个 $\mathit {Vector}$<$\mathit {String}$>,就可以存储多个字符串类型的数据,那么如何区分 $\mathit {user}$ 和 $\mathit {robot}$ 呢?
一个最简单的方式当然就是在编写代码的时候人为固定规则,例如 $\mathit {Vector}$ 中下标为 $\text 0$ 的数据表示 $\mathit {user_left}$,下标为 $\text 1$ 的数据表示 $\mathit {user_right}$,以此类推,但还是那个问题,如果后面遇到的情况更为复杂呢?这样显然不现实。
$\mathit {Table}$ 也是 $\mathit {Move}$ 当中的一个动态数据结构,简单来说就是一张键-值映射表,可以通过 $\mathit {Key}$ 来找到其对应的 $\mathit {Value}$。<br>这里有一个限制,$\mathit {Key}$ 必须具备 $\mathit {copy\ +\ drop\ +\ store}$ 能力,而 $\mathit {Value}$ 必须具备 $\mathit {store}$ 能力,因为 $\mathit {Table}$ 当中的键往往需要被复制出来使用,而值则往往需要被存储在对象当中。<br>一般而言,$\mathit {Move}$ 语言自带的数据类型大部分都可以直接作为键或者值使用,如果有自定义结构的需要,那么不要忘记上述规则。
回到刚刚的问题,只需要将 $\mathit {Table}$ 和 $\mathit {Vector}$ 简单结合就可以用来存储我们所需要的数据:Table<String, vector<String>>
$\mathit {Sui}\ \mathit {Move}$ 提供了一套完善的资金服务,具体可见 $\mathit {Coin}$ 和 $\mathit {Balance}$。
$\mathit {Coin}$ 就像是一个钱包,而 $\mathit {Balance}$ 则是该钱包当中的现金余额,后者无法单独存在,必须被保存在某个地方,前者虽然也有 $\mathit {store}$ 能力,理论上讲也可以被存储到自定义的结构当中,但一般不这么做,而是将其中的余额取出存储在自定义的对象当中来进行后续操作。
调用对应的函数方法,就可以将余额从一个地方转移到另一个地方,如果想要将其支付给某个人,只需要将其包装成 $\mathit {Coin}$(也有对应的函数可直接调用)再将其所有权转移到对应地址即可,就像 $\mathit {transfer}$ 一个 $\mathit {Hello\ World}$ 一样简单。
结合上述三点,可以定义如下结构:
struct Game has key {
id: UID,
balance: Balance<SUI>,
hands: Table<String, vector<String>>,
}
我们还需要一个能够存储游戏发布者地址的结构,它应该在一开始就被定义,所有人可见但无法被更改,某种程度上就像是执行最后一步(猜拳最终结果)的权杖所在。
struct GameCap has key {
id: UID,
creator: address,
}
fun init(ctx: &mut TxContext) {
transfer::freeze_object(GameCap {
id: object::new(ctx),
creator: tx_context::sender(ctx),
});
}
玩家缴纳押金,给出自己双手究竟出的是石头、剪刀还是布,同时传入0x6
代表clock
所在的地址用作截取当前时间戳,以此来决定对方出手。<br>每个 $\mathit {Vector}$ 由于只有两个值,所以在定义的时候直接通过vector[sth1, sth2, ...]
的方式赋值,而 $\mathit {Table}$ 则是先新建一个空的结构table::new<String, vector<String>>(ctx)
再通过table::add(&mut table, key, val)
的形式添加。
const ENOTBALANCE: u64 = 0;
const ENOTCORRECTHANDS: u64 = 1;
const ROCK: vector<u8> = b"rock";
const PAPER: vector<u8> = b"paper";
const SCISSOR: vector<u8> = b"scissor";
fun check(hand: vector<u8>): bool {
hand == ROCK || hand == PAPER || hand == SCISSOR
}
fun hand_to_number(hand: vector<u8>): u64 {
if (hand == ROCK)
0
else if (hand == PAPER)
1
else
2
}
fun number_to_hand(number: u64): vector<u8> {
if (number == 0)
ROCK
else if (number == 1)
PAPER
else
SCISSOR
}
entry fun create_game(left_hand: vector<u8>, right_hand: vector<u8>, coin: Coin<SUI>, clock: &Clock, ctx: &mut TxContext) {
assert!(coin::value(&coin) > 0, ENOTBALANCE);
assert!(check(left_hand) && check(right_hand), ENOTCORRECTHANDS);
let robot_left_hand = number_to_hand(clock::timestamp_ms(clock) % 3);
let robot_right_hand = number_to_hand((hand_to_number(left_hand) + hand_to_number(right_hand) + hand_to_number(robot_left_hand)) % 3);
let hands = table::new<String, vector<String>>(ctx);
let user_hands = vector[string::utf8(left_hand), string::utf8(right_hand)];
let robot_hands = vector[string::utf8(robot_left_hand), string::utf8(robot_right_hand)];
table::add(&mut hands, string::utf8(b"user"), user_hands);
table::add(&mut hands, string::utf8(b"robot"), robot_hands);
let game = Game {
id: object::new(ctx),
balance: coin::into_balance(coin),
hands,
};
transfer::transfer(game, tx_context::sender(ctx));
}
创建完游戏后可以查看 $\mathit {Game\ Object}$ 当中双方的出手情况,经过头脑风暴决定自己最后是出左手还是右手,这里还需要传入 $\mathit {GameCap}$ 来确保玩家输了之后押金可以发送给游戏发布者,同样的,对方的出手依旧由时间戳来决定,最后,不要忘记把需要手动 $\mathit {drop}$ 的操作一下。<br>$\mathit {Vector}$ 和 $\mathit {Table}$ 的取值需要用到各自的 $\mathit {borrow}$ 函数,只不过前者用的是下标$\mathit {(index)}$,后者用的是键$\mathit {(Key)}$。
const ENOTCORRECTCHOOSE: u64 = 2;
const LEFT: vector<u8> = b"left";
const RIGHT: vector<u8> = b"right";
fun choose_to_number(hand: vector<u8>): u64 {
if (hand == LEFT)
0
else
1
}
fun user_win(user_hand: String, robot_hand: String): bool {
let rock = string::utf8(ROCK);
let paper = string::utf8(PAPER);
let scissor = string::utf8(SCISSOR);
user_hand == rock && robot_hand == scissor || user_hand == scissor && robot_hand == paper || user_hand == paper && robot_hand == rock
}
entry fun choose_hand(game_cap: &GameCap, game: Game, hand: vector<u8>, clock: &Clock, ctx: &mut TxContext) {
assert!(hand == LEFT || hand == RIGHT, ENOTCORRECTCHOOSE);
let Game {
id,
balance,
hands,
} = game;
object::delete(id);
let user_idx = choose_to_number(hand);
let robot_idx = clock::timestamp_ms(clock) % 2;
let user_hand = vector::borrow(table::borrow(&hands, string::utf8(b"user")), user_idx);
let robot_hand = vector::borrow(table::borrow(&hands, string::utf8(b"robot")), robot_idx);
let recipient = if (user_win(*user_hand, *robot_hand)) tx_context::sender(ctx) else game_cap.creator;
let amount = math::min(balance::value(&balance), 1000000000);
transfer::public_transfer(coin::take(&mut balance, amount, ctx), recipient);
table::drop(hands);
if (balance::value(&balance) > 0) {
amount = balance::value(&balance);
transfer::public_transfer(coin::take(&mut balance, amount, ctx), tx_context::sender(ctx));
};
balance::destroy_zero(balance);
}
sui move build
解决报错后发布sui client publish --gas-budget 100000000
在成功后得到的信息当中,获取关键的 $\mathit {Object\ ID}$ 设置环境变量,方便后续调用。
export PACKAGE_ID=0x679f4f04bce8d849a1f6488655cd67c9b91c1d5c757bebee8e8ff59ca14311bb
export GAMECAP=0xd265d859652446def1539abb200ce526a9ae4d1593387e96a2672f8c8909220b
切换其他用户(玩家)并分出参加游戏时用的 $\mathit {Coin}$:
sui client switch --address peaceful-hiddenite
sui client gas
# 记录上述命令得到的gasCoinId
export COIN=0xa5e9cc06d7bfb34b49fd2f6631e0580129fa8124a36030ae5107ebf785fca37c
# 分出999
sui client split-coin --coin-id $COIN --amounts 999 --gas-budget 10000000
sui client gas
# 记录上述命令得到的gasCoinId(用新分出的999)
export COIN=0xa4e204dd0fcf85910aa6411bfab04a56203ae67de25d743e55f28bb494062a8f
在 $\mathit {split}$-$\mathit {coin}$ 环节(以及后面所有可能的将 $\mathit {Coin}$ 作为参数传递的交易当中),可能会得到余额不足的报错,这是因为你将唯一的 $\mathit {gasCoinId}$ 用作参数传递(分割的本体),以至于没有其它余额支付这一笔交易了。<br>想要解决很简单,如果是测试网直接开水就行,如果是主网就用另一个账户给你转一笔账(左手倒右手)。
sui client call --package $PACKAGE_ID --module hands_rock_paper_scissors --function create_game --args rock paper $COIN 0x6 --gas-budget 10000000
在得到的信息当中,将 $\mathit {Game}$ 这个对象地址也设为环境变量:export GAME=0xb603fe81956368419dab526e5f502bbeb0ccb95380d5c0a4690132900f325864
如果你直接sui client $GAME
会发现,看不到内部存储的 $\mathit {Table}$ 和 $\mathit {Vector}$ 具体的值,没关系,不是还有两个类型为0x2::dynamic_field::Field<0x1::string::String, vector<0x1::string::String>>
的对象么?它们一个是你双手的石头剪刀布的情况,一个是对手双手的石头剪刀布的情况,都可以通过sui client object <Object ID>
进行查看。
一阵头脑风暴过后,决定选择左手,那么:sui client call --package $PACKAGE_ID --module hands_rock_paper_scissors --function choose_hand --args $GAMECAP $GAME left 0x6 --gas-budget 10000000
在后半段信息当中,有一块是 $\mathit {Created\ Objects}$,这里新建了一个 $\mathit {Coin}$<$\mathit {SUI}$>,也就是押金从余额$\mathit {(balance)}$又变成了钱包$\mathit {(coin)}$,而它的拥有者就是Owner: Account Address
后的地址。<br>不难发现,这个地址跟我们(玩家)的地址一致,说明在这一局双手石头剪刀布的游戏当中,玩家取得了胜利。<br>这一点,也可以通过sui client gas
来确认,可以看见有一笔余额为 $\text {999}$ 的 $\mathit {gasCoinId}$ 新增了。
这里给出一局赢和一局输的游戏调用的 $\mathit {hash}$,有兴趣的可以在浏览器进行查看:
注意: 本篇所涉及的双手石头剪刀布游戏已发布至主网,有兴趣的欢迎来体验 送钱 ,同时请思考,如果想要在最后得知具体的出手情况,该对代码作何修改?
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!