摘要:本文同步自我的過去,在文曲星等各種電子詞典中,經常會有一個叫做猜單詞的游戲。算法與自動化流程相關的函數都已經準備好了,接下來需要實現的就是算法了。最后附上完整的源碼實現源碼
本文同步自我的 GitHub
過去,在文曲星等各種電子詞典中,經常會有一個叫做猜單詞的游戲。給定一個單詞,告訴你這個單詞有幾個字母,然后你去猜。輸入一個字母,如果單詞中包含這個字母,則將單詞中所有的這個字母都顯示出來,如果猜錯,則扣生命值,在生命值扣光之前全部猜對則為勝利。
過去我很喜歡玩這個游戲,因為它能讓背單詞顯得不那么枯燥乏味,也能提高自己對單詞構詞規律的認識。但是這篇文章要說的,不是怎么去玩好這個游戲,而是怎么借助程序的力量去自動破解猜單詞的難題。
假設現在存在這樣的一個接口http://hangman.com/game/on,它可以接受 post 請求,合法的請求共有四種。第一種是開始游戲,發送這樣的數據可以重新開始一次新的游戲:
javascript{ "playerId": "classicemi", "action": "startGame" }
服務器會返回如下信息:
javascript{ "message": "THE GAME IS ON", "sessionId": "xxxx", "data": { "numberOfWordsToGuess": 80, "numberOfGuessAllowedForEachWord": 10 } }
它告訴用戶游戲已經開始,共有 80 個單詞要猜,每個單詞有十次猜錯的機會。
用戶還可以發送下一個單詞的請求:
javascript{ "sessionId": "xxxx", //這是開始游戲時服務器返回的sessionId,用于識別用戶 "action": "nextWord" }
服務器的返回信息如下:
javascript{ "sessionId": "xxxx", "data": { "word": "*****", "totalWordCount": 1, "wrongGuessCountOfCurrentWord": 0 } }
從這樣的信息中可以知道,要猜的單詞由 5 個字母組成,以及現在猜錯了幾次(當然現在是 0 次)。
要進行猜測的話,則發送如下請求:
javascript{ "sessionId": "xxxx", "action": "guessWord", "guess": "t" //舉個栗子 }
如果猜測正確,服務器會返回如下數據:
javascript{ "sessionId": "xxxx", "data": { "word": "***S*", "totalWordCount": 1, "wrongGuessCountOfCurrentWord": 0 } }
如果猜錯了,則返回如下數據:
javascript{ "sessionId": "xxxx", "data": { "word": "*****", "totalWordCount": 1, "wrongGuessCountOfCurrentWord": 1 } }
如果猜錯超過十次還繼續猜,則會返回如下信息:
javascript{ "message": "No more guess left." }
這時,只能選擇跳轉至下一個單詞了,即再次發送nextWord請求。當用戶猜完了 80 個詞(當然也可以是任何時候),用戶可以選擇提交成績結束游戲,只要發送如下請求:
javascript{ "sessionId": "xxxx", "action" : "submitResult" }
服務器返回最終完成的信息:
javascript{ "message": "GAME OVER", "sessionId": "xxxx", "data": { "playerId": "classicemi", "sessionId": "xxxx", "totalWordCount": 80, "correctWordCount": 77, "totalWrongGuessCount": 233, "score": 1307, "datetime": "2014-10-28 11:45:58" } }
同時,在游戲過程中,用戶可以隨時查看當前已有的成績,發送請求如下:
javascript{ "sessionId": "xxxx", "action" : "getResult" }
返回信息如下:
javascript{ "sessionId": "xxxx", "data": { "totalWordCount": 40, "correctWordCount": 20, "totalWrongGuessCount": 100, "score": 300 } }
OK,關于接口已經介紹完了,下面就來玩這個游戲吧。
思考首先,由于我們要實現一個全自動的程序,不能借助人的力量,也就是說,用戶的單詞量的多少根本派不上用場。如果這個單詞只是一個隨機字符串的話,問題倒也簡單了,隨機猜字母即可。但是現在已經明確是英語單詞,雖然比起隨機字符串,范圍大大縮小,但是要準確去猜英語單詞,隨機猜字母肯定是行不通了。
既不能借助用戶的單詞量,又不能使用隨機字母,那么我們就需要一個樣本總量足夠大的單詞表作為我們的數據庫。在 UNIX 系統中,/usr/share/dict目錄中,有一個words文件,用 vim 打開看一下,發現里面有 20 多萬個單詞,這就是一個現成的單詞數據庫。不過根據后來的測試結果來看,20多萬的單詞量玩這個游戲還是有點不夠,所以,還是去找開源的單詞列表數據吧,最后我找到一個 65w 單詞量的文件,正確率就比較高了。
流程有了大量的單詞數據,只是打好了基礎,就像張無忌練了九陽神功,內力充沛,但是沒有招式還是不行,充其量只是打不死,在這里我們需要的招式則是一個科學的算法。
不過在實現算法之前,先來把自動化程序的骨架搭起來,使流程控制能夠跑通。我使用的是 Node.js 來執行程序,依賴的模塊有兩個,分別是inquirer和request。前者用來構建交互式的命令行程序,便于必要時接受用戶的指令;后者用來方便地發送 post 請求。
程序的流程圖如下:
+-------+ | start | +---+---+ | v +---------+-----------+ +-----------+ +--->+ flow control center | <-------------+ next word | | +---------+-----------+ +-------+---+ | | ^ | is the|guess finished? |no | | is the game finished?| get the|result +--------+yes+----------------------+ | |no yes| | v v | +------+-------+ +----+---+ +-------+ make a guess | | submit | +--------------+ +--------+
根據流程圖可以知道,我們需要幾個函數來實現這個流程,圖中的一個方塊就對應一個函數,首先是流程的入口,程序最開始也是調用這個方法:
javascriptfunction startGame() { inquirer.prompt( [{ type: "input", name: "startGame", message: "please enter "y" to automatically play the game, or enter session id to continue: " }], function(answers) { if (answers.startGame.toLowerCase() != "y") { sessionId = answers.startGame; nextWord(); return; } setTimeout(function() { auto("start"); }, 0); } ); }
這里面有一個 if 語句用來接受用戶直接輸入sessionId的情況,這是為了處理一旦網絡中斷或是程序異常的情況,便于用戶直接輸入sessionId來接著上次的進度繼續執行??梢钥吹狡渲姓{用了auto方法,這個auto方法則是流程圖中的 flow control center,它會根據傳入的參數來決定下一步去調用哪個方法(函數中的一些變量的作用后面會作解釋):
javascriptfunction auto(data, letterToGuess) { if (data == "start") { options.body = { "playerId": playerId, "action": "startGame" }; request(options, function(err, res, data) { if (!err && res.statusCode == 200) { console.log(data) console.log("game restarted,your sessionId is: ", data.sessionId); sessionId = data.sessionId; setTimeout(function() { auto(data); }, 0); } else { console.log(err); } }); return; } // game start if (data.message && data.message == "THE GAME IS ON") { sessionId = data.sessionId; setTimeout(nextWord, 0); return; } if (data.message && data.message == "No more word to guess.") { setTimeout(getResult, 0); return; } // unfinished situation if (data.data.word.indexOf("*") > -1 && data.data.wrongGuessCountOfCurrentWord < 10 && data.data.totalWordCount <= 80) { setTimeout(function() { guess(data.data.word, data.data.wrongGuessCountOfCurrentWord, letterToGuess); }, 0); } else if (data.data.word.indexOf("*") == -1 || data.data.wrongGuessCountOfCurrentWord >= 10) { // guess finished // 猜詞完畢后,復原輔助變量 wordsMatchLength = []; letterFrequency = {}; wrongNum = 0; lettersGuessed = ""; setTimeout(nextWord, 0); } else if (data.data.totalWordCount >= 80 && data.data.wrongGuessCountOfCurrentWord >= 10) { setTimeout(getResult, 0); } }
接下來是實現nextWord功能和guessWord功能的函數:
javascriptfunction nextWord() { options.body = { "sessionId": sessionId, "action": "nextWord" }; request(options, function(err, res, data) { if (err) { console.log(err); } else if(data.message) { console.log(data.message); } else { console.log("current word: ", data.data.word); console.log("current word count: ", data.data.totalWordCount); console.log("wrong guess: ", data.data.wrongGuessCountOfCurrentWord + " times"); index = 0; } auto(data); }); } function guess(word, wrongNum, letter) { var letterToGuess = filter(word, wrongNum, letter); options.body = { "sessionId": sessionId, "action": "guessWord", "guess": letterToGuess.toUpperCase() }; request(options, function(err, res, data) { if (err) { console.log(err); } else if(data.message) { console.log(message); } else { console.log("your guess: ", letterToGuess.toUpperCase()); console.log("current word: ", data.data.word); console.log("current word count: ", data.data.totalWordCount); console.log("wrong guess: ", data.data.wrongGuessCountOfCurrentWord + " times"); } setTimeout(function() { auto(data, letterToGuess); }, 0); }); }
最后是獲取成績和提交成績的方法:
javascriptfunction getResult() { options.body = { "sessionId": sessionId, "action": "getResult" }; function submitDicide() { inquirer.prompt( [{ type: "input", name: "submitDicision", message: "enter "y" to submit your score or enter "n" to restart: " }], function(answers) { if (answers.submitDicision.toLowerCase() != "y" && answers.submitDicision.toLowerCase() != "n") { console.log("illegal command, please reenter: "); submitDicide(); return; } switch (answers.submitDicision.toLowerCase()) { case "y": submit(); break; case "n": startGame(); break; default: break; } } ); } request(options, function(err, res, data) { if (err) { console.log(err); } else if(data.message) { console.log(message); } else { console.log(data); console.log("current word: ", data.data.word); console.log("current word count: ", data.data.totalWordCount); console.log("wrong guess: ", data.data.wrongGuessCountOfCurrentWord + " times"); console.log("current score: ", data.data.score); submitDicide(); } }); } function submit() { options.body = { "sessionId": sessionId, "action": "submitResult" }; request(options, function(err, res, data) { if (err) { console.log(err); } else { console.log("player: ", data.data.playerId); console.log("session id: ", data.data.sessionId); console.log("total word count: ", data.data.totalWordCount); console.log("correct word count: ", data.data.correctWordCount); console.log("total wrong guess count: ", data.data.totalWrongGuessCount); console.log("total score: ", data.data.score); console.log("submit time: ", data.data.datetime); } }); }
由于整個程序的方法之間會一直相互調用,為了防止調用棧過深,所有的調用都用setTimeout改成了異步的方式。
算法與自動化流程相關的函數都已經準備好了,接下來需要實現的就是算法了。說是算法,其實就是充分利用已有的信息對詞典進行篩選的過程,首先要對現有的詞典文件進行一些預處理的工作,這些工作在執行程序的一開始就會完成:
javascript// 同步方式讀取字典文件 var dict = fs.readFileSync("words.txt", "utf-8"); // 獲得保存所有單詞的數組 var wordArr = dict.split(" ");
接下來就是核心函數filter,它位于guess方法中,用來分析數據,返回接下來應該猜哪個字母,它的工作流程如下:
第一次調用時,根據要猜單詞的長度遍歷數組wordArr,篩選出長度符合條件的單詞并push到wordsMatchLength數組中:
javascriptif (!wordsMatchLength.length) { for (var i = 0, len = wordArr.length; i < len; i++) { if (wordArr[i].length === word.length) { wordsMatchLength.push(wordArr[i]); } } }
對wordsMatchLength數組進行雙循環遍歷,借助一個空對象letterFrequency,選出這些單詞中出現頻率最高的字母,并返回。
javascriptfor (var i = 0, len = wordsMatchLength.length; i < len; i++) { for (var j = 0, innerLen = wordsMatchLength[i].length; j < innerLen; j++) { letterFrequency[wordsMatchLength[i][j].toLowerCase()] == undefined ? letterFrequency[wordsMatchLength[i][j].toLowerCase()] = 1 : letterFrequency[wordsMatchLength[i][j].toLowerCase()]++; } } for (var key in letterFrequency) { if (letterFrequency[key] > frequency && lettersGuessed.indexOf(key) < 0) { frequency = letterFrequency[key]; l = key; } }
這是猜第一個字母的方法,后續的篩選將要依賴之前猜詞的結果來進行,filter方法在遞歸中會被重復調用,之前猜詞的結果會作為參數傳入。
如果上一次猜對,那么返回的信息大概會長這樣:
javascriptword: **t**u*
這顯然是一種模式,可以將它轉化為正則去篩選候選數組,我又實現了一個將此類字符串轉化為正則的方法:
javascriptfunction generatePattern(word) { var patternStr = ""; var starNum = 0; for (var i = 0, len = word.length; i < len; i++) { if (word[i] == "*") { starNum = starNum + 1; } else { patternStr = patternStr + (starNum ? "w{" + starNum + "}" : "") + word[i]; starNum = 0; } } // 修正結尾的星號 patternStr = patternStr + (starNum ? "w{" + starNum + "}" : ""); return new RegExp(patternStr, "i"); }
得到正則后,用這個正則去過濾一下wordsMatchLength數組,刪掉不匹配的單詞:
javascriptfor (var i = 0, len = wordsMatchLength.length; i < len; i++) { if (wordsMatchLength[i] && !generatePattern(word).test(wordsMatchLength[i])) { wordsMatchLength.splice(i, 1); i--; len--; } }
如果上一次猜錯了呢,那么上一次猜了哪個字母,就說明正確的單詞中不應該包含它,那么遍歷一下wordsMatchLength數組,凡是包含這個字母的單詞通通干掉:
javascriptfor (var i = 0, len = wordsMatchLength.length; i < len; i++) { if (wordsMatchLength[i] && (wordsMatchLength[i].indexOf(letter.toLowerCase()) > -1 || wordsMatchLength[i].indexOf(letter.toUpperCase()) > -1)) { wordsMatchLength.splice(i, 1); i--; len--; } }
過濾工作完成后,要做的就是再統計一次字母頻率,選擇最常出現的那個即可。
另外,還需要做一些修正工作,來應對所猜單詞過于偏門,沒有出現在單詞庫中的情況,準備一個備用數組,里面的單詞順序按照一般情況下字母的出現頻率排列,一旦單詞庫被過濾完,就去遍歷這個數組,選出頻率最高,而之前還沒有猜過的字母并返回。這時候就看運氣了。
同時也要記住在沒猜完一個單詞后要把候選數組清空,紀錄猜錯次數和已猜過字母的變量也要復原,不要影響后面的計算。
優化以上方法還有一些優化的空間:
統計字母出現頻率的時候,同一個單詞中的同一個字母,不論出現幾次都只算一次,比如 e 或 s 這樣的字母,在一個單詞中可能出現很多次,但是沒有必要重復計數。
當候選數組被過濾完時,可以不用備用數組,切換為用戶手動輸入,這樣可以利用用戶英語構詞法的知識進行有目的的猜測,但這種方法偏離了全自動程序的初衷。
最后就要借助對構詞法的科學計算進行優化了,這種計算需要專業知識的支撐,普通開發者無法勝任。
最后附上完整的源碼實現:
源碼
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85650.html
Python作為一門常見的編程語言,可以用到的地方是比較的多的,而且他還能夠去編程相關的游戲,那么,下文就會給大家教一個比較簡單的小游戲,就是寫猜數字和字母的游戲,詳細的內容可以看下文,看完之后,可以自己去手動敲下代碼哦?! ∏把浴 W完語法和正在學習語法的時候,我們可以在空閑的時候,寫幾個簡單的小項目,今天我們就用最基礎的語法看兩個實戰語法練習 猜數字游戲 項目游戲說明:讓用戶輸入一個數...
摘要:我在這里將他寫的程序恭錄于此,單元李航同學不要見怪,如果李航同學認為此舉侵犯了自己的知識產權,可以告知我,我馬上撤下此代碼。我用的是,在輸入指令上區別于李同學程序用變量接收了輸入的內容。 while,翻譯成中文是當...的時候,這個單詞在英語中,常常用來做為時間狀語,while ... someone do somthing,這種類型的說法是有的。在python中,它也有這個含義,不過...
摘要:就是一個用于搭建類似于網頁版知乎這種表單項繁多,且內容需要根據用戶的操作進行修改的網頁版應用。單頁應用程序顧名思義,單頁應用一般指的就是一個頁面就是應用,當然也可以是一個子應用,比如說知乎的一個頁面就可以視為一個子應用。 最近在逛各大網站,論壇,以及像SegmentFault等編程問答社區,發現Vue.js異?;鸨?,重復性的提問和內容也很多,樓主自己也趁著這個大前端的熱潮,著手學習了一...
導讀:興許所有程序員都有命名困難癥,在考慮變量、常量、方法、類、文件等命名時,總會千方百計嘗試一些語義化的方式去實現。 曾經有那么一段時間,一些node初學的同學遇到了同樣的問題:Hello World 跑不動! 原文首發于個人博客:這事要從node node.js說起 0. 謎之 Hello World 問題的起源非常簡單,當我們在編寫一個入門程序時,就會迅速想起那句膾炙人口的語句: conso...
閱讀 3877·2021-09-10 11:22
閱讀 2339·2021-09-03 10:30
閱讀 3666·2019-08-30 15:55
閱讀 1891·2019-08-30 15:44
閱讀 844·2019-08-30 15:44
閱讀 592·2019-08-30 14:04
閱讀 3047·2019-08-29 17:18
閱讀 1269·2019-08-29 15:04