一:游戏简介
双手同时给出选择(石头剪刀布),对方同样也给出自己的选择,这个时候出拳情况是透明的(玩家可见),经过分析后选择自己的左手或者右手作为最终的选择,和对方的抉择进行比较,如果玩家赢了,那么押金就原封不动退回,如果玩家输了(平局也算输),押金就归对方(也就是此游戏的发布者)所有。
二:结构设计
2.1 Vector
首先考虑的是双方出手情况该如何存储,当然可以 u s e r _ l e f t , u s e r _ r i g h t , r o b o t _ l e f t , r o b o t _ r i g h t \mathit {user\_left},\ \mathit {user\_right},\ \mathit {robot\_left},\ \mathit {robot\_right} user_left, user_right, robot_left, robot_right 这样子存储多个 S t r i n g \mathit {String} String,但如果后面遇到了更多的选择呢?也一个一个列举吗?答案是否定的,这里就引入一个动态数组 V e c t o r \mathit {Vector} Vector 的概念。
它是一个泛型类型,这意味着它可以保存任何类型的数据,但一个 V e c t o r \mathit {Vector} Vector 当中的数据类型是统一。你不需要像某些编程语言一样,在一开始声明数组容量,因为它是动态的,可以根据情况增加或减少,只是,当存储数据过多的时候,可能会导致各项相关的操作速度变慢。
也就是说,我们只需要一个 V e c t o r \mathit {Vector} Vector< S t r i n g \mathit {String} String>,就可以存储多个字符串类型的数据,那么如何区分 u s e r \mathit {user} user 和 r o b o t \mathit {robot} robot 呢?
2.2 Table
一个最简单的方式当然就是在编写代码的时候人为固定规则,例如 V e c t o r \mathit {Vector} Vector 中下标为 0 \text 0 0 的数据表示 u s e r _ l e f t \mathit {user\_left} user_left,下标为 1 \text 1 1 的数据表示 u s e r _ r i g h t \mathit {user\_right} user_right,以此类推,但还是那个问题,如果后面遇到的情况更为复杂呢?这样显然不现实。
T
a
b
l
e
\mathit {Table}
Table 也是
M
o
v
e
\mathit {Move}
Move 当中的一个动态数据结构,简单来说就是一张键-值映射表,可以通过
K
e
y
\mathit {Key}
Key 来找到其对应的
V
a
l
u
e
\mathit {Value}
Value。
这里有一个限制,
K
e
y
\mathit {Key}
Key 必须具备
c
o
p
y
+
d
r
o
p
+
s
t
o
r
e
\mathit {copy\ +\ drop\ +\ store}
copy + drop + store 能力,而
V
a
l
u
e
\mathit {Value}
Value 必须具备
s
t
o
r
e
\mathit {store}
store 能力,因为
T
a
b
l
e
\mathit {Table}
Table 当中的键往往需要被复制出来使用,而值则往往需要被存储在对象当中。
一般而言,
M
o
v
e
\mathit {Move}
Move 语言自带的数据类型大部分都可以直接作为键或者值使用,如果有自定义结构的需要,那么不要忘记上述规则。
回到刚刚的问题,只需要将
T
a
b
l
e
\mathit {Table}
Table 和
V
e
c
t
o
r
\mathit {Vector}
Vector 简单结合就可以用来存储我们所需要的数据:Table<String, vector<String>>
2.3 押金
S u i M o v e \mathit {Sui}\ \mathit {Move} Sui Move 提供了一套完善的资金服务,具体可见 C o i n \mathit {Coin} Coin 和 B a l a n c e \mathit {Balance} Balance。
C o i n \mathit {Coin} Coin 就像是一个钱包,而 B a l a n c e \mathit {Balance} Balance 则是该钱包当中的现金余额,后者无法单独存在,必须被保存在某个地方,前者虽然也有 s t o r e \mathit {store} store 能力,理论上讲也可以被存储到自定义的结构当中,但一般不这么做,而是将其中的余额取出存储在自定义的对象当中来进行后续操作。
调用对应的函数方法,就可以将余额从一个地方转移到另一个地方,如果想要将其支付给某个人,只需要将其包装成 C o i n \mathit {Coin} Coin(也有对应的函数可直接调用)再将其所有权转移到对应地址即可,就像 t r a n s f e r \mathit {transfer} transfer 一个 H e l l o W o r l d \mathit {Hello\ World} Hello World 一样简单。
结合上述三点,可以定义如下结构:
struct Game has key {
id: UID,
balance: Balance<SUI>,
hands: Table<String, vector<String>>,
}
2.4 发布者
我们还需要一个能够存储游戏发布者地址的结构,它应该在一开始就被定义,所有人可见但无法被更改,某种程度上就像是执行最后一步(猜拳最终结果)的权杖所在。
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),
});
}
三:代码实现
3.1 创建一局游戏
玩家缴纳押金,给出自己双手究竟出的是石头、剪刀还是布,同时传入0x6
代表clock
所在的地址用作截取当前时间戳,以此来决定对方出手。
每个
V
e
c
t
o
r
\mathit {Vector}
Vector 由于只有两个值,所以在定义的时候直接通过vector[sth1, sth2, ...]
的方式赋值,而
T
a
b
l
e
\mathit {Table}
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));
}
3.2 选择左右手
创建完游戏后可以查看
G
a
m
e
O
b
j
e
c
t
\mathit {Game\ Object}
Game Object 当中双方的出手情况,经过头脑风暴决定自己最后是出左手还是右手,这里还需要传入
G
a
m
e
C
a
p
\mathit {GameCap}
GameCap 来确保玩家输了之后押金可以发送给游戏发布者,同样的,对方的出手依旧由时间戳来决定,最后,不要忘记把需要手动
d
r
o
p
\mathit {drop}
drop 的操作一下。
V
e
c
t
o
r
\mathit {Vector}
Vector 和
T
a
b
l
e
\mathit {Table}
Table 的取值需要用到各自的
b
o
r
r
o
w
\mathit {borrow}
borrow 函数,只不过前者用的是下标
(
i
n
d
e
x
)
\mathit {(index)}
(index),后者用的是键
(
K
e
y
)
\mathit {(Key)}
(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
在成功后得到的信息当中,获取关键的 O b j e c t I D \mathit {Object\ ID} Object ID 设置环境变量,方便后续调用。
export PACKAGE_ID=0x679f4f04bce8d849a1f6488655cd67c9b91c1d5c757bebee8e8ff59ca14311bb
export GAMECAP=0xd265d859652446def1539abb200ce526a9ae4d1593387e96a2672f8c8909220b
切换其他用户(玩家)并分出参加游戏时用的 C o i n \mathit {Coin} 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
在
s
p
l
i
t
\mathit {split}
split-
c
o
i
n
\mathit {coin}
coin 环节(以及后面所有可能的将
C
o
i
n
\mathit {Coin}
Coin 作为参数传递的交易当中),可能会得到余额不足的报错,这是因为你将唯一的
g
a
s
C
o
i
n
I
d
\mathit {gasCoinId}
gasCoinId 用作参数传递(分割的本体),以至于没有其它余额支付这一笔交易了。
想要解决很简单,如果是测试网直接开水就行,如果是主网就用另一个账户给你转一笔账(左手倒右手)。
五:游戏时间
5.1 创建一局游戏
sui client call --package $PACKAGE_ID --module hands_rock_paper_scissors --function create_game --args rock paper $COIN 0x6 --gas-budget 10000000
在得到的信息当中,将
G
a
m
e
\mathit {Game}
Game 这个对象地址也设为环境变量:export GAME=0xb603fe81956368419dab526e5f502bbeb0ccb95380d5c0a4690132900f325864
如果你直接sui client $GAME
会发现,看不到内部存储的
T
a
b
l
e
\mathit {Table}
Table 和
V
e
c
t
o
r
\mathit {Vector}
Vector 具体的值,没关系,不是还有两个类型为0x2::dynamic_field::Field<0x1::string::String, vector<0x1::string::String>>
的对象么?它们一个是你双手的石头剪刀布的情况,一个是对手双手的石头剪刀布的情况,都可以通过sui client object <Object ID>
进行查看。
5.2 选择左右手
一阵头脑风暴过后,决定选择左手,那么:sui client call --package $PACKAGE_ID --module hands_rock_paper_scissors --function choose_hand --args $GAMECAP $GAME left 0x6 --gas-budget 10000000
在后半段信息当中,有一块是
C
r
e
a
t
e
d
O
b
j
e
c
t
s
\mathit {Created\ Objects}
Created Objects,这里新建了一个
C
o
i
n
\mathit {Coin}
Coin<
S
U
I
\mathit {SUI}
SUI>,也就是押金从余额
(
b
a
l
a
n
c
e
)
\mathit {(balance)}
(balance)又变成了钱包
(
c
o
i
n
)
\mathit {(coin)}
(coin),而它的拥有者就是Owner: Account Address
后的地址。
不难发现,这个地址跟我们(玩家)的地址一致,说明在这一局双手石头剪刀布的游戏当中,玩家取得了胜利。
这一点,也可以通过sui client gas
来确认,可以看见有一笔余额为
999
\text {999}
999 的
g
a
s
C
o
i
n
I
d
\mathit {gasCoinId}
gasCoinId 新增了。
5.3 其它
这里给出一局赢和一局输的游戏调用的 h a s h \mathit {hash} hash,有兴趣的可以在浏览器进行查看:
- W I N \mathit {WIN} WIN
- L O S E \mathit {LOSE} LOSE
注意: 本篇所涉及的双手石头剪刀布游戏已发布至主网,有兴趣的欢迎来体验 送钱 ,同时请思考,如果想要在最后得知具体的出手情况,该对代码作何修改?
六:加入组织,共同进步!
- Sui 中文开发群(TG)
- M o v e \mathit{Move} Move 语言学习交流群: 79489587