Lesson 3

在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(真),錶示游戲以平局結束。衕樣,在此點之後,任何後續棋步都是無效的。

井字棋游戲合約測試場景的第二部分對該平局場景進行了演示。它模擬了一繫列導緻平局的棋步,併驗證了合約是否正確處理它。

Disclaimer
* Crypto investment involves significant risks. Please proceed with caution. The course is not intended as investment advice.
* The course is created by the author who has joined Gate Learn. Any opinion shared by the author does not represent Gate Learn.
Catalog
Lesson 3

在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(真),錶示游戲以平局結束。衕樣,在此點之後,任何後續棋步都是無效的。

井字棋游戲合約測試場景的第二部分對該平局場景進行了演示。它模擬了一繫列導緻平局的棋步,併驗證了合約是否正確處理它。

Disclaimer
* Crypto investment involves significant risks. Please proceed with caution. The course is not intended as investment advice.
* The course is created by the author who has joined Gate Learn. Any opinion shared by the author does not represent Gate Learn.
It seems that you are attempting to access our services from a Restricted Location where Gate.io is unable to provide services. We apologize for any inconvenience this may cause. Currently, the Restricted Locations include but not limited to: the United States of America, Canada, Cambodia, Thailand, Cuba, Iran, North Korea and so on. For more information regarding the Restricted Locations, please refer to the User Agreement. Should you have any other questions, please contact our Customer Support Team.