如何存儲(chǔ)當(dāng)前棋局
方案有3種:
象棋一共32個(gè)棋子,每個(gè)棋子有91種狀態(tài):死亡或位于0-89中任一位置。所以用長(zhǎng)度為32的列表即可,每個(gè)數(shù)的值域是0-90,其中90代表死亡。死亡的棋子不再占用空間,使用類似map的結(jié)構(gòu),key是棋子id,value是棋子位置(0-89)。壓縮空間的方案:將帥個(gè)子有9個(gè)可能在的位置,只需要0-9即可表示,需要至多5位二進(jìn)制。士有5種位置,每個(gè)士只需要至多3位二進(jìn)制。以此類推……占用空間最少。分析方案一:數(shù)組長(zhǎng)度為32,每個(gè)數(shù)組項(xiàng)目是個(gè)uint8,總共8 * 32 = 256 位。
分析方案二:在棋子多的時(shí)候,占用空間較多,所以存儲(chǔ)空間的大小不太穩(wěn)定。
(資料圖片)
方案三占用空間少,但是開發(fā)成本也較高,需要開發(fā)者去拼接二進(jìn)制位。
今天我們探討方案三。
前綴碼
引入一個(gè)概念:Prefix-Free Code
,也可以叫 Prefix Code
,它來(lái)源于信息論學(xué)科,維基百科:en.wikipedia.org/wiki/Prefix… 描述如下:
A prefix codeis a type of code system distinguished by its possession of the "prefix property", which requires that there is no whole code word in the system that is a prefix (initial segment) of any other code word in the system. It is trivially true for fixed-length code, so only a point of consideration in variable-length code.
For example, a code with code words {9, 55} has the prefix property; a code consisting of {9, 5, 59, 55} does not, because "5" is a prefix of "59" and also of "55". A prefix code is a uniquely decodable code: given a complete and accurate sequence, a receiver can identify each word without requiring a special marker between words. However, there are uniquely decodable codes that are not prefix codes; for instance, the reverse of a prefix code is still uniquely decodable (it is a suffix code), but it is not necessarily a prefix code.
它舉了個(gè)例子,針對(duì)集合{9, 5, 59, 55}就不是 prefix code,因?yàn)椤?」有二義性,遇到5后,不知道該結(jié)束流程,還是繼續(xù)讀取后面的9或5。
哈夫曼編碼 Huffman Coding
信息論中有個(gè)經(jīng)典問題:給定一篇文章,如何用最短的二進(jìn)制編碼它。
解決方案就是:找出出現(xiàn)的所有單詞集合(例如:I am good good good,出現(xiàn)了3個(gè)單詞),計(jì)算每個(gè)單詞出現(xiàn)頻率,以某種方式,構(gòu)造每個(gè)單詞對(duì)應(yīng)的二進(jìn)制編碼,滿足條件:基于前綴就能知道它代表哪個(gè)單詞。然后我們把這些前綴拼在一起,就成功編碼了(并且是可以解碼的)。
例如這種編碼 good = 0, I = 10, am = 11,文章就表示為1011000。
這是最簡(jiǎn)短的編碼了。構(gòu)造方法就是通過(guò)構(gòu)造一顆哈夫曼樹,算法如下:
針對(duì)每一個(gè)單詞(或組合),都有一個(gè)對(duì)應(yīng)的頻數(shù),作為頻數(shù)表。如果當(dāng)前只有1個(gè),就進(jìn)入4,否則進(jìn)入2。找到頻數(shù)最低的2個(gè),作為表示一個(gè)組合,他們對(duì)應(yīng)的頻數(shù)就是兩個(gè)單詞(或組合)的頻數(shù)之和,加入頻數(shù)表(同時(shí)刪除這2個(gè)單詞或組合各自的頻數(shù))。選取的2個(gè)單詞(或組合),分別作為左子樹和右子樹,組成一個(gè)樹。進(jìn)入1?,F(xiàn)在得到了一個(gè)二叉樹(叫做哈夫曼樹),每個(gè)葉子結(jié)點(diǎn)代表一個(gè)單詞。規(guī)定左分叉為0,右分叉為1,這個(gè)單詞對(duì)應(yīng)的Prefix Code
就是根節(jié)點(diǎn)到它的路徑。例如上述編碼對(duì)應(yīng)的哈夫曼樹就是:
對(duì)于象棋的啟發(fā)
回到象棋棋盤狀態(tài)的問題:
將帥有10個(gè)位置(包括死亡狀態(tài))。士有6個(gè)位置(包括死亡狀態(tài))。象有8個(gè)位置(包括死亡狀態(tài))。馬有91個(gè)位置(包括死亡狀態(tài))。車有91個(gè)位置(包括死亡狀態(tài))。炮有91個(gè)位置(包括死亡狀態(tài))。兵有48個(gè)位置(包括死亡狀態(tài))。不妨假設(shè)他們出現(xiàn)在各個(gè)位置的頻率都一致,不難構(gòu)造出對(duì)應(yīng)的編碼。(這樣的編碼是比較穩(wěn)定的,無(wú)論棋局變成什么樣子,存儲(chǔ)占用空間都不會(huì)太大)
10個(gè)位置,需要3-4位。6個(gè)位置,需要2-3位。8個(gè)位置,需要3位。48個(gè)位置,需要4-5位。91個(gè)位置,需要6-7位。這樣算下來(lái),保存一個(gè)象棋的棋子位置信息,最少需要:
(3+2*2+3*2+6*6+4*5)*2=138
位,再用1位保存該誰(shuí)下棋了,總共至少需要139位。至多需要(4+3*2+3*2+7*6+5*5)*2=166
位,再用1位保存該誰(shuí)下棋了,總共至多需要167位。
有辦法實(shí)現(xiàn)嗎?
上面說(shuō)的很理想,如何實(shí)現(xiàn)呢?
我們以10個(gè)位置的情況,來(lái)探討通用的編碼生成方法。首先根據(jù)哈夫曼樹,可以構(gòu)造這樣的編碼:
000代表0001代表1010代表2011代表3100代表4101代表51100代表61110代表71101代表81111代表9隨后容易發(fā)現(xiàn)這樣的規(guī)律:
至于0-5,用3位二進(jìn)制編碼即可。至于6-7,我們需要在3位的6(110)
和7(111)
末尾新增0。至于8-9我們需要在3位的6
和7
末尾新增1。可以利用數(shù)學(xué)歸納法,歸納總結(jié)出這樣的算法:
針對(duì)X個(gè)位置的情況,計(jì)算Log2(X),分別向下取整和向上取整,得到A和B。如果A=B,則用A位二進(jìn)制表示這X個(gè)數(shù)即可,直接轉(zhuǎn)換進(jìn)制。如果A0至2^A-1-(X-2^A)
;用B位二進(jìn)制表示其它位置;針對(duì)位置2^A-(X-2^A)
至2^A-1
,編碼為A位的進(jìn)制轉(zhuǎn)換,并在末尾拼接一位0
(共計(jì)B位);針對(duì)其它位置,編碼為位置減去(X-2^A)
再轉(zhuǎn)換二進(jìn)制,并在末尾拼接一位1
(共計(jì)B位)。可以發(fā)現(xiàn),這種算法,位置編號(hào)小的比位置編號(hào)大的少了一位。也就是說(shuō),我們應(yīng)該盡量把出現(xiàn)頻率較高的位置放在前面。
生成各棋子的位置列表
const RedAllCandidates = new Array(90).fill(0).map((a, i) => 89 - i);const BlackAllCandidates = new Array(90).fill(0).map((a, i) => i);const RedSoliderCandidates = new Array(45).fill(0).map((a, i) => 44 - i);const BlackSoliderCandidates = new Array(45).fill(0).map((a, i) => 45 + i);// 分別是將、士、士、……const PieceCandidates = [ [85, 86, 84, 76, 77, 75, 67, 68, 66, 127], [127, 86, 84, 76, 68, 66], [127, 84, 86, 76, 66, 68], [127, 87, 67, 71, 51, 83, 47, 63], [127, 83, 67, 63, 47, 87, 51, 71], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, 62, 53, ...RedSoliderCandidates], [127, 60, 51, ...RedSoliderCandidates], [127, 58, 49, ...RedSoliderCandidates], [127, 56, 47, ...RedSoliderCandidates], [127, 54, 45, ...RedSoliderCandidates], [4, 3, 5, 13, 12, 14, 22, 21, 23, 127], [127, 3, 5, 13, 21, 23], [127, 5, 3, 13, 23, 21], [127, 2, 22, 18, 38, 6, 42, 26], [127, 6, 22, 26, 42, 2, 38, 18], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, 27, 36, ...BlackSoliderCandidates], [127, 29, 38, ...BlackSoliderCandidates], [127, 31, 40, ...BlackSoliderCandidates], [127, 33, 42, ...BlackSoliderCandidates], [127, 35, 44, ...BlackSoliderCandidates],];
解釋:
我可以把將帥的「死亡」(127)調(diào)整到了最后一位,因?yàn)樗麄兯劳鍪欠浅:币姷?,這樣可以節(jié)約2bit空間。我刻意把棋子常見位置放在了數(shù)組前幾位,尤其是將帥、士、兵,這樣可以節(jié)約幾bit空間。兵的位置,紅色和黑色不同,剛過(guò)河的一排放在前面,離河遠(yuǎn)的位置放在后面,可以節(jié)約幾bit空間。提前計(jì)算log
為了提高效率,我應(yīng)該避免在JS中計(jì)算Math.log2,而要提前定義好運(yùn)算結(jié)果。
const ceilLog2Map = new Map([ [1, 0], [2, 1], [3, 2], [4, 2], [6, 3], [8, 3], [10, 4], [17, 5], [48, 6], [91, 7],]);const floorLog2Map = new Map([ [1, 0], [2, 1], [3, 1], [4, 2], [6, 2], [8, 3], [10, 3], [17, 4], [48, 5], [91, 6],]);
按照編碼規(guī)則encode
基于文章《JS 按自定義格式 拼接二進(jìn)制串 解析二進(jìn)制串》提到的concatBits
函數(shù),我寫了concatFlexibleBits
函數(shù):
function concatFlexibleBits(current: number, offset: number, candidateIndex: number, candidateLength: number): [number, number, number[]] { const floorLog = floorLog2Map.get(candidateLength)!; const ceilLog = ceilLog2Map.get(candidateLength)!; const last = 2 ** floorLog; const beyond = candidateLength - last; if (floorLog === ceilLog || candidateIndex < last - beyond) { return concatBits(current, offset, candidateIndex, floorLog); } let newCurrent = current; let newOffset = offset; const array: number[] = []; let newUint8: number[]; if (candidateIndex < last) { [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, candidateIndex, floorLog); array.push(...newUint8); [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, 0, 1); array.push(...newUint8); } else { [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, candidateIndex - beyond, floorLog); array.push(...newUint8); [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, 1, 1); array.push(...newUint8); } return [newCurrent, newOffset, array];}
這里encode規(guī)則,就是按照上面提到的算法實(shí)現(xiàn)的。不過(guò)多解釋了。
按照編碼規(guī)則decode
基于文章《JS 按自定義格式 拼接二進(jìn)制串 解析二進(jìn)制串》的readBits
函數(shù),我寫了readFlexibleBits
函數(shù):
function readFlexibleBits(array: Uint8Array, bitsOffset: number, candidateLength: number) { const floorLog = floorLog2Map.get(candidateLength)!; const ceilLog = ceilLog2Map.get(candidateLength)!; const last = 2 ** floorLog; const beyond = candidateLength - last; const [number, offset] = readBits(array, bitsOffset, floorLog); if (floorLog === ceilLog || number < last - beyond) { return [number, offset]; } const [current, offset2] = readBits(array, offset, 1); if (current) { return [number + beyond, offset2]; } return [number, offset2];}
這里decode規(guī)則,是按照上面算法解析的。先讀取floorLog
位,如果總位置數(shù)就是2的次方,則結(jié)束。如果讀取到的數(shù)比較小,也結(jié)束。如果讀取到的數(shù)超過(guò)某個(gè)臨界值,就需要多讀取一位,決定它代表誰(shuí)。
結(jié)論
方案三可以實(shí)現(xiàn),并且比方案一節(jié)約了35%-45%的空間。
關(guān)于性能:編碼、解碼邏輯全都位于用戶瀏覽器中執(zhí)行,對(duì)服務(wù)器無(wú)影響,瀏覽器也會(huì)在人感知不到的耗時(shí)內(nèi)運(yùn)算完。
有什么用?
我在開發(fā)《象棋》時(shí),期望通過(guò)URL來(lái)分享棋局。我希望分享的URL能永久有效,而且不喜歡給服務(wù)器太多債務(wù)(不采用token+數(shù)據(jù)庫(kù)存儲(chǔ)棋盤信息)。那么URL中必須包含完整的棋盤信息。
如果把棋盤信息存到URL中,那么URL越短,越好。
例如:game.hullqin.cn/xq?p=gSQCL5P5oIDhCFJCIBJ4eQCAkX8&r=86pU6-4nbSh38OCojLarcupWOb1rXw&s=1 ,點(diǎn)開后就能立馬展現(xiàn)某場(chǎng)對(duì)局。
這個(gè)URL里,保存了棋盤所有棋子信息、所有歷史記錄(10個(gè)回合即20步)。方便大家保存、分享。
保存歷史記錄,也是通過(guò)類似的手段實(shí)現(xiàn)的,占用空間非常小(長(zhǎng)度兩百的字符串,足夠存儲(chǔ)大部分常規(guī)對(duì)局的歷史記錄)。
歡迎觀看 【可以「旋轉(zhuǎn)棋盤」的聯(lián)機(jī)象棋】 - 嗶哩嗶哩
寫在最后
我是HullQin,公眾號(hào)線下聚會(huì)游戲的作者(歡迎關(guān)注我,交個(gè)朋友)。轉(zhuǎn)發(fā)本文前需獲得作者HullQin授權(quán)。我獨(dú)立開發(fā)了《聯(lián)機(jī)桌游合集》,是個(gè)網(wǎng)頁(yè),可以很方便的跟朋友聯(lián)機(jī)玩UNO、飛行棋、斗地主、五子棋、一夜狼、狼人殺、象棋、德國(guó)心臟病、達(dá)芬奇密碼等游戲,不收費(fèi)無(wú)廣告。還開發(fā)了《Dice Crush》參加Game Jam 2022。喜歡可以關(guān)注我噢~我有空了會(huì)分享做游戲的相關(guān)技術(shù),會(huì)在這個(gè)專欄里分享:《教你做小游戲》。
關(guān)鍵詞:
凡注有"環(huán)球傳媒網(wǎng) - 環(huán)球資訊網(wǎng) - 環(huán)球生活門戶"或電頭為"環(huán)球傳媒網(wǎng) - 環(huán)球資訊網(wǎng) - 環(huán)球生活門戶"的稿件,均為環(huán)球傳媒網(wǎng) - 環(huán)球資訊網(wǎng) - 環(huán)球生活門戶獨(dú)家版權(quán)所有,未經(jīng)許可不得轉(zhuǎn)載或鏡像;授權(quán)轉(zhuǎn)載必須注明來(lái)源為"環(huán)球傳媒網(wǎng) - 環(huán)球資訊網(wǎng) - 環(huán)球生活門戶",并保留"環(huán)球傳媒網(wǎng) - 環(huán)球資訊網(wǎng) - 環(huán)球生活門戶"的電頭。
- 焦點(diǎn)日?qǐng)?bào):保存象棋棋盤信息,需要多少比特2023-04-23
- ?五月天樂隊(duì)成員介紹 樂隊(duì)成員怎么認(rèn)識(shí)的2023-04-23
- 全球百事通!python-異常處理和錯(cuò)誤調(diào)試-異2023-04-23
- 英雄無(wú)敵4怎么玩?英雄無(wú)敵4攻略?2023-04-23
- 全球快報(bào):造紙印刷是什么?關(guān)于造紙印刷的2023-04-23
- 鄭州買菜網(wǎng)是什么網(wǎng)站?關(guān)于鄭州買菜網(wǎng)的資2023-04-23
- 天天動(dòng)態(tài):安葬證是什么?關(guān)于安葬證的資料2023-04-23
- 硫酸亞汞參比電極是什么?關(guān)于硫酸亞汞參比2023-04-23
- 每日熱文:三星筆記本電腦網(wǎng)卡驅(qū)動(dòng)怎么下載2023-04-23
- hp1600打印機(jī)怎么樣?惠普1600打印機(jī)用的什2023-04-23
- 如何用松下FZ20拍風(fēng)景?松下FZ20的優(yōu)缺點(diǎn)是2023-04-23
- 無(wú)鉛錫是什么?關(guān)于無(wú)鉛錫的資料介紹-觀點(diǎn)2023-04-23
- 環(huán)球觀熱點(diǎn):空盆來(lái)蛇是什么?關(guān)于空盆來(lái)蛇2023-04-23
- 《讓子彈飛》結(jié)局是什么 讓子彈飛師爺結(jié)局2023-04-23
- 防范利差損 人身險(xiǎn)新開發(fā)產(chǎn)品定價(jià)利率或下2023-04-23
- 理想之城結(jié)局 趙顯坤結(jié)局是什么?2023-04-23
- 世界速訊:最新數(shù)據(jù)!一季度20個(gè)行業(yè)平均月2023-04-23
- 韶關(guān)市農(nóng)業(yè)農(nóng)村局“牽手”廣東省農(nóng)科院加工2023-04-23
- 集邦咨詢:N型電池片技術(shù)百花齊放 TOPCon2023-04-23
- 環(huán)球微動(dòng)態(tài)丨醫(yī)療周動(dòng)態(tài)2023-04-23
- 雪中悍刀行中的老黃 雪中悍刀行老黃多少2023-04-23
- 《讓我們蕩起雙槳》原唱是誰(shuí) 女歌手?劉慧2023-04-23
- 陸寒為什么失蹤 狂飆陸寒死了沒?2023-04-23
- 披荊斬棘的哥哥誰(shuí)被馬賽克 四公淘汰名單是2023-04-23
- 李?yuàn)W林小音是誰(shuí) 獨(dú)生子講了什么?2023-04-23
- 人民的名義結(jié)局是什么 人民的名義祁同偉被2023-04-23
- 口袋妖怪火紅攻略_妖怪攻略大盤點(diǎn) 焦點(diǎn)速訊2023-04-23
- 飛升后徐脂虎去哪了 雪中悍刀行徐脂虎為什2023-04-23
- 熱頭條丨東方演藝集團(tuán)將合作打造蘇東坡音樂2023-04-23
- 中國(guó)農(nóng)科院?jiǎn)?dòng)第五屆農(nóng)科開放日2023-04-23
- 約翰·洛克菲勒介紹 洛克菲勒的勵(lì)志故事是什么?
- 學(xué)生營(yíng)養(yǎng)改善計(jì)劃 學(xué)生營(yíng)養(yǎng)改善計(jì)劃主要內(nèi)容是什么?
- 征信黑戶是什么 征信黑戶有什么影響?
- 深圳汽車限購(gòu)簡(jiǎn)介 深圳汽車限購(gòu)弊端又在哪里呢?
- 玫瑰價(jià)格暴漲咋回事 花卉市場(chǎng)的發(fā)展前景如何?
- 西昌市是什么 ?西昌旅游風(fēng)景有哪些?
- 電話車險(xiǎn)好嗎 電話車險(xiǎn)的利與弊?
- 布依族簡(jiǎn)介 ?布依族風(fēng)俗是什么?
- 個(gè)稅改革方案內(nèi)容 個(gè)稅改革方案內(nèi)容是什么?
- 世界八大建筑奇跡介紹 八大建筑奇跡還爭(zhēng)議有哪些?
資訊
- 天天日?qǐng)?bào)丨古風(fēng)游園會(huì)與現(xiàn)代智慧閱讀的交融 中南大學(xué)讀書文化節(jié)掀起全民閱讀高潮
- 4月21日國(guó)際原油期貨收漲-全球即時(shí)
- 每日熱聞!八部門:到2025年底 IPv6技術(shù)演進(jìn)和應(yīng)用創(chuàng)新取得顯著成效
- 阿里P9下崗再就業(yè)|熱議
- 《勇氣默示錄2終結(jié)次元》發(fā)售8周年?官方發(fā)布賀圖 熱點(diǎn)聚焦
- 散粉的保質(zhì)期一般是多久(散粉的保質(zhì)期是多久) 天天熱推薦
- 新城控股集團(tuán):2023新城商業(yè)正式啟動(dòng)深度運(yùn)營(yíng)戰(zhàn)略模_視焦點(diǎn)訊
- 多名內(nèi)地游客入境香港被帶進(jìn)小黑屋,甚至被拒入境! 全球新資訊
- 九江將新建一城市森林花園|全球看點(diǎn)
- 全球短訊!環(huán)紋堅(jiān)石蛤_關(guān)于環(huán)紋堅(jiān)石蛤介紹
焦點(diǎn)
- 亞洲第一高樓是什么 亞洲第一高樓多高?
- ?貓島是什么意思 日本貓島名稱及位置是什么?
- 吉爾吉斯斯坦首都 吉爾吉斯首都比什凱克的經(jīng)濟(jì)發(fā)展如何?
- 全球金融危機(jī)原因 全球金融危機(jī)的原因介紹
- 土耳其屬于哪個(gè)洲 土耳其的首都是什么?
- 近地小行星是否會(huì)對(duì)地球造成威脅?近地小行星的蹤影匯總
- 油價(jià)或迎第八漲 油價(jià)的后市會(huì)是怎樣的發(fā)展?
- 最丑十大建筑 世界最丑十大建筑有哪些?
- 英國(guó)脫歐是什么 英國(guó)脫歐的原因介紹
- 油價(jià)一跌再跌 海外爆發(fā)對(duì)各國(guó)油氣行業(yè)有什么影響?