音游UP主居然寫程序教學專欄系列第1期 基于VB.NET語言編寫小游戲《2048》
一、前言
由于本專欄是我的第一次編寫程序教學的嘗試,前言部分多寒暄幾句,沒錯,你們沒目害,一個音游屑up主現(xiàn)在閑著沒事干開始寫屑程序啦,嘿嘿嘿~
寫程序的愛好和我打音游的愛好其實是一并開始的,扯到音游上的年代,大概是初中時期,我接觸Cytus初代的時間,也是我玩的第一款音游,幾乎同期我摸到了VB 6.0,并真正開始用if then結(jié)構(gòu)寫各種彈窗微型闖關(guān)游戲了,仔細想想當年就會寫劇情發(fā)展樹真是不得了呢。
VB的程序用語和編寫習慣從那時起就根深蒂固了,現(xiàn)在VB 6.0進化到了VB.net,雖然這兩種語言也有很大差異,但用VB的語句構(gòu)思邏輯已經(jīng)很難改變了,所以,我的教學專欄是100%純VB.net語言編寫,這確實和現(xiàn)在的時代落差相當大,但我真的。。。emmm,不想換語言從頭熟悉了,摸過python,但真正玩起來還是磕磕絆絆的,不像VB.net那樣順手,同時python給我了一個巨大的手癖,寫循環(huán)總是for i in [數(shù)組],然而VB是不認識的。
《2048》這款游戲具有非常強烈的時代特征,它誕生于2014年,作為一款爆款手游橫行霸道,參考最近的案例就是《羊了個羊》的火爆程度,那時幾乎人人都在手機上搓搓搓,然后曬一個大大的黃色2048表示自己畢業(yè)了。2014年我正好讀大一,選修了一門VB.net課程,開始系統(tǒng)性學習編程的概念,掌握了很多新的VB功能,而不是單純的if then加按鈕的簡陋組合了,有幸,這是我第一次上手編寫有簡單界面的游戲。它相對我后期開發(fā)的游戲,顯得比較簡陋,功能單一,代碼量也比較少,但可以拿來先做一次專欄編寫的嘗試,我嘗試把這個系列的教學專欄寫給編程的入門小白,讓大多數(shù)初學者看懂并且可以自己操作,就像我的音游專欄初衷一樣,寫給入門者,開發(fā)興趣之用。
本系列專欄內(nèi)容我更多偏重于寫關(guān)鍵功能實現(xiàn)的邏輯,淡化編程語言的使用(因為我也不是專業(yè)程序猿hhh),如果各位讀者并沒有使用過VB.net,希望本篇也可以提供相關(guān)思路,幫助你們用自己所學的編程語言去實現(xiàn),就好比我用private sub func(),可以換成c語言的void func(),也可以換成python的def func(),思路到了,編程語言只是一個工具去實現(xiàn)我們的思路。
二、編程環(huán)境簡介
開發(fā)工具:Microsoft Visual Studio 2013及以上版本(這個IDE可以實時代碼查錯,并且具有代碼單詞分顏色、代碼自動分段錯位,高亮變量、代碼預測等功能,很人性化,推薦使用);
編程框架:Microsoft .NET Framework 4.5及以上版本;
電腦配置:intel 13900k處理器+ NVIDIA 4090Ti顯卡(誤,隨便弄個電腦就可以嘿嘿);
外設(shè)硬件:鼠標鍵盤顯示器,加一個的腦袋;
軟件優(yōu)勢:軟件最終可以形成一個單獨的.exe文件,不需要安裝部署、寫注冊表、調(diào)用組件等額外步驟,即開即用,基本上交給普通windows系統(tǒng)用戶都可以雙擊直接運行,用的UI框架、程序庫等都是微軟自己的產(chǎn)品,兼容性很好,疑難雜癥相對較少。
軟件劣勢:語言比較偏門,脫離python時代,游戲比較古老,其實相關(guān)開源代碼等資料已經(jīng)很豐富了,本期單純拿來試寫專欄當案例使用,看看怎樣編排章節(jié)能更好說清楚一個程序的思路。
三、界面UI交互
我使用VB做小游戲主要原因是自帶UI組件,可以自行畫按鈕、畫文本框來直觀表達操作交互邏輯,而不是隱晦的Y/N(沒錯,黑乎乎程序交互窗體點名批評)。首先,放出2048涉及到的VB組件,見下圖:??

雙擊運行后,正常的顯示效果如下圖:

這里額外贅述一下VB.net的組件特點,這里每一個組件都有很多屬性,如顯示字體顏色、是否可用、是否可見、組件名稱、排版方式、組件大小、組件位置等等自身屬性,同時不同類型的組件支持的觸發(fā)事件也各不相同,如單擊、雙擊、懸停、拖動、內(nèi)容變更、獲得或失去焦點、屬性改動等等。舉個簡單例子,2048的界面中那個最長的按鈕,底色屬性是“Gold”,顯示內(nèi)容是“重新開始游戲”,字體是“微軟雅黑”,它還有窗體坐標、可見性等很多屬性可調(diào)整,大多數(shù)都是默認的,如果有特殊的屬性需求,我會單獨提出相關(guān)的改動內(nèi)容,如果沒有特殊說明,復刻界面可以憑自己喜好來做,按鈕長什么樣子不影響咱們功能的實現(xiàn),一般要注意的問題,就是按鈕的位置選擇了,像2048這個界面中,至少,按鈕要放在4*4的方陣下方避免遮蓋,相關(guān)屬性列表列出一個供參考:


再引入程序面向?qū)ο蟮母拍?,這里的屬性可以理解為一個對象的屬性,比如對象是button,可以針對這個button的屬性進行編程修改,比如button1.text=”按鈕”這樣。
界面中的4*4文本框方陣需要單獨設(shè)置一些的屬性,這16個textbox統(tǒng)一應(yīng)用這個配置:enabled屬性為false,這樣文本框就不再支持編輯了,僅僅起到顯示作用;multiline屬性為true,可以多行顯示,這樣可以把文本框拉長變成正方形。其余屬性并不太重要,可以自行調(diào)整。
這個游戲由于涉及到敲擊鍵盤控制,所以窗體Form屬性需要單獨設(shè)置一項,KeyPreview設(shè)置為true,讓這個程序正常反饋敲擊鍵盤的動作。
這樣我們就完成了全部UI的設(shè)計工作,建立VB程序項目后,按圖示的這些組件一一畫出來,并調(diào)好相應(yīng)的屬性,接下來就可以正式寫代碼了,如果調(diào)試運行期間,某個功能或效果并沒有達到心理預期,除了查驗代碼的編寫問題,也要注意這些組件的屬性是否符合我們的設(shè)計思路。UI界面的美丑問題切忌糾結(jié),調(diào)UI是個很費功夫的工作,初學程序本著能用就行,這里也放一下我當年交作業(yè)的初版UI,比起我現(xiàn)在扁平化設(shè)計的精調(diào)版本,實在是兩個時代的產(chǎn)物~

四、核心功能實現(xiàn)
在搬上所有代碼前,首先用文字捋一遍我們將要實現(xiàn)的功能,這里要具備模塊化的思維,我們所做的過程或者函數(shù),都分別代表一個專有的功能模塊,搞清楚所有的功能模塊,就可以組合在一起實現(xiàn)全部效果了。
2048從交互邏輯來看,主要是程序會響應(yīng)我們的一個“上下左右”操作,把數(shù)字往我們操作的方向平移,合并相同的數(shù)字,然后隨機生成一個新的2或4元素在方陣里面。
從這段描述中,我們不難提煉出一些核心功能點,用模塊化的思維將其逐個分解并實現(xiàn):
1、讀取“上下左右”的操作指令,并調(diào)用對應(yīng)的數(shù)據(jù)處理功能(難點在于對操作指令的正確識別);
2、記錄、運算、輸出我們方陣中的數(shù)字(難點在于運算過程的邏輯);
3、根據(jù)顯示的數(shù)字,調(diào)整方格顏色(難點在于各顏色間要差異明顯);
4、檢測生成新數(shù)字的時機,并生成新數(shù)字(難點在于生成什么,放在哪);
5、檢測游戲是否已經(jīng)結(jié)束,無法移動數(shù)字(難點在于檢測邏輯的構(gòu)建,具體參數(shù)有哪些,如何界定這個游戲結(jié)束);
6、計算我們的游戲得分,并保存到計算機的某個位置(難點在于讀寫外部文件功能的函數(shù)調(diào)用技巧)。
由此可見,2048游戲的算法不算太難,功能也很少,是一個比較入門的小游戲編程訓練。
五、代碼搬運
這個游戲只需要一個窗體,所以接下來的代碼都會在一個Public Class Form1中編寫,讀懂代碼后,調(diào)好各個組件名稱,這段代碼去掉中間我增加的整段描述文字,直接全部復制即可(可以保留代碼末尾的注釋),報錯則多注意是不是名稱沒調(diào)好,導致函數(shù)或過程引用錯誤了。
<——<代碼搬運開始>——>
Public Class Form1
這里定義了很多公共變量,變量的作用詳見代碼末尾的注釋,用于記錄方陣數(shù)據(jù)和游戲狀態(tài)。我寫的程序可能變量命名比較放飛自我,比如對文本框編組,使用Two Zero Four Eight縮寫tzfe來代替,各位讀者見諒,奇怪的命名我盡力文字都解釋到位。。。
這一版本的代碼是我經(jīng)過優(yōu)化后的結(jié)果,保留原汁原味的“青澀”外,盡量節(jié)約了很多代碼行數(shù),而且很多擴展功能我并沒有真正添加進來,比如sqr變量是4,原版代碼我并沒有單獨做變量來表達階數(shù),是我后續(xù)學會了動態(tài)數(shù)組后,寫高階2048才用到的參數(shù),這里建議初學者直接把后續(xù)帶有sqr變量的部分替換成純數(shù)字,方便理解程序思路。Diff變量是一個玄學參數(shù),我搜索了各大論壇,以及GitHub上的源代碼,都沒有真正論述過難度問題,我會在相應(yīng)功能模塊處再做深入解釋,我認為這個參數(shù)和游戲體驗的關(guān)系很密切,同樣,初次寫2048也可以不用理它,帶diff變量引用的部分,替換成純數(shù)字。
另外,我還寫了彩蛋功能,也可以不用去管它,比如作弊模式讓2048盤面可編輯,或者自動2048滾動播放等,這些功能一般集成在label控件的觸發(fā)事件里,在末尾會出現(xiàn)這些彩蛋功能的代碼,刪除不影響正常游戲體驗。
??? Public a(3, 3) As Long '2048底層數(shù)據(jù),先數(shù)據(jù)處理,再輸出至盤面
??? Public b(15), c(15) As Long '輔助數(shù)組。詳見相關(guān)功能模塊
??? Public tzfe(15) As TextBox '2048的盤面由textbox控件展示
??? Public sqr As Byte = 4 '記錄2048階數(shù),這里限制在了4階,做動態(tài)控件的話,放開這個參數(shù)即可實現(xiàn)高階2048
??? Public diff As Byte = 3 '記錄2048難度,這里默認為7級,這個參數(shù)實際為10-難度數(shù)值,由隨機生成數(shù)字的公式引用達到更改概率的效果
??? Public score As Long '記錄分數(shù),先加分,再顯示分數(shù)
??? Public nomove As Boolean = False '結(jié)束游戲狀態(tài)變量,true代表游戲結(jié)束了
??? Public cheat As Boolean = False '標記功能開關(guān),true代表作弊模式開啟,彩蛋功能
??? Public turn As Integer '配個秒表,上右下左循環(huán)autoplay,彩蛋功能
?
以下是程序框體加載時執(zhí)行的內(nèi)容,為了保留“青澀味”,手動建的16個文本框我并沒有替換成動態(tài)數(shù)組,但我又不想保留對每個文本框?qū)懸淮闻卸ǖ娜唛L代碼,于是這里還是用了“先進”的控件對象編組,用序號把它們串了起來,可以用循環(huán)來依次編輯各個文本框的狀態(tài)了,注意手動建立的獨立textbox是沒法放到循環(huán)里的,只能一個一個枚舉。后續(xù)就是游戲初始化、彈窗歡迎界面功能了,相關(guān)引用可以查閱對應(yīng)的功能模塊,比如這里用了Restart()過程,則可以查找對Restart()定義的部分,我安排內(nèi)容一般會就近放,就可以查看這個功能模塊具體寫了什么了。
??? Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
??????? tzfe(0) = TextBox1
??????? tzfe(1) = TextBox2
??????? tzfe(2) = TextBox3
??????? tzfe(3) = TextBox4
??????? tzfe(4) = TextBox5
??????? tzfe(5) = TextBox6
??????? tzfe(6) = TextBox7
??????? tzfe(7) = TextBox8
??????? tzfe(8) = TextBox9
??????? tzfe(9) = TextBox10
??????? tzfe(10) = TextBox11
??????? tzfe(11) = TextBox12
??????? tzfe(12) = TextBox13
??????? tzfe(13) = TextBox14
??????? tzfe(14) = TextBox15
??????? tzfe(15) = TextBox16
??????? Restart()
??????? MsgBox("歡迎進入2048")
??? End Sub
?
這個函數(shù)是我查CSDN論壇得來的,對于各種需要鍵盤響應(yīng)的小游戲來說,一般需要加這樣一段,目的是讓一些系統(tǒng)默認的操作轉(zhuǎn)換為自己定義的操作,鍵盤“上下左右”一般用來移動光標,我們要拿來移動數(shù)字用,所以覆蓋了系統(tǒng)默認分配的事件,這段無需太深入理解,扔在那里即可,涉及到的知識點比較復雜。
??? Protected Overrides Function ProcessDialogKey(keyData As Keys) As Boolean
??????? If keyData = Keys.Left Or keyData = Keys.Right Or keyData = Keys.Up Or keyData = Keys.Down Then
??????????? Return False
??????? Else
??????????? Return MyBase.ProcessDialogKey(keyData)
??????? End If
??????? '某些鍵,如 Tab、Return、Esc 和箭頭鍵,由控件自動處理。
??????? '所以當你的窗體添加了其它控件如按鈕,checkbox等,form的keydown事件就觸發(fā)不了.
??????? '為使這些鍵引發(fā)窗體的KeyDown事件,你需要重寫form.ProcessDialogKey函數(shù),這個函數(shù)可以在消息預處理期間 處理對話字符,例如TAB、RETURN、ESCAPE和箭頭鍵等
??????? '同時窗體屬性Keypreview要改為True,將Keydown事件傳給內(nèi)部控件,避免失去焦點導致Keydown觸發(fā)失敗
??? End Function
?
這個函數(shù)完成了我們歸納的第一個功能點,讀取“上下左右”的操作指令,并調(diào)用對應(yīng)的數(shù)據(jù)處理功能,敲下鍵盤時觸發(fā)了2048這個窗體的事件,開始判定敲下了什么按鍵(select case),如果是“上下左右”,則調(diào)用對應(yīng)的移動功能(upmove/downmove/leftmove/rightmove詳見對應(yīng)的功能模塊代碼),移動前后調(diào)用了兩次復制盤面數(shù)據(jù)的功能(copy/copy2過程),根據(jù)判定結(jié)果來決定是否增加一個2/4新元素,這些完成后,刷新盤面各個文本框的顏色(setcolor過程),再判定游戲是不是結(jié)束了(theend過程)。
??? Public Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles MyBase.KeyDown
??????? If e.KeyCode <> Keys.Left And e.KeyCode <> Keys.Right And e.KeyCode <> Keys.Up And e.KeyCode <> Keys.Down Then
?? ?????????Exit Sub
??????? End If
??????? Input()
??????? Copy()
??????? Select Case e.KeyCode
??????????? Case Keys.Up
??????????????? Upmove()
??????????? Case Keys.Down
??????????????? Downmove()
??????????? Case Keys.Left
??????????????? Leftmove()
??? ????????Case Keys.Right
??????????????? Rightmove()
??????? End Select
??????? Copy2()
??????? If Check2() = True Then '盤面挪動了,證明空出來一個格子,繼續(xù)隨機塞入數(shù)字
??????????? Add()
??????? End If
??????? Output()
??????? Setcolor()
??????? Theend()
??? End Sub
?
這個過程清零盤面數(shù)據(jù),加一個初始2或4元素,記錄數(shù)據(jù),刷新格子顏色,重新標記游戲狀態(tài)為“未結(jié)束”(nomove=false),歸零得分等,由特定的按鈕(重新開始游戲)和主窗體過程(開局初始化)調(diào)用。
??? Private Sub Restart() '重置盤面
??????? Dim i As Integer
??????? For i = 0 To sqr ^ 2 - 1
??????????? tzfe(i).Text = "0"
??????? Next
??????? Input()
????? ??Add()
??????? Output()
??????? Setcolor()
??????? score = 0
??????? nomove = False
??? End Sub
?
這是判定游戲是否結(jié)束的過程(check/check2/check3,具體判定功能的代碼詳見后續(xù)),每一次敲擊鍵盤都會調(diào)用一次這個過程,為了避免游戲結(jié)束后,多次敲擊鍵盤重復彈窗提示“游戲結(jié)束”,過程開頭做了個跳出的判定,如果游戲已經(jīng)結(jié)束了,再調(diào)用這個過程將什么都不發(fā)生(使用exit sub命令跳出),nomove變量實現(xiàn)了這個功能,當它的值發(fā)生改變,代表著“游戲結(jié)束”已經(jīng)彈窗過一次了。
其中涉及到IO.File的代碼是記錄分數(shù)功能,效果是在exe程序文件路徑中生成了另一個文檔形式的文件“score”,在這里寫入了游戲最高分,如果編程基礎(chǔ)薄弱,可以暫緩這部分代碼,刪去也是可以的,不影響2048游戲體驗,但這個功能在更復雜的游戲中不可避免地出現(xiàn),是需要掌握的一個技能,IO.File是可以直接調(diào)用的一個系統(tǒng)模塊,提供了很多文件讀寫的功能,深入了解需要查閱這個關(guān)鍵詞的資料,了解全部功能的調(diào)用技巧。
過程末尾再掃描一下2048的盤面數(shù)據(jù),如果成功合成了一個2048,則額外彈窗"2048目標達成!"恭喜一番,增加儀式感。
??? Private Sub Theend()
??????? If nomove = True Then
??????????? Exit Sub '避免游戲結(jié)束后還彈窗
??????? End If
??????? If Check() = sqr ^ 2 And Check3() = False Then '都填滿了,而且沒有數(shù)字可以合并,游戲結(jié)束
??????????? nomove = True
??????????? MsgBox("游戲結(jié)束,你的得分:" & score)
??????????? If IO.File.Exists(Application.StartupPath & "/score") = True Then
??????????????? Dim gameover As Integer
??????????????? gameover = Val(IO.File.ReadAllText(Application.StartupPath & "/score"))
??????????????? If score > gameover Then
??????????????????? IO.File.WriteAllText(Application.StartupPath & "/score", score)
??????????????????? MsgBox("恭喜你創(chuàng)造新紀錄")
??? ????????????Else
??????????????????? MsgBox("很遺憾你沒有打破紀錄," & "最高分:" & gameover)
??????????????? End If
??????????? Else
??????????????? IO.File.WriteAllText(Application.StartupPath & "/score", score)
??????????????? MsgBox("分數(shù)已記錄")
??????????? End If
????? ??????Dim i As Integer
??????????? For i = 0 To sqr ^ 2 - 1
??????????????? If Val(tzfe(i).Text) >= 2048 Then
??????????????????? MsgBox("2048目標達成!")
??????????????????? Exit For
??????????????? End If
??????????? Next
??????? End If
??? End Sub
?
以下是兩個基本功能,逐個掃描文本框內(nèi)容,input將文本框數(shù)據(jù)寫入a()數(shù)組記錄,output將計算好的a()數(shù)組寫回到文本框顯示出來。如果想解鎖高階2048,調(diào)整sqr參數(shù)之外,注意調(diào)整a()數(shù)組的容量,在開頭public變量聲明中調(diào)整,否則運行時會報錯數(shù)組溢出。
??? Private Sub Input()
??????? Dim i, j As Integer
??????? For i = 0 To sqr - 1
??????????? For j = 0 To sqr - 1
??????????????? a(i, j) = Val(tzfe(i * sqr + j).Text)
??????????? Next
??????? Next
??? End Sub
??? Private Sub Output()
??????? Dim i, j As Integer
??????? For i = 0 To sqr - 1
??????????? For j = 0 To sqr - 1
??????????????? tzfe(i * sqr + j).Text = CStr(a(i, j))
??????????? Next
??????? Next
??? End Sub
?
以下是另外兩個小功能,兩次掃描盤面數(shù)據(jù)并記錄在臨時數(shù)組中(b/c),對比兩個數(shù)組,如果不同則代表上一個操作使得2048盤面變動了,那么后續(xù)再執(zhí)行填入新元素的功能。同理,如果需要解鎖高階2048,注意擴容b和c的數(shù)組容量避免數(shù)組溢出。這里顯示了另一個“青澀”的代碼習慣,我用j、k、l作為臨時計數(shù)變量,這個l(字母)就很靈性,它太像1(數(shù)字)了!負面教材,以后大家請使用i、j、k。
??? Private Sub Copy() '移動前記錄狀態(tài)
??????? Dim j, k As Integer
??????? Dim l As Integer = 0
??????? For j = 0 To sqr - 1
??????????? For k = 0 To sqr - 1
??????????????? b(l) = a(j, k)
??????????????? l += 1
??????????? Next
??????? Next
??? End Sub
??? Private Sub Copy2() '移動后記錄狀態(tài)
??????? Dim j, k As Integer
??????? Dim l As Integer = 0
??????? For j = 0 To sqr - 1
??????????? For k = 0 To sqr - 1
??????????????? c(l) = a(j, k)
??????????????? l += 1
??????????? Next
??????? Next
??? End Sub
?
這里的三個check函數(shù)是2048比較核心的邏輯內(nèi)容了,玩2048很少會深入思考到這個邏輯,編寫程序卻非常重要。我們總共需要判定三個游戲狀態(tài),格子滿沒滿?盤面動沒動?還有沒有相同數(shù)字貼在一起?格子滿了,且沒有相鄰一樣的數(shù)字,代表游戲結(jié)束,盤面動了,代表我們可以生成一個新的2/4元素。
判斷格子是否滿了容易實現(xiàn),統(tǒng)計非0的格子即可。
盤面動沒動的判定需要借助輔助過程,數(shù)字移動操作前后各掃描一下盤面狀態(tài),對比兩個數(shù)組(b/c)得出結(jié)論
相同數(shù)字相鄰的判定,我們需要用嵌套循環(huán),逐行逐列,然后逐個相鄰格子對比,如果有相同的數(shù)字,則跳出所有循環(huán)(exit for),返回一個true就夠了,這里節(jié)約算力,不需要遍歷所有相鄰格子(遍歷需要3*4*2=24次相鄰格比較)。為了方便理解代碼中嵌套循環(huán)的比較過程,附圖一張:

??? Private Function Check() '檢查是不是格子滿了,返回格子總數(shù)就是滿了
??????? Dim i As Integer = 0
??????? Dim m, n As Integer
?????? ?For m = 0 To sqr - 1
??????????? For n = 0 To sqr - 1
??????????????? If a(m, n) <> 0 Then
??????????????????? i += 1
??????????????? End If
??????????? Next
??????? Next
??????? Return i
??? End Function
??? Private Function Check2() '檢查一次操作后盤面是否有變動,變動過則返回true
??????? Dim i As Boolean = False
??????? Dim r As Integer
??????? For r = 0 To sqr ^ 2 - 1
??????????? If b(r) <> c(r) Then
??????????????? i = True
??????????????? Exit For
??????????? End If
????? ??Next
??????? Return i
??? End Function
??? Private Function Check3() '檢查相鄰兩格是否有可合并的數(shù)字,如果沒有,返回false
??????? Dim same As Boolean = False
??????? Dim i, j As Integer
??????? For i = 0 To sqr - 1
??????????? For j = 0 To sqr - 2
??????????????? If a(i, j) = a(i, j + 1) Then
??????????????????? same = True
??????????????????? Exit For
??????????????? End If
??????????????? If a(j, i) = a(j + 1, i) Then
??????????????????? same = True
??????????????????? Exit For
??????????????? End If
??????????? Next
??????? ????If same = True Then
??????????????? Exit For
??????????? End If
??????? Next
??????? Return same
??? End Function
?
以下是生成2/4新元素的功能模塊,隨機選擇一個位置,并塞入隨機2或4的數(shù)字。隨機選位置功能由于我早期的算法設(shè)計能力“不強”(更屑),用的是個簡單粗暴枚舉循環(huán)的邏輯,1~16隨機數(shù)不停生成,直到找到對應(yīng)無數(shù)字的格子進行填寫,好在16個單元格并不會增加很大計算負載,后續(xù)體量稍大的游戲,我統(tǒng)一使用了Fisher-Yates設(shè)計的填充算法,屆時若用到會具體描述其優(yōu)勢。
隨后就是生成2、4的邏輯,這一個細節(jié)只有在真正做程序才會發(fā)現(xiàn),玩家只管移動數(shù)字,而我們程序員需要考察到底2、4占據(jù)怎樣的比例游戲體驗更高,為此,我專門單獨開一個章節(jié)討論這個問題,詳見我的第六章節(jié):創(chuàng)新點。初學者如果不深入探討2048的游戲邏輯,可以默認一個3:1的比例分別生成2和4,不需要引入我設(shè)計的diff變量調(diào)節(jié)難度。生成2、4算法上(最后一行),也可以直接簡化為隨機生成數(shù)字,根據(jù)這個數(shù)字大小來選擇2、4的if then結(jié)構(gòu)(要好幾行代碼才能實現(xiàn),先出數(shù),再選擇),容我裝個杯,寫了一個數(shù)學表達式來直接區(qū)分生成結(jié)果了(好處是就用了一行代碼),如果各位讀者想進一步魔改2048,改成存在生成8的可能性,則需要老老實實用if then來重構(gòu)一下最后一行代碼。
??? Private Sub Add() '加一個新的數(shù)字進入盤面
??????? Dim x, y As Integer
??????? Randomize()
??????? Do
??????????? x = Int(Rnd() * sqr)
??????????? y = Int(Rnd() * sqr)
??????? Loop Until a(x, y) = 0
??????? a(x, y) = 2 * (Int(Rnd() * (diff + 1) / diff) + 1) '2或4的生成概率比例會影響2048難度,這里默認按75%出現(xiàn)2,25%出現(xiàn)4設(shè)定
??? End Sub
?
以下是2048另一塊核心邏輯,盤面數(shù)據(jù)移動時的計算規(guī)則。經(jīng)過我大學上課長時間“測試”(摸魚)2048的移動結(jié)果,我將這個過程慢動作化來介紹,首先四個方向,使用的移動、合并邏輯都是一樣的,我們只需要研究單向即可。對于一個方向的數(shù)字,首先軟件會把它們?nèi)砍恋祝ㄒ簿褪且苿臃较颍?,比如?/p>
[2]-[0]-[0]-[0]向右,得[0]-[0]-[0]-[2];
[0]-[2]-[0]-[0]向右,得[0]-[0]-[0]-[2];
[2]-[0]-[2]-[0]向右,得[0]-[0]-[2]-[2];
[4]-[2]-[0]-[2]向右,得[0]-[4]-[2]-[2];
不難看出,這個過程最多執(zhí)行2048的階數(shù)次,前半段沉底代碼一共三重嵌套循環(huán),首先逐行/列掃描(變量m/n),然后當前行/列掃描4次(4階2048,變量i),最后當前行/列相鄰格對比(變量n/m),前置位0格把下一格數(shù)字格復制,下一格數(shù)字格清0。
下一步動作是合并運算,經(jīng)過多次試玩,我們得出結(jié)論,一次移動的合并運算,僅僅會對當前行/列的所有所見數(shù)字,執(zhí)行一次合并,不會遞歸合并,如何理解,如下(數(shù)據(jù)經(jīng)過第一環(huán)節(jié),已沉底):
[2]-[2]-[2]-[2]向右,得[0]-[0]-[4]-[4],而不是[0]-[0]-[0]-[8];
[0]-[2]-[2]-[2]向右,得[0]-[0]-[2]-[4],而不是[0]-[0]-[0]-[8],也不是 [0]-[0]-[4]-[2];
[0]-[0]-[2]-[2]向右,得[0]-[0]-[0]-[4];
[8]-[4]-[4]-[2]向右,得[0]-[8]-[8]-[2],而不是[0]-[0]-[16]-[2];
基本上可能看到的情況都已經(jīng)列出,設(shè)計循環(huán)邏輯時,還是逐行/列掃描,與移動方向相反,倒序依次掃描相鄰格數(shù)字,如果可合并,則前置位數(shù)字翻倍,后置位數(shù)字歸零,后續(xù)數(shù)字依次前移(再次執(zhí)行一次沉底)。至此,我們就完整解析了一次移動所需要的邏輯運算過程,并用代碼呈現(xiàn)出來。
??? Private Sub Upmove()
??????? Dim m, n, i As Integer
??????? For m = 0 To sqr - 1 '數(shù)字沉底,排出空位
??????????? For i = 1 To sqr - 1
??????????????? For n = 0 To sqr - 2
??????????????????? If a(n, m) = 0 Then
??????????????????????? a(n, m) = a(n + 1, m)
??????????????????????? a(n + 1, m) = 0
??????????????????? End If
??????????????? Next
??????????? Next
??????? Next
??????? For m = 0 To sqr - 1 '數(shù)字合并,算分,再平移沉底
??????????? For n = 0 To sqr - 2
??????????????? If a(n, m) = a(n + 1, m) Then
??????????????????? score += a(n, m)
??????????????????? a(n, m) *= 2
??????????????????? For i = n + 1 To sqr - 2
??????????????????????? a(i, m) = a(i + 1, m)
??????????????????? Next
??????????????????? a(sqr - 1, m) = 0
??????????????? End If
??????????? Next
??????? Next
??? End Sub
??? Private Sub Downmove()
??????? Dim m, n, i As Integer
??????? For m = 0 To sqr - 1 '數(shù)字沉底,排出空位
??????????? For i = 1 To sqr - 1
??????????????? For n = sqr - 1 To 1 Step -1
??????????????????? If a(n, m) = 0 Then
??????????????????????? a(n, m) = a(n - 1, m)
??????????????????????? a(n - 1, m) = 0
??????????????????? End If
???????? ???????Next
??????????? Next
??????? Next
??????? For m = 0 To sqr - 1 '數(shù)字合并,算分,再平移沉底
??????????? For n = sqr - 1 To 1 Step -1
??????????????? If a(n, m) = a(n - 1, m) Then
??????????????????? score += a(n, m)
??????????????????? a(n, m) *= 2
???????????? ???????For i = n - 1 To 1 Step -1
??????????????????????? a(i, m) = a(i - 1, m)
??????????????????? Next
??????????????????? a(0, m) = 0
??????????????? End If
??????????? Next
??????? Next
??? End Sub
??? Private Sub Rightmove()
??????? Dim m, n, i As Integer
??????? For m = 0 To sqr - 1 '數(shù)字沉底,排出空位
??????????? For i = 1 To sqr - 1
??????????????? For n = sqr - 1 To 1 Step -1
??????????????????? If a(m, n) = 0 Then
??????????????????????? a(m, n) = a(m, n - 1)
???????????????????? ???a(m, n - 1) = 0
??????????????????? End If
??????????????? Next
??????????? Next
??????? Next
??????? For m = 0 To sqr - 1 '數(shù)字合并,算分,再平移沉底
??????????? For n = sqr - 1 To 1 Step -1
??????????????? If a(m, n) = a(m, n - 1) Then
??????????????????? score += a(m, n)
??????????????????? a(m, n) *= 2
??????????????????? For i = n - 1 To 1 Step -1
??????????????????????? a(m, i) = a(m, i - 1)
??????????????????? Next
??????????????????? a(m, 0) = 0
??????????????? End If
??????????? Next
??????? Next
??? End Sub
??? Private Sub Leftmove()
??????? Dim m, n, i As Integer
??????? For m = 0 To sqr - 1 '數(shù)字沉底,排出空位
??????????? For i = 1 To sqr - 1
??????????????? For n = 0 To sqr - 2
??????????????????? If a(m, n) = 0 Then
??????????????????????? a(m, n) = a(m, n + 1)
??????????????????????? a(m, n + 1) = 0
??????????????????? End If
??????????????? Next
??????????? Next
??????? Next
??????? For m = 0 To sqr - 1 '數(shù)字合并,算分,再平移沉底
??????????? For n = 0 To sqr - 2
?????????????? ?If a(m, n) = a(m, n + 1) Then
??????????????????? score += a(m, n)
??????????????????? a(m, n) *= 2
??????????????????? For i = n + 1 To sqr - 2
??????????????????????? a(m, i) = a(m, i + 1)
??????????????????? Next
??????????????????? a(m, sqr - 1) = 0
??????????????? End If
??????????? Next
??????? Next
??? End Sub
?
以下是盤面文本框著色的邏輯,這里就會看到對16個文本框編組的重要性,通過一個1~16的循環(huán),即可對逐個文本框選定顏色并顯示,如果不編組,需要對每一個文本框單獨定義一個textchange事件,單獨寫一遍選擇顏色的代碼,同樣一段代碼要寫16遍。。。我根據(jù)原版2048的選色,用VB.net自帶的顏色參數(shù)復刻了一下,考慮到硬核(開掛)玩家的需求,我從0到65536均配置了獨特的顏色顯示,其余數(shù)字則統(tǒng)統(tǒng)顯示為黑色。
??? Private Sub Setcolor() '不同數(shù)字不同顏色區(qū)分,當年不會控件編組,枚舉了16遍,16遍!
??????? Dim i As Integer
??????? For i = 0 To sqr ^ 2 - 1
??????????? Select Case tzfe(i).Text
??????????????? Case 0
??????????????????? tzfe(i).BackColor = Color.Silver
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 2
??????????????????? tzfe(i).BackColor = Color.Ivory
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 4
??????????????????? tzfe(i).BackColor = Color.LemonChiffon
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 8
??????????????????? tzfe(i).BackColor = Color.Khaki
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 16
??????????????????? tzfe(i).BackColor = Color.Coral
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 32
??????????????????? tzfe(i).BackColor = Color.Tomato
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 64
??????????????????? tzfe(i).BackColor = Color.OrangeRed
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 128
??????????????????? tzfe(i).BackColor = Color.Red
??????????????????? tzfe(i).ForeColor = Color.Black
?????????????? ?Case 256
??????????????????? tzfe(i).BackColor = Color.Yellow
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 512
??????????????????? tzfe(i).BackColor = Color.Gold
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 1024
??????????????????? tzfe(i).BackColor = Color.Orange
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 2048
??????????????????? tzfe(i).BackColor = Color.DarkOrange
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 4096
??????????????????? tzfe(i).BackColor = Color.GreenYellow
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 8192
??????????????????? tzfe(i).BackColor = Color.YellowGreen
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 16384
??????????????????? tzfe(i).BackColor = Color.DarkTurquoise
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case 32768
??????????????????? tzfe(i).BackColor = Color.CornflowerBlue
?? ?????????????????tzfe(i).ForeColor = Color.Black
??????????????? Case 65536
??????????????????? tzfe(i).BackColor = Color.Plum
??????????????????? tzfe(i).ForeColor = Color.Black
??????????????? Case Else
??????????????????? tzfe(i).BackColor = Color.Black
??????????????????? tzfe(i).ForeColor = Color.White
??????????? End Select
??????? Next
??? End Sub
?
兩個Label控件的觸發(fā)事件以及一個隱藏的Timer控件,組成了兩個隱藏功能,這是基于2048傳統(tǒng)玩法額外增加的功能。
第一個Click事件,將所有文本框的enabled屬性改為true,這樣可以自行鍵入數(shù)字了,比如敲進去一個3作為障礙物什么的,或者開掛直接敲一個2048達到目標,相應(yīng)做了一個輔助變量cheat來記錄開掛模式的開啟關(guān)閉狀態(tài)。
第二個MouseHover事件,鼠標懸停在那個2048標簽上一會,會觸發(fā)autoplay功能,啟動timer控件開始tick,并設(shè)置為半秒一次tick,每次tick會觸發(fā)后續(xù)的移動指令,這里不想額外定義變量名了,于是單純用timer的interval屬性來記錄aotuplay的開啟狀態(tài),如果是1000(1秒),證明當前秒表是停止的,將interval屬性設(shè)置為500(半秒)并啟動秒表,如果是500,證明當前秒表正在運行,將interval屬性設(shè)置為1000并停止秒表,最后做一次啟動判定,僅在游戲可以繼續(xù)的情況下(nomove=false)開啟秒表。
Timer控件的tick會按順序觸發(fā)“上右下左”順序的移動指令,仔細端詳一下,不難看出就是把先前鍵盤的keydown事件對應(yīng)代碼抄過來了,并用turn變量記錄接下來應(yīng)該移動的方向,總共4個方向,turn每次移動后自加1,除4取余數(shù)就代表了0、1、2、3四個方位代號,根據(jù)方位代號調(diào)用移動函數(shù)即可,代替了鍵盤的“上下左右”。
??? Private Sub Label1_Click(sender As Object, e As EventArgs) Handles Label1.Click
??????? Dim i As Integer
?????? ?If cheat = False Then
??????????? For i = 0 To sqr ^ 2 - 1
??????????????? tzfe(i).Enabled = True
??????????? Next
??????????? cheat = True
??????? Else
??????????? For i = 0 To sqr ^ 2 - 1
??????????????? tzfe(i).Enabled = False
??????????? Next
??????? ????cheat = False
??????? End If
??? End Sub
??? Private Sub Label1_MouseHover(sender As Object, e As EventArgs) Handles Label1.MouseHover
??????? If Timer1.Interval = 500 Then
??????????? Timer1.Stop()
??????????? turn = turn Mod 4 'autoplay跑久了turn變量太大,停下來時壓縮一下
??????????? Timer1.Interval = 1000
??????? Else
??????????? If nomove = False Then
??????????????? Timer1.Interval = 500
??????????????? Timer1.Start()
??????????? End If
??????? End If
??? End Sub
??? Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick '半秒一次移動
??????? If nomove = True Then
??????????? Timer1.Stop()
??????????? Timer1.Interval = 1000
??????????? turn = 0
??????? Else
??????????? Input()
??????????? Copy()
? ??????????Select Case turn Mod 4
??????????????? Case 0
??????????????????? Upmove()
??????????????? Case 1
??????????????????? Rightmove()
??????????????? Case 2
??????????????????? Downmove()
??????????????? Case 3
??????????????????? Leftmove()
??????? ????End Select
??????????? Copy2()
??????????? If Check2() = True Then '盤面挪動了,證明空出來一個格子,繼續(xù)隨機塞入數(shù)字
??????????????? Add()
??????????? End If
??????????? Output()
??????????? Setcolor()
??????????? Theend()
??????????? turn += 1
??????? End If
??? End Sub
?
以下代碼分別對應(yīng)四個按鈕的單擊觸發(fā)事件,Button1對應(yīng)的是“更改難度”(輸入新的難度數(shù)值,判定是否合法,然后更換diff變量),Button2對應(yīng)的是“清空記錄”(清零計分文件的數(shù)據(jù)),Button3對應(yīng)的是“關(guān)于作者”(一系列放飛自我的彈窗,順便介紹游戲規(guī)則和隱藏功能),這三個按鈕都是輔助功能,刪去不影響2048游戲的核心玩法。
Button4對應(yīng)的是“重新開始游戲”,功能比較單一,點擊后調(diào)用了一次restart()過程,將游戲初始化了。
??? Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
??????? Dim exp As String
??????? Dim diff2 As Byte
??????? exp = InputBox("2048難度可以改動,默認是7級,可以輸入新的難度等級(1-9級,等級越高,難度越高),后續(xù)游戲體驗會出現(xiàn)細微變化", "難度設(shè)定", 7,,)
??????? If exp = "" Then
??????? Else
??????????? diff2 = Val(exp)
??????????? If diff2 >= 1 And diff2 <= 9 Then
??????????????? diff = 10 - diff2
??????????? Else
??????????????? MsgBox("修改方案無法實現(xiàn),請重新輸入?yún)?shù)")
??????????? End If
? ??????End If
??? End Sub
??? Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
??????? IO.File.WriteAllText(Application.StartupPath & "\score", 0)
??????? MsgBox("記錄已清空")
??? End Sub
??? Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
??????? Restart()
??? End Sub
??? Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
??????? MsgBox("軟件作者:菡萏芳菲")
??????? MsgBox("本軟件所有代碼由本人所編,版權(quán)所有,不得抄襲")
??????? MsgBox("軟件免費,祝各位玩的開心")
??????? MsgBox("[>---游戲規(guī)則---<]")
??????? MsgBox("游戲初始,方格內(nèi)會出現(xiàn)2或者4這兩個小數(shù)字")
??????? MsgBox("玩家只需要上下左右(方向鍵控制)其中一個方向來移動出現(xiàn)的數(shù)字")
??????? MsgBox("所有的數(shù)字就會向滑動的方向靠攏")
??????? MsgBox("而滑出的空白方塊就會隨機出現(xiàn)一個數(shù)字")
??????? MsgBox("相同的數(shù)字相撞時會疊加")
??????? MsgBox("然后一直這樣,不斷的疊加最終拼湊出2048這個數(shù)字就算成功,當然,分數(shù)多多益善")
??????? MsgBox("你的最高分數(shù)將會被記錄")
??????? MsgBox("點擊2048進入作弊模式,鼠標懸停在2048上一段時間后進入自動移動模式")
??????? MsgBox("2048,創(chuàng)造游戲新潮流!")
??? End Sub
End Class
<——<代碼搬運結(jié)束>——>
六、創(chuàng)新點
未來的每一款軟件出專欄,我也都會列舉一些我編程過程中發(fā)現(xiàn)的問題,經(jīng)過查詢論壇、網(wǎng)站均沒發(fā)現(xiàn)很完整的論述后,我會自行調(diào)參做一點點研究。2048這個游戲邏輯比較簡單,我中途僅發(fā)現(xiàn)了一個特別的討論論題,就是2、4出現(xiàn)頻次的概率分布,注:后續(xù)所稱X:Y的比例,意為2的出現(xiàn)次數(shù)X與4的出現(xiàn)次數(shù)Y的比值,不考慮出現(xiàn)8的異種2048版本。
為此,我調(diào)整1~9的參數(shù),經(jīng)歷了漫長的一下午2048爆搓,瞌睡過后,從實踐中找到了這個參數(shù)的影響力。隨后又蒙特卡洛模擬了一下,稍微對我總結(jié)出的理論做了一點點建模驗證,可以得出一個能說出一點點道理的結(jié)論了。
網(wǎng)上對這個概率分布可謂是幾乎只字不提,估計也很少程序員認真到考慮代碼的每一條參數(shù)了。我沒有看過源代碼的詳細算法,但據(jù)網(wǎng)上的只言片語描述,好像使用的是1:9比例生成,實際比較流行的移動端2048游戲生成比例我實際玩感覺在1:6的水平,而最早期我不假思索寫的比例是1:1(后來看一些教學短篇文章,也很多是不假思索寫了1:1),游玩過程發(fā)現(xiàn)了莫名的體驗差異,才發(fā)現(xiàn)了這個論題。
本篇教學,我給了3:1的生成比例,是我經(jīng)過理論推理,感覺可能是比較好玩的一個比例了。原因大體是這樣的:
生成2和4比例會影響接下來可持續(xù)游戲的概率
在近乎填滿的狀態(tài)下,2和4交替頻繁易導致游戲失敗,直觀感受是“冒不出自己想要的數(shù)字”
在空缺較多的狀態(tài)下,2或4重復出現(xiàn)易導致游戲策略性降低,直觀感受是“不停朝一個方向移動”
2048的難度根據(jù)這個比例會發(fā)生微妙的變化,在上一步2+2=4后,希望生成4進一步合并,當沒有2+2時,希望下一步生成2去合并
以上兩種情況到底那種多,又和玩游戲策略有關(guān),難以界定,所以難度也不是線性的,具體分析其特征,作者稱之為《2048理論》。
選擇3/1理由:
首先,直觀推理可得,2+2得4,那么此時生成4最不卡手,2、2、4序列,是2/1的配置,令人滿意
2、4、2、4交替會比較卡手,2、2、4、4交替比較舒服,可以發(fā)現(xiàn)1/1配置兩極分化
我們從1/1到9/1分別作了模擬數(shù)據(jù),詳見數(shù)據(jù)表(專欄這里我就不放了,是我做的一個亂七八糟的excel模型,可讀性為0),發(fā)現(xiàn)了一些有趣的規(guī)律
把可能出現(xiàn)的組合拆分為ABA、AAB與ABB、AAA三種配列
ABA 242 424 劣 劣
AAB 224 442 優(yōu) 劣
ABB 244 422 劣 優(yōu)
AAA 222 444 優(yōu) 優(yōu)
模擬1000次結(jié)果,可得這三種模型次數(shù)累計加起來是998次,其中三種模型出現(xiàn)次數(shù)存在不同分布,按5的倍數(shù),做了一下頻率比例近似
ABA:(AAB+ABB):AAA
對于1/1,25:50:25
對于2/1,25:45:30
對于3/1,20:40:40
對于4/1,15:30:55
對于5/1,15:25:60
對于6/1,15:20:65
對于7/1,10:25:65
對于8/1,10:20:70
對于9/1,10:15:75
真正玩起來,就能發(fā)現(xiàn)端倪了
ABA太多,會經(jīng)??ㄊ?,會經(jīng)常出現(xiàn)“局點”的緊迫環(huán)節(jié),導致頻繁破局思考,心累,令人沮喪;
AAA太多,大量的2讓這個游戲高度可預測,缺乏盤面變化,一直2+2=4推,玩著枯燥,推著推著就極限了,缺少懸念,萬一真偶爾蹦個4,心態(tài)爆炸;
ABB兩級分化,224或422,雪中送碳,442或244,后續(xù)乏力,超級卡手,游戲體驗55開;
這樣我們就有個傾向性了,可以初步設(shè)立幾條原則
1. 避免ABA占比太大
2. ABB、AAA合理穿插帶來樂趣,AAA提供正反饋,ABB/AAB提供盤面變數(shù)
然后看看模擬數(shù)據(jù)表總結(jié)的比例結(jié)果
ABA從3/1后占比迅速下降,前幾種比例分布中,尤其是1/1的變數(shù)很多很雜,不可取,完美解釋了當時第一版2048做出來后,玩不順暢的原因
AAA從3/1后占比迅速上升,可預測性直線增加
綜上,我發(fā)現(xiàn)3/1比較舒服,ABA下降得差不多了,但又不是很少見,游戲難度保證,ABA和AAA占比類似,實際AAA還高一點,勞逸結(jié)合,保證游戲體驗和正反饋。
七、結(jié)語
至此我們的《2048》編程教學就告一段落了,如有疏漏還請各位讀者指正,如果有不錯的建議,我會在下一期教學中調(diào)整我的內(nèi)容框架。如果讀者你還是一個剛選修程序課的初學者,請盡情“參考”本專欄內(nèi)容用來交作業(yè),希望你不是直接抄走了代碼就交了~這個程序加上我另外開發(fā)的一個“小”游戲為我?guī)砹艘粋€100分的VB課選修成績,還是很滿意的,希望也對你們現(xiàn)在的課程有幫助(現(xiàn)在應(yīng)該沒人學VB了吧,吧?)
未來預告,截至本期專欄,我手里累計開發(fā)了7款游戲/應(yīng)用程序,有2048、鋪地板、數(shù)獨、貪吃蛇、德州撲克發(fā)牌器兼計算器、拼豆圖案設(shè)計器、塔羅牌占卜板,都是閑得冒泡的時期拍腦袋拍出來的,現(xiàn)在可能沒那么閑再開發(fā)新游戲了(主要是人菜),它們涉及到更多的邏輯算法、更復雜的UI交互、更多的創(chuàng)新論題和分析成果,寫起來著實體量龐大,我盡力陸續(xù)把它們寫成教學專欄呈現(xiàn)出來,敬請期待,我們“明年”專欄再見啦(我發(fā)現(xiàn)2048都要寫好長的篇幅,還這么一點點功能,帶彩蛋功能一共就446行代碼,后續(xù)真是不一定寫得動呀~咕咕咕咕咕)!