Sui Move 双手石头剪刀布

一:游戏简介

双手同时给出选择(石头剪刀布),对方同样也给出自己的选择,这个时候出拳情况是透明的(玩家可见),经过分析后选择自己的左手或者右手作为最终的选择,和对方的抉择进行比较,如果玩家赢了,那么押金就原封不动退回,如果玩家输了(平局也算输),押金就归对方(也就是此游戏的发布者)所有。

二:结构设计

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,有兴趣的可以在浏览器进行查看:

注意: 本篇所涉及的双手石头剪刀布游戏已发布至主网,有兴趣的欢迎来体验 送钱 ,同时请思考,如果想要在最后得知具体的出手情况,该对代码作何修改?

六:加入组织,共同进步!

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值