-- 關(guān)于這個桌游 --
開始之前先介紹一下要開發(fā)的這個游戲,它叫Quoridor。如果你是一個深度桌游愛好者,那你可能會知道它的中文版本——步步為營/圍追堵截。
先上一下它美美的劇照厌处。
Quoridor規(guī)則非常簡單,一個人有一個棋子岁疼,十塊木板阔涉。輪流下棋,每次輪到你呢就可以選擇移動你的棋子或者放置一塊木板到棋盤中捷绒。
棋子的移動上下左右瑰排,而且不能跳過木板阻隔的位置。當(dāng)有對手在你身邊的時候暖侨,你可以借助他從而走多一步凶伙。
木板可以任意放置,但是它碎,必須給對手至少留下一條活路函荣。
但是,越簡單扳肛,越不簡單傻挂。反正,從第一次玩這個游戲挖息,我就徹底迷上了金拒。那時候,我還不會iOS開發(fā)呢套腹,就對自己說绪抛,我以后,要把這游戲电禀,實現(xiàn)到手機上去幢码。
-- 關(guān)于這個游戲 --
地址先奉上。Github-Quoridor
當(dāng)然尖飞,我并不是第一個把這個游戲?qū)崿F(xiàn)的人症副。甚至,我不是第一個在iOS平臺上實現(xiàn)這個游戲人政基。就我所搜索到的贞铣,這個游戲有Java版本,C++版本沮明,Javascript版本辕坝。
不過,倒霉的事情是荐健,我沒有能讓我下載到的任何一個版本成功運行起來酱畅。(T-T)天啦擼琳袄,這是何等的悲傷。加之我對以上三種語言都是處在勉強能閱讀的程度圣贸,對于整個程序挚歧,Hold不住呀扛稽。(本來找到C++版本好開心的吁峻,然而里面用了好多我連搜索都搜索不出來是什么意思的語句,就……跪了在张。)
所以用含,只好全盤自己寫了。
當(dāng)然在開始之前帮匾,我需要先說明啄骇,我也只能算一個小白。所以如果你覺得我代碼寫得一塌糊涂瘟斜。我十分熱烈的歡迎你對我鄙視然后寫一個牛逼的版本讓我拜讀缸夹,我將感激不盡。
-- 游戲UI --
為了能讓我后面開發(fā)起來有的放矢螺句,知道自己在寫什么虽惭,所以我習(xí)慣先畫個UI出來。
我用Sketch蛇尚,畫了一個很簡單的界面芽唇。
如大家所見,就是中間棋盤取劫,兩邊一個悔棋匆笤,一個重新開始,還有木板數(shù)量谱邪,到達(dá)步數(shù)炮捧。簡簡單單的游戲界面就這樣出來了。
我承認(rèn)這并不好看惦银,我也承認(rèn)這設(shè)計有點屎寓盗,但是!我會努力的璧函。歡迎提建議傀蚌。
-- 游戲結(jié)構(gòu) --
先丟上游戲的程序結(jié)構(gòu)。
沒錯蘸吓,這是一個經(jīng)典的MVC設(shè)計模式善炫。
畢竟游戲結(jié)構(gòu)很簡單,我們沒必要把它復(fù)雜化库继。
-- 開始構(gòu)建游戲 --
我喜歡從UI開始構(gòu)建一個應(yīng)用箩艺,而且我喜歡用Storyboard窜醉。(曾幾何時,我一直覺得純代碼構(gòu)建整個應(yīng)用才是最酷的艺谆,但是當(dāng)我對一個應(yīng)用的UI進(jìn)行了幾次改動后榨惰,我就體會到這種酷是有代價的。)
所以静汤,上UI截圖琅催。
納尼!一片空白虫给,啪藤抡!回水!
冷靜冷靜抹估,注意看缠黍,其實這個UI界面還是有很多視圖的。只不過我都把他們的顏色給選擇clearColor了药蜻。原因也很簡單瓷式,我需要圓角,而最簡單的實現(xiàn)圓角的辦法就是用代碼修改UIView的Layer语泽。而且這樣做的好處也很多贸典,比如,我可以很容易的就進(jìn)行主題顏色的切換湿弦。
我先放個大圖然后后面講我的思路瓤漏。
0.首先游戲由三個部分組成。
棋盤颊埃;
Top控制面板蔬充;
Down控制面板;
每個控制面板中四個按鈕班利。
于是Center饥漫、Top、Down Background確定罗标。
它們都由一個Background類庸队,來繪制一個邊框以及一個圓角。
1.由于游戲要顯示當(dāng)前輪到誰了闯割,我的做法比較粗暴彻消,就是輪到你,才顯示你的控制面板宙拉。否則就把它蓋起來宾尚。
于是又多了兩個視圖,Top、Down Screen煌贴。它們跟Top御板、Down Background完全對齊。
作用就是輪到你時牛郑,就隱藏起來怠肋,讓你可以看到自己的控制面板,否則就顯示出來淹朋,當(dāng)個馬賽克笙各。
它們都由Screen類控制,以顯示對應(yīng)內(nèi)容還有顯示消失等控制時的動畫瑞你。
2.游戲棋盤是Chess Board酪惭。
由ChessBoard控制希痴,其實也很簡單者甲,就是畫9*9個小格子。
它跟Center Background對齊砌创,但是比它小10個像素虏缸。
至于為什么不把它放在Center Background里面呢,因為……我不喜歡嫩实。
3.Chess Wall層的工作是繪制當(dāng)前的墻壁刽辙。
4.Player Prompt層是用來在你提起棋子時顯示當(dāng)前可以移動的位置的圖層。
5.Chess Player層就是繪制棋子的圖層啦甲献,里面有兩個Layer宰缤,各自表示一方棋子。由于各種原因晃洒,這個游戲只能兩個人玩慨灭。
6.Wall Prompt,顧名思義球及,就是要放置墻壁時用來提示墻壁位置的氧骤。它會在墻壁到不可放置時變成紅色。
7.Touch View吃引,這就是用來接收玩家觸摸位置信息的圖層筹陵。上面說到的所有的類都是屬于Views當(dāng)中的類。唯有TouchView我是把它放在Controllers當(dāng)中的镊尺。因為在我看來朦佩,它的角色就是一個控制器。實際上庐氮,它也不顯示任何東西语稠,只是單純的接收觸摸位置,然后進(jìn)行簡單的分析之后旭愧,通過Delegate設(shè)計模式將觸摸事件反饋給GameController颅筋。
當(dāng)然宙暇,你要說這是一個View也是對的。
8.EndScreen议泵。這是用來顯示游戲結(jié)束動畫的圖層占贫。
好啦。至此先口,整個游戲的UI我們就構(gòu)建好了型奥。請原諒我只是介紹了一下每個圖層的作用。因為對應(yīng)的代碼你都可以找到碉京。而且我在當(dāng)中也有相應(yīng)的注釋厢汹。并且每個類也都讓我用各種MARK把一個個功能劃分開來。如果你有不明白的地方谐宙,也歡迎來吐槽一下烫葬。
-- 構(gòu)建游戲模型 --
游戲的Models一共有五個文件,但是其實只有3個類凡蜻。
我不想讓GameModel看起來太過臃腫搭综,就用了擴展把它區(qū)分成幾個文件,希望能讓他看起來更加清晰一點划栓。
從最基礎(chǔ)的部分說吧兑巾。
-- DataModel --
顧名思義,這就是整個游戲最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)。
用他可以來表示棋子,木板兩種元素致盟。
主要內(nèi)容無比簡單糜颠,就是:
// MARK: - Data
/* 數(shù)據(jù)具體坐標(biāo)和類型 */
/** x軸坐標(biāo) */
var x: Int { didSet { updateId() } }
/** y軸坐標(biāo) */
var y: Int { didSet { updateId() } }
/** Horizontal: 如果是木板,則表示橫向horizontal與豎向vertical。如果是棋子,則表示頂方Top與下方Down。 */
var h: Bool {
didSet {
if t {
updateWallIds()
}
}
}
/** Type: 類型称诗,True表示木板,F(xiàn)alse表示棋子头遭。 */
var t: Bool
我還給他寫了另外兩個屬性寓免,用來方便計算游戲邏輯以及Ai的時候使用。你可以注意到计维,這里我沒有使用計算屬性袜香,而是利用屬性監(jiān)視器在XY坐標(biāo)改變時更新它們。因為鲫惶,我覺得蜈首,比起變更坐標(biāo)。調(diào)用輔助屬性的次數(shù)會更多,如果每次調(diào)用都進(jìn)行計算欢策,無疑是會比較消耗CPU的吆寨。加上,這也耗不了多少內(nèi)存踩寇,所以就這樣做了啄清。
原諒我對CPU占用如此敏感,因為我第一個Ai計算一步棋需要四分鐘俺孙,所以……我……
// MARK: Count Data
/* 不進(jìn)行存儲的輔助數(shù)據(jù) */
/** 棋子坐標(biāo) */
var id: Int = 0
/** 墻壁坐標(biāo) */
var wallIds: [Int] = [Int]()
此外辣卒,我還給他寫了一些Copy方法,還有轉(zhuǎn)換方法睛榄,都是為了調(diào)用方便荣茫。有興趣你可以看看。
-- GameModel --
這部分就是真正的重頭了场靴。
這個類第一部分是Interface啡莉。這是我為了方便自己進(jìn)行調(diào)用而寫的函數(shù)。我覺得這樣做其實挺違背Swift的初衷的憎乙,畢竟它把接口文件都取消了票罐,我還自己寫了一個接口部分叉趣。但是我覺得這樣做泞边,能讓我在各個對象之間調(diào)用的時候思路更加清晰。
每一個類都專注做自己的事情疗杉,就好像一個人一樣阵谚,并且有特定的與其他類進(jìn)行交互的內(nèi)容。這樣我覺得很好烟具。至少我是這樣理解面向?qū)ο箝_發(fā)的——程序的本質(zhì)是各種對象協(xié)作方式的體現(xiàn)梢什。(自言自語,如有雷同朝聋,我深感榮幸嗡午。)
這是一個單例類,游戲模型嘛冀痕,你懂的荔睹。
// MARK: - Singleton Class
/* 創(chuàng)建游戲模型的單例類,并禁止其他類初始化該方法 */
/** 游戲模型的單例 */
static var shared = GameModel()
/** 私有化初始化方法 */
private override init() {
super.init()
initModelData()
}
各個接口的意義我都有注釋言蛇,所以大家看看就好僻他。
主要的內(nèi)容包括了這么些部分:
0.控制游戲主題顏色的變量
// MARK: 色彩方案
/** Color Model */
var color: Bool = true
1.代表當(dāng)前游戲的玩家數(shù)據(jù)信息
// MARK: 玩家數(shù)據(jù)
/** Top Player Data */
var topPlayer: DataModel = DataModel()
/** Down Player Data */
var downPlayer: DataModel = DataModel()
/** Top Walls Data */
var topWalls: [DataModel] = [DataModel]()
/** Down Walls Data */
var downWalls: [DataModel] = [DataModel]()
/** All walls data */
var allWalls: [DataModel] { return topWalls + downWalls }
/** Ai is open */
var gameAi: Bool = true
2.棋譜數(shù)據(jù),悔棋的時候會用到腊尚。也可以保存游戲數(shù)據(jù)吨拗,但是由于一盤棋很短,所以我暫時沒有做保存的功能。感覺需求不大劝篷。
// MARK: 棋譜
/** Chess manual Stack */
var gameStack: [DataModel] = [DataModel]()
3.游戲數(shù)據(jù)哨鸭。
包括當(dāng)前輪到哪個玩家。游戲的狀態(tài)娇妓,是誰贏了兔跌。還是依舊在進(jìn)行中。實際上這個游戲是有平局的峡蟋。但是目前還沒有體現(xiàn)坟桅。計算方式非常普通,稍微看看代碼就可以明白的蕊蝗。
// MARK: 游戲數(shù)據(jù)
/** The current player */
var player: Bool {...}
/** The game state */
var status: GameStatus {...}
還有很重要的兩個數(shù)據(jù)仅乓。
棋盤通道記錄,我把每一個棋盤的小方塊用0-80共81個數(shù)子來表示蓬戚,然后記錄每一個點可以行走的下一步的范圍夸楣。
墻壁數(shù)據(jù)。我把棋盤從9x9.加上墻壁的槽子漩,變成了17x17的大棋盤豫喧。每一個墻壁都會占據(jù)三個點。這樣就可以防止出現(xiàn)墻壁交叉的情況幢泼。
這兩個數(shù)據(jù)的存在依然是為了以空間換時間紧显。我對于Ai部分有深深的恐懼感,為了能夠更快的計算出合適的棋步缕棵。只好這樣了孵班。
/** 棋盤通道記錄 */
var gameNears: [[Int]] = [[Int]]()
/** 墻壁數(shù)據(jù)記錄 */
var gameWalls: [Bool] = [Bool]()
老樣子,看圖說話:
GameModel+Action 文件基本上是對游戲接口函數(shù)的實現(xiàn)招驴。假如在以后有什么游戲操作行為的函數(shù)篙程,都會放到這其中。
GameModel+Logic 是一個計算當(dāng)前棋子的可移動距離的函數(shù)别厘。以后有需要計算的邏輯類型的內(nèi)容也都會在這里面虱饿。
-- GameAi --
恩。這個触趴,故名思議氮发,游戲的Ai是這個文件計算出來的〉癖危……我放到最后面才說游戲Ai部分折柠,因為它是我最后實現(xiàn)的。同時批狐,它也是到現(xiàn)在都一直不斷在改進(jìn)的地方扇售。
-- Controller --
一共就三個文件前塔。
FrameCalculator文件是一個坐標(biāo)計算器,是為了給View他們使用的時候考慮一些偏移之類的情況承冰。我統(tǒng)一把這部分的計算函數(shù)放在一個新的類里华弓。由于,這部分本來應(yīng)該是Controller的東西困乒,所以放在這個目錄之下寂屏。
(到這里,可能要有人說娜搂,我已經(jīng)把MVC設(shè)計模式丟到某個不知名的星球上去了迁霎。確實我承認(rèn)這一部分我做得不是很嚴(yán)謹(jǐn),但是百宇,我認(rèn)為MVC設(shè)計模式的精髓是讓我們區(qū)分好整個程序的功能架構(gòu)考廉。分成三個部分,一部分管理數(shù)據(jù)携御,一部分管理視圖昌粤,一部分協(xié)調(diào)數(shù)據(jù)與視圖之間的關(guān)系。所以啄刹,我認(rèn)為涮坐,只要能讓別人一眼看出來,我哪些文件是負(fù)責(zé)哪一塊功能誓军,并且文件之間的關(guān)系是怎么樣的袱讹,那就足夠了。畢竟谭企,后來新興起的各種設(shè)計模式廓译,也都是在MVC的基礎(chǔ)上發(fā)展而來的,無非债查,就是把各個模塊進(jìn)行細(xì)分罷了。目的瓜挽,也就是一個盹廷,清晰思路。)
TouchView就是一個接收器久橙,前面提到過了俄占。
GameController是一個正兒八經(jīng)的控制器。打開它的索引淆衷,你可以看到缸榄,盡管我已經(jīng)將它的很多功能都切出去了。但是依然很長很長祝拯。
我們一點點來甚带。
-- Controller Life Cycle --
這個很好理解她肯,就是生命周期的控制。而這里關(guān)鍵就是一些初始化配置鹰贵。你可以看到晴氨,這里我就是調(diào)用一個個函數(shù),而不是直接進(jìn)行各個控件之間的設(shè)置碉输。
這個是我的一個習(xí)慣籽前,不知道算好還是算壞。
我喜歡用MARK吧各個功能模塊劃分出來敷钾。
比如下一個GameLogic模塊枝哄,就是控制了游戲邏輯的代碼。
再下一個Buttons阻荒。這個模塊中可以找到所有的按鈕初始化函數(shù)膘格,按鈕事件反饋。
每個模塊我都將它當(dāng)成一個獨立小對象來對待财松。這樣的劃分讓我可以非常直觀的立刻查詢到我所想要的內(nèi)容瘪贱。而且,極大的減少了我更改某一個功能模塊的代碼時辆毡,對其他模塊的影響菜秦。
// MARK: Init Game
/** 初始化游戲啟動配置。 */
private func gameLaunch() {
initDemonstrationView()
initSound()
}
/** 更新游戲顯示內(nèi)容 */
private func gameAppear() {
alignmentButtons()
updateButtonsTitle()
updateGameColor()
initTouchView()
updateScreen()
chessPlayer.update(true)
chessPlayer.update(false)
}
那么舶掖,按照我的這種邏輯球昨。
整個控制器其實就很簡單了。
它的功能實現(xiàn)主要就是通過實現(xiàn)TouchViewDelegate的函數(shù)眨攘,接收到觸摸事件后主慰,將觸摸點分發(fā)給對應(yīng)的視圖,讓他們做出反饋鲫售。
當(dāng)觸摸結(jié)束時共螺,判斷是否一次合理的操作。不是就直接結(jié)束操作情竹。
是的話就更新GameModel數(shù)據(jù)藐不。
然后調(diào)用Game Logic當(dāng)中的changePlayer函數(shù)。
changPlayer函數(shù)負(fù)責(zé)更新視圖秦效,并且判斷游戲是不是結(jié)束了雏蛮。
是不是下一步要計算機來下棋了。
-- 說回GameAi --
好了阱州。終于挑秉,回到這個話題了。其實我寫了這么長的篇幅苔货,我就想跟大家聊聊這個犀概。
因為……臣妾真的做不到A⒀啤!阱冶!
-- 0.0版 --
棋盤類游戲的Ai刁憋,相信很多會立馬想到,做一個博弈樹木蹬,然后根據(jù)阿法貝塔剪枝技術(shù)進(jìn)行優(yōu)化至耻。所以,很簡單啊镊叁。
我一開始也是這么的覺得的尘颓,于是我以廣度優(yōu)先算法計算出最短路徑,然后以最短路徑以及木板數(shù)量作為評估方式晦譬。
Ai開始計算的時候疤苹,直接羅列出所有可能的落子范圍,對每一個點進(jìn)行評估敛腌,計算出最大值卧土,然后對最大值進(jìn)行下一層評估,獲取最小值像樊。(極大極小算法)
中規(guī)中矩但是非常實用尤莺。一開始我只讓他計算兩層,即已方下一步生棍,對方下一步颤霎。而且,由于我知道這游戲不能簡單的極大極小涂滴,最短路徑很可能是對方給你留下的坑友酱,所以我放棄了剪枝。
于是柔纵,我愉快的開始游戲了缔杉。
然后,等了4分鐘首量,它終于動了壮吩。好極了,Xcode上顯示Cpu占用100%4分鐘加缘,如果我用的是手機,那這時候雞蛋都熟了觉啊。
于是我仔細(xì)查看了一下層序拣宏,發(fā)現(xiàn),邏輯沒有任何錯誤杠人。錯就錯在勋乾,每一步棋宋下,需要計算的可能解是132個,計算兩步就是132個132次方辑莫。天啦擼……而且由于每次計算都要考慮是否封死對方的路徑学歧,就是需要進(jìn)行兩次廣度優(yōu)先計算。而且在當(dāng)時棋子可能解各吨,墻壁可能解這些我還都是直接每次都進(jìn)行運算得出的枝笨,沒有用空間換時間。所以……
雖然剪枝后會好點揭蜒,但是說實話横浑,剪枝后基本上跟瞎下也沒有太大差別……
怎么辦呢?
蒙特卡羅算法屉更?
遺傳算法徙融?
蟻群算法?
大數(shù)據(jù)瑰谜?
-- 1.0版 --
我仔細(xì)的考慮過后欺冀,感覺,問題應(yīng)該出在局面上萨脑。
因為很多位置的墻壁是可以不需要考慮的隐轩。
而且,不管是什么樣的算法砚哗,如果我沒有辦法給出一個高效而且具有實際意義的局面評分龙助,那都無法算出有效的棋路。
于是蛛芥,我這一次Ai的改進(jìn)集中在棋局評估上提鸟。
然后我想到,為什么不直接阻擋對方的最短路徑呢仅淑?
所以称勋,這一次Ai總算靠譜點了,只對對方的最短路徑進(jìn)行運行涯竟。查找能夠最讓對方惡心卻又不影響自己的最短路徑的一個位置放木板赡鲜。(也就是,計算木板對對方最短路徑的影響庐船。取影響最大的那一塊银酬。)玩起來已經(jīng)有在下棋的感覺了。
然后我做了一個開局函數(shù)筐钟,讓它前幾步都是隨機放置的揩瞪,而到了中期就根據(jù)前面的隨機木板進(jìn)行運算,從而達(dá)到不會說一直重復(fù)一個下法的效果篓冲。
加上我改進(jìn)數(shù)據(jù)結(jié)構(gòu)李破,空間換時間宠哄,空間換時間,空間換時間嗤攻。好極了毛嫉。快到一瞬間的事情妇菱。
然而承粤,這家伙只會惡心對方,卻完全不可能贏呀恶耽。因為它會傻傻的把所有木板一次性放置進(jìn)去密任。但是就我對這游戲的理解,木板就好像核武器偷俭,你哪怕留著一塊對對方都是一個威脅浪讳。在開局被對方堵到吐血,等對方用光木板后把它當(dāng)豬一樣殺是非常簡單的事情涌萤。
然而淹遵,這個版本的Ai就是那頭豬。
-- 2.0版 --
我覺得我應(yīng)該考慮一下出現(xiàn)多路徑的情況负溪。
就是當(dāng)計算機知道自己現(xiàn)在有兩條路透揣,一條可以五六步到達(dá)終點,另一條是十多步到達(dá)的時候川抡,它應(yīng)該有意識地去切斷自己的長路徑辐真,從而進(jìn)行防守。
這是每個正常玩家都可以想到的事情崖堤。
那么侍咱,如何計算出來多路徑呢?
翻遍了算法書密幔,我也沒有找到可以現(xiàn)成使用的算法……當(dāng)然楔脯,也可能是我翻的書不夠多,如果我翻到了胯甩,我回來自己扇自己臉昧廷。
要知道,我要計算多路徑偎箫,是計算每一個可能解的最短路徑木柬。中間不能在棋盤上亂繞淹办。所以如何讓他在遇到分叉路口的時候會自動產(chǎn)生分支呢弄诲?
-- 2.1版 --
我在看到不少拓?fù)渑判虻膽?yīng)用后,忽然感覺有個靈感……(看完別問我這跟拓?fù)渑判蛴惺裁搓P(guān)系娇唯,我也不知道齐遵,但是我就是這樣來靈感的。)
我可以在廣度優(yōu)先算法的計算上塔插,每次進(jìn)行擴展時梗摇,對擴展出來的點進(jìn)行分析,如果他們是相鄰的想许,則放在一個集合中伶授。如果他們不相鄰,這產(chǎn)生多個集合流纹。
然后糜烹,每個新的集合都會變成一個新的分支,然后進(jìn)行迭代處理就可以查到到所有的路徑了漱凝。
示意圖如下疮蹦,以46號點作為棋子的位置。
第一次擴展會擴展出來37茸炒,45愕乎,55號點得位置。他們都是相鄰的壁公。
第二次則是56感论,64,54 以及 36紊册,28五個點比肄。我們可以明顯的看到他們并不相鄰。于是囊陡,進(jìn)行分支芳绩。如表格1-1與1-1-1.
其中1-1.在進(jìn)行了又一次擴展后,找到了重點位置关斜,于是返回最短路徑示括。
1-1-1.則繼續(xù)運行,并且在上方會又產(chǎn)生新的分支痢畜。
最終產(chǎn)生這樣的路徑圖譜垛膝。
我一看樂了。多路徑有了丁稀,連路徑寬度都有了吼拥。當(dāng)時有種走上人生巔峰的感覺。
于是线衫,我趕緊實現(xiàn)這個算法凿可。
然而……實際測試中,這樣出現(xiàn)的問題還真不小……我每次進(jìn)行分支都會繼承上一個分支所查詢過的點,而由于這個點是斜方向擴展的枯跑,很可能會把一些回路給遮擋掉惨驶。導(dǎo)致本來有多個路徑,結(jié)果計算出來只有1個路徑敛助。
截止到目前粗卜,我還在往這個方向努力中。其實寫了這篇文章主要的目的有兩個纳击,一個是理清我自己的思路续扔。另一個是希望能有更多對這個游戲感興趣的朋友一起來改進(jìn)它。我相信有更多的人進(jìn)行思想碰撞后焕数,肯定可以得到更多更棒的想法纱昧。
第一個目的我達(dá)成了,因為寫到這里堡赔,我已經(jīng)多出來了很多想法识脆,包括如何改進(jìn)多路徑查詢。然后利用這樣的局面評分做到準(zhǔn)確的判定加匈。以及如何利用棋局記錄來做一個學(xué)習(xí)型的Ai存璃。
第二個目的就看你的了。棋盤擺好了雕拼,我們來下一局纵东?
對了,我真不是標(biāo)題黨啥寇,這個應(yīng)用我已經(jīng)申請上架了偎球。審核中~當(dāng)然,這是免費的游戲辑甜。因為作為一款游戲衰絮,它欠缺趣味性,欠缺美感磷醋,欠缺各種各種……但是猫牡,作為一個應(yīng)用,它讓我學(xué)習(xí)到了好多東西邓线。
歡迎各位吐槽淌友,同時,也希望大家能來碰撞一下骇陈。我還在學(xué)習(xí)震庭,也在學(xué)習(xí)如何學(xué)習(xí)。希望大家能指點一二你雌。