Previous << Day10 - マリガンおよびゲーム開始
Next >> Day12 - 勝敗と賞金送付、ランキング
Why Flow
FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。
ということでやっていきます、猿でも分かるP2P(ピアツーピア)決済アプリ開発!
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。
カードを場に出す、攻撃、防御アクション
カードゲームにおいて、カードを場に出す、攻撃、防御アクションはゲームの勝敗を決める大事なものですが、そこはゲームのロジックの内容を書く必要があり、ソースコードが非常に長くなります。フルオンチェーンのカードゲームは作るのは面白いですが。。そのため、今回は割愛致します。
こちらのコードに興味のある方はこちらからスマートコントラクトを見ることができます。
攻撃ターンを交代する
自分のターンを終わらせて、相手にターンを譲ります。
ターンが変わるときに新しいカードを2枚引きます。そこで乱数生成関数revertibleRandom
を使います。revertibleRandom
は、整数を与えると、0~(整数-1)
までの乱数を生成してくれる非常に使い勝手のいいBuilt-In関数です。
randomは0~29の値になります。
let modulo: UInt8 = 30
let random = revertibleRandom(modulo: modulo)
攻撃ターンを交代するロジックは以下のように実装しました。
:
access(all) struct BattleStruct {
:
access(contract) fun set_is_first_turn(new_value: Bool) {
self.is_first_turn = new_value
}
:
}
:
/*
** [Resource] Admin (Game Server Processing)
*/
access(all) resource Admin {
:
access(all) fun turn_change(player_id: UInt, from_opponent: Bool, trigger_cards: {UInt8: UInt16}) {
if let info = AwesomeCardGame.battleInfo[player_id] {
/* Check is turn already changed. */
if info.is_first != info.is_first_turn {
return;
}
/** 今回はバトルはしないので、以下部分は割愛
if (info.your_attacking_card != nil && info.your_attacking_card!.attacked_time + 20.0 > info.last_time_turnend!) {
:
**/
/* トリガーゾーンのカードを合わせる */
for position in trigger_cards.keys {
/* セット済みは除外 */
if info.your_trigger_cards[position] != trigger_cards[position] {
/* ハンドの整合性を合わせる(トリガーゾーンに移動した分、ハンドから取る) */
var isRemoved = false
if info.your_trigger_cards[position] != trigger_cards[position] && trigger_cards[position] != 0 {
let card_id = trigger_cards[position]
info.your_trigger_cards[position] = card_id
for hand_position in info.your_hand.keys {
if card_id == info.your_hand[hand_position] && isRemoved == false {
info.your_hand[hand_position] = nil
isRemoved = true
}
}
if (isRemoved == false) {
panic("You set the card on trigger zone which is not exist in your hand")
}
}
}
}
var handCnt = 0
let handPositions: [UInt8] = [1, 2, 3, 4, 5 ,6, 7]
for hand_position in handPositions {
if info.your_hand[hand_position] != nil {
handCnt = handCnt + 1
}
}
/** 今回はバトルはしないので、以下部分は割愛
for position in info.your_field_unit.keys {
:
**/
/* Process Turn Change */
info.set_last_time_turnend(new_value: getCurrentBlock().timestamp)
info.set_is_first_turn(new_value: !info.is_first_turn)
if (info.is_first_turn) {
info.set_turn(new_value: info.turn + 1)
}
let opponent = info.opponent
if let infoOpponent = AwesomeCardGame.battleInfo[opponent] {
/* Turn Change */
infoOpponent.set_last_time_turnend(new_value: info.last_time_turnend!)
infoOpponent.set_is_first_turn(new_value: !infoOpponent.is_first_turn)
infoOpponent.set_turn(new_value: info.turn)
/** 今回はバトルはしないので、以下部分は割愛
infoOpponent.opponent_hand = handCnt
:
**/
/* draw card */
let cardRemainCounts = infoOpponent.your_remain_deck.length
/* 乱数生成関数`revertibleRandom`を使います。 */
let modulo: UInt8 = cardRemainCounts as! UInt8
let withdrawPosition1 = revertibleRandom(modulo: modulo - 1)
let withdrawPosition2 = revertibleRandom(modulo: modulo - 2)
var isSetCard1 = false
var isSetCard2 = false
var handCnt2 = 0
let handPositions: [UInt8] = [1, 2, 3, 4, 5 ,6, 7]
let nextPositions: [UInt8] = [1, 2, 3, 4, 5 ,6]
/* カード位置を若い順に整列 */
for hand_position in handPositions {
var replaced: Bool = false
if infoOpponent.your_hand[hand_position] == nil {
for next in nextPositions {
if replaced == false && hand_position + next <= 7 && infoOpponent.your_hand[hand_position + next] != nil {
infoOpponent.your_hand[hand_position] = infoOpponent.your_hand[hand_position + next]
infoOpponent.your_hand[hand_position + next] = nil
replaced = true
}
}
}
}
for hand_position in handPositions {
if infoOpponent.your_hand[hand_position] == nil && isSetCard1 == false {
infoOpponent.your_hand[hand_position] = infoOpponent.your_remain_deck.remove(at: withdrawPosition1)
isSetCard1 = true
}
if infoOpponent.your_hand[hand_position] == nil && isSetCard2 == false {
infoOpponent.your_hand[hand_position] = infoOpponent.your_remain_deck.remove(at: withdrawPosition2)
isSetCard2 = true
}
if infoOpponent.your_hand[hand_position] != nil {
handCnt2 = handCnt2 + 1
}
}
/** 今回はバトルはしないので、以下部分は割愛
infoOpponent.your_field_unit_bp_amount_of_change = {} /* Damage are reset */
:
**/
AwesomeCardGame.battleInfo[opponent] = infoOpponent
}
/* save */
AwesomeCardGame.battleInfo[player_id] = info
}
/* judge the winner */
// self.judgeTheWinner(player_id: player_id)
}
}
:
turn_change
関数はAdmin
リソースの中に配置します。
AwesomeCardGame
コントラクトを以下コマンドで更新します。
flow accounts update-contract ./AwesomeCardGame.cdc
攻撃ターンを交代する
次にバックエンドでNode.jsで実行するjsファイルを作っていきます。ここでは/backend
フォルダの中のファイルはサーバーサイドで実行するものと仮定します。
トランザクションを実行するときDictionary
の値も送れるのですが送る方法が少し独特です。
例えば、{1: 10, 2: 20, 3: 30, 4: 40}
という値をトランザクションで送信したいとします。
その値を送る方法はこうです。
message = {1: 10, 2: 20, 3: 30, 4: 40};
/* 値が無かった時のfallback値は0とする。 */
const trigger_cards = [
{ key: 1, value: message["1"] || 0 },
{ key: 2, value: message["2"] || 0 },
{ key: 3, value: message["3"] || 0 },
{ key: 4, value: message["4"] || 0 },
];
:
:
args: (arg, t) => [
arg(player_id, t.UInt),
arg(false, t.Bool),
arg(trigger_cards, t.Dictionary({ key: t.UInt8, value: t.UInt16 })),
],
Dictionaryのkeyはkey
というキーに対するvalueになります。なので私はトランザクションコードではkeyを固定にしていました。Object.keysとかを使ってやると、固定にしない方法もあると思いますが。。。
send_change_turn.js
ファイルを作成して、Non-Custodial方式のトランザクションを書きます。
import fcl from "@onflow/fcl";
import { SHA3 } from "sha3";
import pkg from "elliptic";
const { ec } = pkg;
import { argv } from "node:process";
var player_id;
var set_cards;
argv.forEach((val, index) => {
if (index == 2) {
player_id = val;
} else if (index == 3) {
set_cards = JSON.parse(val);
}
});
fcl
.config()
.put("flow.network", "emulator")
.put("accessNode.api", "http://localhost:8888");
try {
var KEY_ID_IT = 0;
// 以下はProposerのKeyが300ある場合を想定したものなのでエミュレータでは0とする
// if (fs.existsSync("/tmp/sequence.txt")) {
// KEY_ID_IT = parseInt(
// fs.readFileSync("/tmp/sequence.txt", { encoding: "utf8" })
// );
// } else {
// KEY_ID_IT = new Date().getMilliseconds() % 300;
// }
// KEY_ID_IT = !KEY_ID_IT || KEY_ID_IT >= 300 ? 1 : KEY_ID_IT + 1;
// fs.writeFileSync("/tmp/sequence.txt", KEY_ID_IT.toString());
const ec_ = new ec("p256");
/* CHANGE THESE THINGS FOR YOU */
/* 注意! これはエミュレータのプライベートキーなので、テストネットやメインエットに影響がないのでコードに埋めているが、本来は決して行ってはならない */
const PRIVATE_KEY = `9ba63c9cd20a8214bcd8178b6d65d6cb54725670bba95a56f30d3bb1de9baaf4`;
const ADDRESS = "0xf8d6e0586b0a20c7";
const KEY_ID = 0;
const sign = (message) => {
const key = ec_.keyFromPrivate(Buffer.from(PRIVATE_KEY, "hex"));
const sig = key.sign(hash(message)); // hashMsgHex -> hash
const n = 32;
const r = sig.r.toArrayLike(Buffer, "be", n);
const s = sig.s.toArrayLike(Buffer, "be", n);
return Buffer.concat([r, s]).toString("hex");
};
const hash = (message) => {
const sha = new SHA3(256);
sha.update(Buffer.from(message, "hex"));
return sha.digest();
};
async function authorizationFunction(account) {
return {
...account,
tempId: `${ADDRESS}-${KEY_ID}`,
addr: fcl.sansPrefix(ADDRESS),
keyId: Number(KEY_ID),
signingFunction: async (signable) => {
return {
addr: fcl.withPrefix(ADDRESS),
keyId: Number(KEY_ID),
signature: sign(signable.message),
};
},
};
}
async function authorizationFunctionProposer(account) {
return {
...account,
tempId: `${ADDRESS}-${KEY_ID_IT}`,
addr: fcl.sansPrefix(ADDRESS),
keyId: Number(KEY_ID_IT),
signingFunction: async (signable) => {
return {
addr: fcl.withPrefix(ADDRESS),
keyId: Number(KEY_ID_IT),
signature: sign(signable.message),
};
},
};
}
console.log(player_id, set_cards, KEY_ID, KEY_ID_IT);
const trigger_cards = [
{ key: 1, value: set_cards["1"] || 0 },
{ key: 2, value: set_cards["2"] || 0 },
{ key: 3, value: set_cards["3"] || 0 },
{ key: 4, value: set_cards["4"] || 0 },
];
let transactionId = await fcl.mutate({
cadence: `
import AwesomeCardGame from 0xf8d6e0586b0a20c7
transaction(player_id: UInt, from_opponent: Bool, trigger_cards: {UInt8: UInt16}) {
prepare(signer: auth(BorrowValue) &Account) {
let admin = signer.storage.borrow<&AwesomeCardGame.Admin>(from: /storage/AwesomeCardGameAdmin)
?? panic("Could not borrow reference to the Administrator Resource.")
admin.turn_change(player_id: player_id, from_opponent: from_opponent, trigger_cards: trigger_cards)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [
arg(player_id, t.UInt),
arg(false, t.Bool),
arg(trigger_cards, t.Dictionary({ key: t.UInt8, value: t.UInt16 })), // trigger_cards
],
proposer: authorizationFunctionProposer,
payer: authorizationFunction,
authorizations: [authorizationFunction],
limit: 999,
});
console.log(`TransactionId: ${transactionId}`);
fcl.tx(transactionId).subscribe((res) => {
console.log(res);
});
} catch (error) {
console.error(error);
}
ターン交代処理をする
プレイヤーが画面上でTurn End
ボタンを押したとします。
それをバックエンドで受け取って実行すると仮定します。コマンドは以下とします。
node ../backend/send_change_turn.js 1 "{}"
ではplayer_id=1(emulator_account)の画面を表示します。
is_first_turn
がfalse
であればターンが後攻ですのでターンが変わっています。
次に、後攻のプレイヤーもturn_change
トランザクションを実行してみます。コマンドは以下とします。
node ../backend/send_change_turn.js 2 "{}"
is_first_turn
がtrue
ですので先攻に変わっています。また、ターンが2ターン目になっています。
Conclusion:
Cadenceスマートコントラクトの組み込み関数、revertibleRandom
乱数生成関数は非常に使い勝手が良く、開発の生産性が格段に上がります。
もっと高度なGraphQLによる対戦相手と情報を共有しながらトランザクションを実施したいケースの場合は、こちらが参考になります。(本は薄めですが、無駄を省いてピンポイントでコードを参照出来るようにする為です、レビュー滅茶甘めで付けていただけると喜びます😭)
この記事のソースコードはこちらにあります。
Previous << Day10 - マリガンおよびゲーム開始