用SmartPy进行高级Tezos智能合约开发:多重签名合约和游戏开发
本课程是我们SmartPy学习之旅的第三部分,探讨了多签名智能合约、游戏开发和Tezos智能合约的高级功能等重要知识点。该课程旨在提供关于SmartPy及其高级应用的实际知识,以解决区块链系统中的复杂问题。
多重签名合约入门
多重签名(多签)合约,也被称为“M-of-N”合约,是一种用于增加区块链环境中交易的安全性和灵活性的关键机制,通过要求在交易执行之前获得多方批准来改变对资产的控制方式。“M-of-N”指的是N个当事方中必须有M个当事方批准交易才能使其有效。
多重签名合约原理
多重签名合约提供了一种创建对资产的共享控制的方法。典型的用例包括托管服务、企业账户管理、共同签署财务协议等。这些合约尤其适用于需要集体决策的组织或团体。
多重签名合约的设计理念是防篡改,并能够防止单点故障。即使一方的密钥被泄露,攻击者也无法在未经其他方批准的情况下执行交易,从而提高了安全性。
多重签名合约可以被认为是保险箱的数字等价物,需要多个密钥才能打开。在创建合约时,协议规定了密钥总数(N)和打开保险箱所需的最小密钥数(M)。
多重签名合约可以有许多不同的配置,具体取决于M和N的值:
1-of-N:总数中的一方可以批准交易。此配置与没有多重签名的普通交易相同。为方便起见,它可能用于存在多个密钥的情况,但任何一个密钥都可以批准交易。
N-of-N:所有各方都必须批准交易。此配置具有最高级别的安全性,但如果一方丢失密钥或拒绝批准交易,可能会出现问题。
M-of-N(其中M<N):总数中的M方必须批准交易。这种配置是实践中经常使用的一种,因为它在安全性和灵活性之间取得了平衡。
区块链中的多重签名合约
在区块链中,多重签名合约被广泛用于增强交易安全性、支持复杂的治理机制或灵活控制区块链资产。其中一些示例包括:
钱包:多重签名钱包用于保护资产安全。它们需要多方签署交易,从而提供了额外的安全性,防止了盗窃、外部黑客攻击和内部威胁。
去中心化自治组织(DAO):DAO经常使用多重签名合约来执行其治理规则。针对提案的投票以多重签名交易的方式执行,DAO成员充当签署者。只有在提案获得足够的票数时才会执行。
跨链操作:在跨链操作中,多重签名合约可以充当资产的托管方。当资产从一个区块链移动到另一个区块链时,起始链上的多重签名合约可以确保资产被安全锁定,直到在另一个链上的操作得到确认。
尽管多重签名合约的实现在不同的区块链上可能有所不同,但核心概念保持不变:在执行交易之前需要多方批准。这一额外的安全层使多重签名合约成为区块链和加密货币领域的重要工具。
代码示例:使用SmartPy编写和部署多重签名合约
我们将通过代码演示三种不同的多重签名合约实现:
Lambda合约
Lambda合约非常灵活,具有广泛的用途,需要多个签名才能执行任意lambda函数。
Python
import smartpy as sp
@sp.module
def main():
operation_lambda: type = sp.lambda_(sp.unit, sp.unit, with_operations=True)
class MultisigLambda(sp.Contract):
"""Multiple members vote for executing lambdas.
This contract can be originated with a list of addresses and a number of
required votes. Any member can submit as much lambdas as he wants and vote
for active proposals. When a lambda reaches the required votes, its code is
called and the output operations are executed. This allows this contract to
do anything that a contract can do: transferring tokens, managing assets,
administrating another contract...
When a lambda is applied, all submitted lambdas until now are inactivated.
The members can still submit new lambdas.
"""
def __init__(self, members, required_votes):
"""Constructor
Args:
members (sp.set of sp.address): people who can submit and vote
for lambda.
required_votes (sp.nat): number of votes required
"""
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.lambdas = sp.cast(
sp.big_map(), sp.big_map[sp.nat, operation_lambda]
)
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
self.data.nextId = 0
self.data.inactiveBefore = 0
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_lambda(self, lambda_):
"""Submit a new lambda to the vote.
Submitting a proposal does not imply casting a vote in favour of it.
Args:
lambda_(sp.lambda with operations): lambda proposed to vote.
Raises:
`You are not a member`
"""
assert self.data.members.contains(sp.sender), "You are not a member"
self.data.lambdas[self.data.nextId] = lambda_
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote_lambda(self, id):
"""Vote for a lambda.
Args:
id(sp.nat): id of the lambda to vote for.
Raises:
`You are not a member`, `The lambda is inactive`, `Lambda not found`
There is no vote against or pass. If someone disagrees with a lambda
they can avoid to vote.
"""
assert self.data.members.contains(sp.sender), "You are not a member"
assert id >= self.data.inactiveBefore, "The lambda is inactive"
assert self.data.lambdas.contains(id), "Lambda not found"
self.data.votes[id].add(sp.sender)
if sp.len(self.data.votes[id]) >= self.data.required_votes:
self.data.lambdas[id]()
self.data.inactiveBefore = self.data.nextId
@sp.onchain_view()
def get_lambda(self, id):
"""Return the corresponding lambda.
Args:
id (sp.nat): id of the lambda to get.
Return:
pair of the lambda and a boolean showing if the lambda is active.
"""
return (self.data.lambdas[id], id >= self.data.inactiveBefore)
# if "templates" not in __name__:
@sp.module
def test():
class Administrated(sp.Contract):
def __init__(self, admin):
self.data.admin = admin
self.data.value = sp.int(0)
@sp.entrypoint
def set_value(self, value):
assert sp.sender == self.data.admin
self.data.value = value
@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario():
"""Use the multisigLambda as an administrator of an example contract.
Tests:
- Origination
- Lambda submission
- Lambda vote
"""
sc = sp.test_scenario([main, test])
sc.h1("Basic scenario.")
member1 = sp.test_account("member1")
member2 = sp.test_account("member2")
member3 = sp.test_account("member3")
members = sp.set([member1.address, member2.address, member3.address])
sc.h2("MultisigLambda: origination")
c1 = main.MultisigLambda(members, 2)
sc += c1
sc.h2("Administrated: origination")
c2 = test.Administrated(c1.address)
sc += c2
sc.h2("MultisigLambda: submit_lambda")
def set_42(params):
administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())
lambda_ = sp.build_lambda(set_42, with_operations=True)
c1.submit_lambda(lambda_).run(sender=member1)
sc.h2("MultisigLambda: vote_lambda")
c1.vote_lambda(0).run(sender=member1)
c1.vote_lambda(0).run(sender=member2)
# We can check that the administrated contract received the transfer.
sc.verify(c2.data.value == 42)
多重签名行动(MultisigAction)合约
多重签名行动合约引入了提案投票的概念。在此合约中,签署者可以对要执行的某些操作进行投票,如果达到要求的票数,则执行提议的操作。
Python
import smartpy as sp
@sp.module
def main():
# Internal administration action type specification
InternalAdminAction: type = sp.variant(
addSigners=sp.list[sp.address],
changeQuorum=sp.nat,
removeSigners=sp.list[sp.address],
)
class MultisigAction(sp.Contract):
"""A contract that can be used by multiple signers to administrate other
contracts. The administrated contracts implement an interface that make it
possible to explicit the administration process to non expert users.
Signers vote for proposals. A proposal is a list of a target with a list of
action. An action is a simple byte but it is intended to be a pack value of
a variant. This simple pattern make it possible to build a UX interface
that shows the content of a proposal or build one.
"""
def __init__(self, quorum, signers):
self.data.inactiveBefore = 0
self.data.nextId = 0
self.data.proposals = sp.cast(
sp.big_map(),
sp.big_map[
sp.nat,
sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
],
)
self.data.quorum = sp.cast(quorum, sp.nat)
self.data.signers = sp.cast(signers, sp.set[sp.address])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
@sp.entrypoint
def send_proposal(self, proposal):
"""Signer-only. Submit a proposal to the vote.
Args:
proposal (sp.list of sp.record of target address and action): List\
of target and associated administration actions.
"""
assert self.data.signers.contains(sp.sender), "Only signers can propose"
self.data.proposals[self.data.nextId] = proposal
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote(self, pId):
"""Vote for one or more proposals
Args:
pId (sp.nat): Id of the proposal.
"""
assert self.data.signers.contains(sp.sender), "Only signers can vote"
assert self.data.votes.contains(pId), "Proposal unknown"
assert pId >= self.data.inactiveBefore, "The proposal is inactive"
self.data.votes[pId].add(sp.sender)
if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum:
self._onApproved(pId)
@sp.private(with_storage="read-write", with_operations=True)
def _onApproved(self, pId):
"""Inlined function. Logic applied when a proposal has been approved."""
proposal = self.data.proposals.get(pId, default=[])
for p_item in proposal:
contract = sp.contract(sp.list[sp.bytes], p_item.target)
sp.transfer(
p_item.actions,
sp.tez(0),
contract.unwrap_some(error="InvalidTarget"),
)
# Inactivate all proposals that have been already submitted.
self.data.inactiveBefore = self.data.nextId
@sp.entrypoint
def administrate(self, actions):
"""Self-call only. Administrate this contract.
This entrypoint must be called through the proposal system.
Args:
actions (sp.list of sp.bytes): List of packed variant of \
`InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
"""
assert (
sp.sender == sp.self_address()
), "This entrypoint must be called through the proposal system."
for packed_actions in actions:
action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
error="Bad actions format"
)
with sp.match(action):
with sp.case.changeQuorum as quorum:
self.data.quorum = quorum
with sp.case.addSigners as added:
for signer in added:
self.data.signers.add(signer)
with sp.case.removeSigners as removed:
for address in removed:
self.data.signers.remove(address)
# Ensure that the contract never requires more quorum than the total of signers.
assert self.data.quorum <= sp.len(
self.data.signers
), "More quorum than signers."
if "templates" not in __name__:
@sp.add_test(name="Basic scenario", is_default=True)
def test():
signer1 = sp.test_account("signer1")
signer2 = sp.test_account("signer2")
signer3 = sp.test_account("signer3")
s = sp.test_scenario(main)
s.h1("Basic scenario")
s.h2("Origination")
c1 = main.MultisigAction(
quorum=2,
signers=sp.set([signer1.address, signer2.address]),
)
s += c1
s.h2("Proposal for adding a new signer")
target = sp.to_address(
sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
)
action = sp.pack(
sp.set_type_expr(
sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
)
)
c1.send_proposal([sp.record(target=target, actions=[action])]).run(
sender=signer1
)
s.h2("Signer 1 votes for the proposal")
c1.vote(0).run(sender=signer1)
s.h2("Signer 2 votes for the proposal")
c1.vote(0).run(sender=signer2)
s.verify(c1.data.signers.contains(signer3.address))
多重签名视图(MultisigView)合约
多重签名视图合约也采用了一个投票机制。该合约允许成员提交并投票支持任意字节。一旦提案获得所需的票数,就可以通过视图确认其状态。
Python
import smartpy as sp
@sp.module
def main():
class MultisigView(sp.Contract):
"""Multiple members vote for arbitrary bytes.
This contract can be originated with a list of addresses and a number of
required votes. Any member can submit as many bytes as they want and vote
for active proposals.
Any bytes that reached the required votes can be confirmed via a view.
"""
def __init__(self, members, required_votes):
"""Constructor
Args:
members (sp.set of sp.address): people who can submit and vote for
lambda.
required_votes (sp.nat): number of votes required
"""
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
)
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_proposal(self, bytes):
"""Submit a new proposal to the vote.
Submitting a proposal does not imply casting a vote in favour of it.
Args:
bytes(sp.bytes): bytes proposed to vote.
Raises:
`You are not a member`
"""
assert self.data.members.contains(sp.sender), "You are not a member"
self.data.proposals[bytes] = False
self.data.votes[bytes] = sp.set()
@sp.entrypoint
def vote_proposal(self, bytes):
"""Vote for a proposal.
There is no vote against or pass. If one disagrees with a proposal they
can avoid to vote. Warning: old non-voted proposals never become
obsolete.
Args:
id(sp.bytes): bytes of the proposal.
Raises:
`You are not a member`, `Proposal not found`
"""
assert self.data.members.contains(sp.sender), "You are not a member"
assert self.data.proposals.contains(bytes), "Proposal not found"
self.data.votes[bytes].add(sp.sender)
if sp.len(self.data.votes[bytes]) >= self.data.required_votes:
self.data.proposals[bytes] = True
@sp.onchain_view()
def is_voted(self, id):
"""Returns a boolean indicating whether the proposal has been voted on.
Args:
id (sp.bytes): bytes of the proposal
Return:
(sp.bool): True if the proposal has been voted, False otherwise.
"""
return self.data.proposals.get(id, error="Proposal not found")
if "templates" not in __name__:
@sp.add_test(name="MultisigView basic scenario", is_default=True)
def basic_scenario():
"""A scenario with a vote on the multisigView contract.
Tests:
- Origination
- Proposal submission
- Proposal vote
"""
sc = sp.test_scenario(main)
sc.h1("Basic scenario.")
member1 = sp.test_account("member1")
member2 = sp.test_account("member2")
member3 = sp.test_account("member3")
members = sp.set([member1.address, member2.address, member3.address])
sc.h2("Origination")
c1 = main.MultisigView(members, 2)
sc += c1
sc.h2("submit_proposal")
c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)
sc.h2("vote_proposal")
c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)
# We can check that the proposal has been validated.
sc.verify(c1.is_voted(sp.bytes("0x42")))
在以上三个合约中,每个合约都采用了不同的机制来实现多重签名控制,能够灵活地满足你的区块链用例的具体需求。
如何通过SmartPy在线平台运行多重签名合约
要运行我们用SmartPy编写的多重签名合约,可以按照以下步骤操作:
进入SmartPy IDE,网址https://smartpy.io/ide。
将合约代码粘贴到编辑器中。你可以替换掉原有的代码。
要运行合约,请单击顶部面板上的“Run”按钮。
运行合约后,你可以在右侧的“Output”面板中查看执行情况。此处将显示每个操作的详细信息,包括提案、投票和批准。
要在Tezos网络上部署合约,你首先需要对其进行编译,只需单击顶部面板上的“Compile”按钮。
编译完成后,你可以单击“Deploy Michelson Contract”按钮,将合约部署到测试网上。你需要提供一个具有足够资金支付部署的gas费用的Tezos账户的密钥。
合约部署完成后,你将获得区块链上合约的地址。你可以使用此地址进行交易,实现与合约的交互。
要在合约中提交提案或投票,你可以使用合约代码中定义的入口点,如submit_proposal 或 vote_proposal。这些入口点可以直接从你创建的交易中调用。
需要注意的是,虽然我们可以通过SmartPy IDE在模拟区块链上测试合约,但将合约部署到实际的Tezos网络将产生gas费,必须使用Tezos网络的原生加密货币XTZ支付。
多重签名合约详解
我们将深入探讨三种不同的多重签名合约的结构,包括Lambda合约、多重签名行动合约和多重签名视图合约。这些合约在区块链和去中心化金融领域发挥着关键作用,对于我们理解多重签名合约至关重要。我们将对代码进行详细拆解、对其独特特性进行阐述,并对实际应用和用例进行讨论。
多重签名Lambda合约
多重签名Lambda合约使用了sp.lambda类型,是智能合约编程中的一个重大飞跃。它可以执行合约部署时未预先确定的任意函数。该合约主要包含以下部分:
初始参数:对合约进行初始化(__init__),其中包含允许提议和签署提案的成员列表,以及执行提案所需的最小签名数。这些参数存储在合约的存储空间(self.data)中,为合约的运行创建了参考记录。
Propose函数:Propose函数用于创建新的提议。成员可以提交他们希望合约来执行的lambda函数(包含任意操作)。这些提议存储在big_map数据结构中,每个提案都有一个唯一的标识符。
Sign函数:Sign函数用于对提案进行背书。成员可以签名以支持提案。这些签名附有唯一的提案标识符,被收集并保存在单独的big_map中。
Execute函数:Execute函数是合约运行的关键。如果提案已获得所需数量的签名,成员可以调用此函数来执行提议的lambda函数,将其应用于合约的存储。
多重签名Lambda合约具有广泛的通用性,适用于需要复杂、灵活和潜在动态控制结构的场景,如DAO(去中心化自治组织)、具有多个所有者的钱包服务和复杂的DeFi协议。
该合约需要执行具有多个签名的任意lambda函数。它引入了提交和签署lambda函数的概念。我们来详细介绍一下它的各个函数:
submit_lambda:此函数允许成员向合约提交lambda函数。这实质上是提出需要多重签名批准的合约操作。在SmartPy IDE的右侧面板中,成功提交lambda函数后,你将能够看到发送者地址向合约地址发送的新交易。

vote_lambda:此函数允许成员投票(签名)支持提交的lambda函数。投票后,你将在IDE面板中看到从成员地址发送到合约地址的新交易。

多重签名行动合约
多重签名行动合约将民主投票机制引入智能合约领域。在该模型中,成员提出具体的行动,对其进行投票,并在达到规定人数后执行。这一合约将人类共识与自动合约执行独特地结合在一起,对合约行为实施了民主控制。
本合约的核心组成部分包括:
初始参数:与Lambda合约一样,多重签名行动合约在初始化时包含一份成员列表和所需票数。
Propose Action函数:此函数用于添加新的提议。成员可以提交合约中预定义的行动,并将其与唯一标识符相关联。这些提议的行动存储在big_map中。
Vote Action函数:此函数用于投票支持提议的行动。投票与提案的唯一标识符相关联,并保存在不同的big_map中。
Execute Action函数:当提案获得足够的票数时,该函数就会发挥作用。成员可以调用此函数来执行所提议的行动。
多重签名行动合约非常适合需要团体就特定合约行动达成共识的情况,如在DAO中,成员对资源分配或协议更改进行投票。
submit_proposal:这是提出新行动的过程。比如在我们的合约示例中,一名成员提议向合约添加新的签署者。
在SmartPy IDE上执行此操作时,你将看到在IDE的右侧面板中创建了一个新交易。交易摘要将显示发送者(提议成员的地址)和接收者(合约地址)。

这里,“OK”状态显示提案已成功提交。
vote_proposal:下一步便是对提案进行投票。该函数使现有签署者能够对上一步提交的提案进行投票。
当签署者1投票支持提案时,你将看到一个新的交易,签署者1作为发送者,合约作为接收者。当签署者2投票支持提案时,将记录类似的交易:

这些交易表明签署者1和签署者2都成功为该提案投了票。
多重签名视图合约
多重签名视图合约推进了民主共识的概念,但将其应用于任意字节而不是预定义的合约操作。该合约开辟了在表示为字节的数据上达成共识的途径,而不会立即导致行动执行。
多重签名视图合约的主要组成部分包括:
初始参数:与前两个合约一样,多重签名视图合约在初始化时带有成员列表和表示所需投票数量的数字。
Submit Proposal函数:此函数使成员能够以任意字节的形式引入新的提案。字节存储在big_map中,由字节本身索引,表示唯一的提案标识符。
Vote Proposal函数:此函数允许成员投票支持提案。投票记录在一个单独的big_map中,与提案的唯一字节标识符相关联。

Is Voted函数:此函数是一个链上视图,检查提案是否已获得所需的投票。它返回一个布尔值,指示提案的投票状态。
多重签名视图合约适用于智能合约需要维护已批准的提案记录的情况,如在加密应用中,字节可能代表散列协议、证明或任何其他需要集体批准的数据形式。
在Tezos上开发井字棋游戏
区块链游戏世界为开发者提供了诸多机会。它提供了一种独特而创新的方式,将去中心化和透明的机制整合到游戏中。通过在区块链上开发游戏,我们可以整合安全透明的交易、游戏内资产的所有权等功能。在本课中,我们将在Tezos区块链上开发经典的井字棋游戏,开启区块链游戏领域的开发之旅,帮助大家了解区块链游戏的游戏逻辑和状态管理。
首先,我们来具体分析一下这个井字棋游戏合约:
合约结构
Python
# TicTacToe - Example for illustrative purposes only.
import smartpy as sp
@sp.module
def main():
class TicTacToe(sp.Contract):
def __init__(self):
self.data.nbMoves = 0
self.data.winner = 0
self.data.draw = False
self.data.deck = {
0: {0: 0, 1: 0, 2: 0},
1: {0: 0, 1: 0, 2: 0},
2: {0: 0, 1: 0, 2: 0},
}
self.data.nextPlayer = 1
@sp.entrypoint
def play(self, params):
assert self.data.winner == 0 and not self.data.draw
assert params.i >= 0 and params.i < 3
assert params.j >= 0 and params.j < 3
assert params.move == self.data.nextPlayer
assert self.data.deck[params.i][params.j] == 0
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1
self.data.nextPlayer = 3 - self.data.nextPlayer
self.data.winner = self.checkLine(
sp.record(winner=self.data.winner, line=self.data.deck[params.i])
)
self.data.winner = self.checkLine(
sp.record(
winner=self.data.winner,
line={
0: self.data.deck[0][params.j],
1: self.data.deck[1][params.j],
2: self.data.deck[2][params.j],
},
)
)
self.data.winner = self.checkLine(
sp.record(
winner=self.data.winner,
line={
0: self.data.deck[0][0],
1: self.data.deck[1][1],
2: self.data.deck[2][2],
},
)
)
self.data.winner = self.checkLine(
sp.record(
winner=self.data.winner,
line={
0: self.data.deck[0][2],
1: self.data.deck[1][1],
2: self.data.deck[2][0],
},
)
)
if self.data.nbMoves == 9 and self.data.winner == 0:
self.data.draw = True
@sp.private()
def checkLine(self, winner, line):
winner_ = winner
if line[0] != 0 and line[0] == line[1] and line[0] == line[2]:
winner_ = line[0]
return winner_
# Add a game reset function
@sp.entrypoint
def confirm_and_reset(self):
assert self.data.winner != 0 or self.data.draw
self.__init__()
# Tests
if "templates" not in __name__:
@sp.add_test(name="TicTacToe")
def test():
scenario = sp.test_scenario(main)
scenario.h1("Tic-Tac-Toe")
# define a contract
c1 = main.TicTacToe()
# show its representation
scenario.h2("A sequence of interactions with a winner")
scenario += c1
scenario.h2("Message execution")
scenario.h3("A first move in the center")
c1.play(i=1, j=1, move=1)
scenario.h3("A forbidden move")
c1.play(i=1, j=1, move=2).run(valid=False)
scenario.h3("A second move")
c1.play(i=1, j=2, move=2)
scenario.h3("Other moves")
c1.play(i=2, j=1, move=1)
c1.play(i=2, j=2, move=2)
scenario.verify(c1.data.winner == 0)
c1.play(i=0, j=1, move=1)
scenario.verify(c1.data.winner == 1)
scenario.p("Player1 has won")
c1.play(i=0, j=0, move=2).run(valid=False)
c2 = main.TicTacToe()
scenario.h2("A sequence of interactions with a draw")
scenario += c2
scenario.h2("Message execution")
scenario.h3("A first move in the center")
c2.play(i=1, j=1, move=1)
scenario.h3("A forbidden move")
c2.play(i=1, j=1, move=2).run(valid=False)
scenario.h3("A second move")
c2.play(i=1, j=2, move=2)
scenario.h3("Other moves")
c2.play(i=2, j=1, move=1)
c2.play(i=2, j=2, move=2)
c2.play(i=0, j=0, move=1)
c2.play(i=0, j=1, move=2)
c2.play(i=0, j=2, move=1)
c2.play(i=2, j=0, move=2)
c2.play(i=1, j=0, move=1)
# Add tests for game reset
scenario.h2("Testing game reset")
scenario.p("Winner or draw confirmed, now resetting the game")
c1.confirm_and_reset()
scenario.verify(c1.data.nbMoves == 0)
scenario.verify(c1.data.winner == 0)
scenario.verify(not c1.data.draw)
c2.confirm_and_reset()
scenario.verify(c2.data.nbMoves == 0)
scenario.verify(c2.data.winner == 0)
scenario.verify(not c2.data.draw)
我们在Tezos上的井字棋游戏合约是用SmartPy语言编写的。它包含两个主要部分组成:合约状态和游戏逻辑。
合约状态
合约的状态通过**init函数初始化。它包括:
nbMoves:这是游戏中移动次数的计数器。初始值为零。
winner:此变量用于跟踪游戏的获胜者。初始值为零,表示没有获胜者。
draw:指示游戏是否以平局结束的标志。初始状态为False(假)。
deck:这是一个3x3的网格,代表井字棋棋盘。棋盘上的所有点最初都为空,用零表示。
nextPlayer:表示轮到哪个玩家下棋。游戏从玩家1开始,所以最初设置为1。
游戏逻辑
游戏逻辑包含在play函数中。它会执行多项检查以确保有效的移动:
确认没有玩家赢得比赛,游戏也没有以平局结束。
验证玩家选择的网格位置的索引是否在网格的边界内。
确保正在下棋的玩家与nextPlayer匹配。
确保网格上选择的位置为空。
一旦落棋,游戏逻辑将会递增nbMoves,切换nextPlayer,并检查这一步棋是否导致胜利或平局。
胜利条件将在最新棋步所在的行、列以及两个对角线上进行检查。
如果棋盘上的所有点都被填满且没有玩家获胜(即nbMoves等于9且winner仍然为0),则宣布游戏为平局。
检查是否获胜
checkLine函数用于检查是否有玩家获胜。它检查一条线(包括行、列或对角线)上的所有点是否由同一玩家填充。如果是,则宣布该玩家为获胜者。
与合约交互
与合约的交互用交易来表示。当玩家通过调用play函数进行移动时,会生成一笔交易。这笔交易被记录下来,可以在SmartPy IDE的右侧面板中看到:

不成功或无效的移动也会生成交易,但带有错误指示:

第二步及之后的棋步
在我们的井字棋游戏中,第一步相对简单,因为棋盘是空的。然而,从第二步开始,棋步将变得比较有趣了,因为它们不仅会向棋盘上添加棋子,还会调用游戏逻辑来检查可能的获胜者。
在第一步之后,nextPlayer值切换到玩家2。现在,play函数会验证玩家2的棋步。合约会执行类似的检查以确保棋步是有效的,即所选网格点在边界内并且为空。
每个玩家落子后,游戏的状态会发生变化。nbMoves会增加,nextPlayer会切换, deck也会更新。此外,在每步棋之后,合约都会检查是否有获胜者或是否平局。
例如,第一个玩家在棋盘的中央i=1, j=1进行了一步棋,第二位玩家可以在不同的位置进行下一步,如i=1, j=2。这两个棋步都会经过验证并成功执行,并生成相应的交易。

游戏进展
后续的棋步以类似的方式进行。每个玩家选择棋盘上的一个空点,轮流落子。在每一次落子之后,合约都会检查是否存在获胜条件。如果一名玩家用他的棋子填满一整行、整列或整个对角线,则游戏结束,该玩家获胜。合约状态中的winner变量将相应更新。
需要注意的是,一旦有玩家获胜,就不再允许继续落子。在游戏结束后尝试进行棋步都将被视为无效,相应的交易也将失败。
平局
在某些游戏中,即使整个游戏棋盘都被填满,也有可能没有玩家达到获胜条件,这将导致平局。合约的设计中已经包含了处理这种情况的方案。
如果棋盘上的所有点位都被填满(nbMoves等于9)并且没有玩家获胜(winner仍然为0),则游戏为平局。合约状态下的draw标识为True(真),表示游戏以平局结束。同样,在此点之后,任何后续棋步都是无效的。

井字棋游戏合约测试场景的第二部分对该平局场景进行了演示。它模拟了一系列导致平局的棋步,并验证了合约是否正确处理它。
高级概念及课程总结
在整个课程中,我们学习了使用SmartPy编写智能合约的各种概念。我们从基础概念开始,学习了如何构建多重签名合约,进而涉足区块链游戏领域,最后探讨了SmartPy提供的高级功能。
SmartPy的其他高级功能
我们已经进行了SmartPy的基础和中级概念的学习,接下来将带大家深入了解这个强大框架提供的一系列高级功能。虽然之前的章节已经介绍了诸多核心功能,但SmartPy作为一个功能宝库,还有更多高级功能等待我们去解锁,包括:
同质化代币:该框架直接支持制作同质化代币,符合FA1.2和FA2.0标准,便于在Tezos区块链上创建你自己的加密货币或代币系统。
非同质化代币(NFT):区块链世界中的一个重要概念是NFT。SmartPy使开发人员能够创建独特的、不可分割的代币——NFT。
预言机:你可以通过SmartPy设计和实现预言机。预言机是获取和提供外部数据给智能合约的重要工具,扩展了区块链应用的使用范围。
合约间通信:SmartPy支持多个智能合约之间的交互,能够帮助大家轻松设计复杂的dApp。
链上和链下视图:有助于解释智能合约状态的视图可以部署在链上(与合约一起)或在链下使用(在SmartPy资源管理器或其他区块链浏览器中)。
可升级合约:该框架还支持设计可升级的合约,而不会破坏现有的合约状态,为后期的改进和更新创造条件。
按类型和各种特征的模板编码示例
为了帮助大家掌握这些高级概念,SmartPy的在线编辑器提供了一系列按类型分类的模板示例,包括FA1.2和FA2.0同质化代币模板、多重签名钱包、拍卖、DeFi协议等等。这些模板具有现实世界的实用性,将帮助大家快速掌握这些高级概念。

除此之外,SmartPy还提供了各种功能,可以提升用户体验并加快开发过程:
场景测试:我们的井字棋合约展示了场景测试的强大功能,能够模拟和验证各种行为和结果。
语法和错误检查:SmartPy IDE中的实时语法和错误检查有助于及早发现错误,简化开发过程。
Michelson代码生成:SmartPy将你的Python代码无缝转换成Tezos智能合约的原生语言Michelson,你可以直接在SmartPy IDE中查看生成的Michelson代码,以更深入地理解。
结语
在整个课程中,我们学习了使用SmartPy编写智能合约的各种概念。我们从基础概念开始,学习了如何构建多重签名合约,进而涉足区块链游戏领域,最后探讨了SmartPy提供的高级功能。现在,你应该能够让好奇心引导你进行后续的发掘和探索。SmartPy IDE及其丰富的模板库将助力你进一步探索。不断突破自己,尝试创建复杂和动态的合约,深入研究新概念,构建创新的dApp,并为蓬勃发展的Tezos开发者社区做出贡献。
请记住,精通是一段旅程,而不是一个终点。你撰写的每份合约,承担的每个项目,都将有助于你了解SmartPy和Tezos的新方面,从而增强你的理解和专业知识。
最后,由衷地感谢大家的加入,跟我们一起完成本系列课程的学习。我们希望这些课程不仅教会了你专业知识,还能激发你对区块链开发的好奇心和热情。你的积极参与是本系列课程取得成功的重要因素。
我们非常期待看到你对Tezos区块链的贡献。恭喜你成功完成本系列课程的学习,祝愿你在区块链开发领域有着光明的未来!请继续学习,继续探索,不断创新吧!