從零開始完整開發基於websocket的在線對弈遊戲【五子棋】,只用幾十行程式碼完成全部邏輯。

五子棋是規則簡單明了的策略型遊戲,先形成五子連線者獲勝。
本課程習作採用兩人在線對弈的方式進行比賽,拿著手機在上下班路上玩特別合適。

整個過程在眾觸低程式碼應用平台進行,使用表達式描述遊戲邏輯(高度簡化版JS)。
本課程重點學習websocket實時消息的發送與接收處理。

兩人在線下棋演示

先動手玩一玩://gobang.zc-app.cn
因為是在線遊戲,需要登錄,可以用手機和郵箱分別註冊,用電腦和手機自己跟自己玩。
URL後面加/z就進入開發模式://gobang.zc-app.cn/z

詳盡的的教學請移步嗶哩嗶哩影片://www.bilibili.com/video/BV1QX4y1A7FW

 

棋盤結構

$v.棋盤 = array(14, array(14, ""))

用嵌套數據組件使用14 * 14的二維數組渲染而成,即圖中畫黑線的格子。

方陣結構

$v.方陣 = array(15, array(15, ""))

用嵌套數據組件使用15 * 15的二維數組渲染而成,即圖中黑線交叉的點,滑鼠hover在組件上時高亮的圓圈。

棋手狀態

undefined、邀請中、對方出棋、己方出棋
默認就是沒有值,表示還沒開始下棋或者結束了(勝負已分)。對方出棋的時候己方不能落子。

帳號登錄

既然是在線玩的,就要求登錄。可以用手機或郵箱註冊。
登錄後馬上打開連接,有玩家上線了,有玩家邀請了,對方落子了都是通過此連接socket即時通知的。

打開連接socket:onLogin

$socket.open($c.exp, { channels: ["比賽"], onOnline: true, onOffline: true, allowMultiLogin: true })

第一個參數$c.exp是個對象,可以包含onConnect, onData, onReconnect, onError,前兩個是必須的。

第二個參數是option選項,channels數組裡放你需關注的頻道,onOnline表示有人上線時是否要通知到你,onOffline則是有人下線是要否通知,allowMultiLogin表示同一個帳號能否能在多個地方登錄而不強制前面登錄的帳號下線。

連上後:onConnect

$socket.onlines(["比賽"])
$v.onlines = $r.比賽.filter('$x !== $c.me._id')
$v.onlines.forEach('$user.get($x)')
render()

連上後查詢一下關注”比賽”頻道的在線玩家,排除自己後放到$v.onlines列表後依次獲取用戶資訊。

消息格式

下面有多個on開頭的表達式都是只收到socket消息。它們都有共同的格式:type是消息類型,x是消息體,from是消息發送人。消息接受人to和發送時間d在此案例中未使用到。

有人上線了:onOnline

$v.onlines.push(x)
$v.onlines = $v.onlines.unique()
$user.get(x)

把上線的人放入上面的$v.onlines中,並去重。

有人斷線了:onOffline

$v.對手 === x ? alert("對方斷線了") : ""
$v.onlines.splice($v.onlines.indexOf(x), 1)

斷線了就把他/她從$v.onlines移除。如果剛好是正在跟你對弈的棋手則拋出一個警告通知。

收到數據後:onData

stopIf($c.me._id == from)
$c.exp[type].exc()
render()

先要排除是自己發出的數據,因為socket是廣播消息的,自己也能收到。
然後再根據消息類型執行對應的表達式,可能的類型有:on被邀、on拒邀、on受邀、on落子。

當其他人登錄時,【對手】右邊的問號圓圈就會閃爍,點擊它會彈出在線玩家列表,從中選擇一個可發出對弈邀請。

發出對弈邀請

$socket.send($x, "on被邀", "邀請")
$v.狀態 = "邀請中"
info("邀請已發出,請等待對方接受邀請")

收到消息:on被邀

stopIf($v.狀態, '$socket.send(from, "on拒邀", "對方正在下棋")')
$user.get(from)
$v.對手 = from
$v.pop = "選棋子"

如果自己正在下棋就直接發出”on拒邀”消息,拒絕邀請。
獲取對方用戶資訊,彈出模態窗口提示接受要是拒絕邀請。

收到消息:on拒邀

$v.狀態 = undefined
warn(x || "對方拒絕你的邀請")

把前面的」邀請中「的狀態置空,彈出對方發來的拒邀消息

選子

$v.己方 = "白" // "黑"
$c.exp.受邀.exc()

接受邀請:受邀

$socket.send($v.對手, "on受邀", $v.己方)
$v.方陣 = array(15, array(15, ""))
$v.pop = undefined
$v.對方 = ($v.己方 === "黑" ? "白" : "黑")
$v.狀態 = "己方出棋"
info("請出棋")

給對方發送「on被邀「消息,捎上自己選的子。
清空方陣,準備出棋。

收到消息:on被邀

$v.方陣 = array(15, array(15, ""))
$v.對手 = from
$v.對方 = x
$v.己方 = (x === "黑" ? "白" : "黑")
$v.狀態 = "對方出棋"
info("對方已接受邀請,請等待對方先出棋")

from是對手用戶ID,x是對方選的子,自己就只能選另一種子了。

落子

stopIf($v.狀態 !== "己方出棋" || $v.方陣[$parent.$index][$index] || $v.連續棋子.length > 4)
$v.落子點 = [$parent.$index, $index]
$socket.send($v.對手, "on落子", $v.落子點)
$("." + $v.己方 + "子聲音").play()
$v.方陣[$parent.$index][$index] = $v.己方
$v.檢查方向.forEach($c.exp.落_是否勝出)
$v.狀態 = "對方出棋"

如果不是己方出棋的狀態,或者落子位置不在方陣內,或者已經組成4個以上連續棋子都不可落子。
發出”on落子”消息,捎上剛才的落子點坐標軸。
播放落子聲音,並把己方棋子放在方陣的落子點上,並通過動態類名發出光暈。

$v.落子點[0] === $parent.$index && $v.落子點[1] === $index ? "光暈" : ""

檢查剛才的落子能否勝出。

檢查勝出(形成五子連線)

要判斷勝負只需落子時從落子點 [y, x] 以四種連線的正反方向分別查看,累計4個以上連續同色棋子為聲。

$v.檢查方向

[
    [
        [-1, 0],
        [1, 0]
    ],
    [
        [0, -1],
        [0, 1]
    ],
    [
        [1, -1],
        [-1, 1]
    ],
    [
        [-1, -1],
        [1, 1]
    ]
]

-1表示往後檢查,0表示不動,1表示往前檢查。比如[-1, 0]是是X軸上往負值方向檢查,即正西方向;[1, -1]表示先往X軸正方向檢查再往Y軸負方向檢查,即東北方向。

即 

落子是否勝出

$v.連續棋子 = [$v.落子點]
$l.方向 = $x[0]
$l.非連續 = false
$v.循環4次.forEach($c.exp.落_相鄰同色)
$l.方向 = $x[1]
$l.非連續 = false
$v.循環4次.forEach($c.exp.落_相鄰同色)
stopIf($v.連續棋子.length > 4, 'info(($v.狀態 === "己方出棋" ? $v.己方 : $v.對方) + "子贏了"); $v.狀態 = undefined;')

先把當前落子位置作為第一個連續棋子,先往$v.檢查方向提供的一對方向的第一個方向試探移動4次(即循環4遍)看是否有相鄰同色子,再往另一個方向也試探4次。
如果試探得到的$v.連續棋子大於4個,那當前落子方勝出。

檢查與落子相鄰的同色子

$l.y = $v.落子點[0] + $l.方向[0] * $x
$l.x = $v.落子點[1] + $l.方向[1] * $x
!$l.非連續 && $v.方陣[$l.y][$l.x] === ($v.狀態 === "己方出棋" ? $v.己方 : $v.對方) ? $v.連續棋子.push([$l.y, $l.x]) : $l.非連續 = true

一個試探方向包括X軸方向和Y軸方向,有-1、0、1三種移法,分別移動一下坐標,檢查新坐標在方陣中的棋子,如果坐標上有子,並且現在是己方出棋而且這個子正好是己方顏色,那這個子就是連續棋子的一部分。其它情況都不能算連續同色子,比如坐標上沒有子,或者是對方的子,再或者是以前就已經非連續了,這次就沒必要繼續檢查了。

勝出的連續5個棋子也要發出光暈。前面新落的子已經通過動態類名發出光暈,現在要找出連續棋子的其它棋子。

$v.連續棋子.length > 4 && $v.連續棋子.find('$x[0] === $ext.$parent.$index && $x[1] === $ext.$index') ? "光暈" : ""

我們從連續棋子裡面找,看看裡面是否有一個棋子的坐標跟當前檢查的坐標位置相同。$x[0]是連續棋子X坐標,$x[1]是Y坐標。注意,這裡是嵌套數據組件里作為動態類名的,$index是當前數據組件的下標,$parent.$index是上一層數據組件的下標。但由於它們是放在find()函數裡面的,需要在前面添加$ext.表示它們函數外面上下文提供的數據,如果沒有$ext.,那就成了find()函數提供給的上下文數據了。

 

準備深入研究的同學請到//www.zcappp.cn/course/gobang頁面後,點擊右側的【克隆】按鈕,把整個遊戲複製一份隨意玩弄更改。

更多教學影片請移步嗶哩嗶哩空間://space.bilibili.com/475645807,裡面不僅有各種前端可視化案例演示和講解,還有多個完整功能的網站應用案例的開發過程演示和講解。