node.js——麻將演算法(六)簡易版麻將出牌AI1.0
阿新 • • 發佈:2019-02-08
普通麻將的出牌AI如果不是要求特別高的話,其實蠻容易實現的,畢竟大多數人打牌都只是看自己的手牌。
所以作為簡易版的AI,出牌的策略只要奔著胡牌去就可以了。我們能想到的就是把相鄰或相同的牌湊到一起,把單獨的牌打出去。以這個思路打牌,就會慢慢接近聽牌至最終的胡牌。
我們簡單舉個例子,我們有1萬2萬,那麼我們認為其打出去的優先順序要高於單獨的牌,因為其只需要1個三萬就可以湊成一組了。
我們看這裡
for (var i = 0; i < 4; i++) { var needhun = 0; for (var j = 0; j < 4; j++) { needhun += j == i ? getneedhun(arr, j,false) : getneedhun(arr, j,true); } if (needhun <= Huncount) { return true; } }
我們通過needhun是否大於本身擁有的混牌(賴子)來判斷是否可以胡,那麼其實這個needhun就可以代表不同牌型胡牌的難度,即needhun值越小其越容易胡,當needhun值為1時(手牌數為3n+1),其處於聽牌階段。
那麼這就很簡單了,我們只需要在此基礎上返回這個needhun,然後在出牌時看出那張牌返回的needhun最小,其就是我們的最佳策略選擇。
function get_needhun_for_hu(old_arr, Hun, special) { var fmin_data = function (data1, data2) { return data1.needhun > data2.needhun ? data2 : data1; }; var del_list = function (old_arr, i, j, data) { var arr = old_arr.concat(); for (var k = 0; k < 3; k++) { if (arr[i + k] > 0) { arr[i + k]--; } else { data.needhun++; } } return dfs(arr, i, j, data); }; var del_same = function (old_arr, i, j, data) { var arr = old_arr.concat(); arr[i] %= 3; switch (arr[i]) { case 0: { break; } case 1: { if (data.hasjiang) { data.needhun += 2; } else { data.needhun++; data.hasjiang = true; } break; } case 2: { if (data.hasjiang) { data.needhun += 1; } else { data.hasjiang = true; } break; } } arr[i] = 0; return dfs(arr, i + 1, j, data); }; var dfs = function (arr, i, j, data) { if (i > j) { if (!data.hasjiang) { data.needhun += 2; } return data; } if (i % 9 == 6 && i < 27 && arr[i + 1]%3 == 1 && arr[i + 2]%3 == 1)//8 9特殊情況,此時應該補個7 { return del_list(arr, i, j, data); } else if (arr[i] == 0) { return dfs(arr, i + 1, j, data); } else if (i % 9 < 7 && i < 27 && (arr[i + 1] > 0 || arr[i + 2] > 0)) { var tmp1 = del_list(arr, i, j, { needhun: data.needhun, hasjiang: data.hasjiang }); var tmp2 = del_same(arr, i, j, { needhun: data.needhun, hasjiang: data.hasjiang }); return fmin_data(tmp1, tmp2); } else { return del_same(arr, i, j, data); } }; var getneedhun = function (old_arr, type, hasjiang) { var data = { needhun: 0, hasjiang: hasjiang }; var arr = old_arr.concat(); var i, j; switch (type) { case 0: { i = 0; j = 8; break; } case 1: { i = 9; j = 17; break; } case 2: { i = 18; j = 26; break; } case 3: { i = 27; j = 33; break; } } data = dfs(arr, i, j, data); return data.needhun; };
var arr = old_arr.concat(); var HunCount = 0; if (Hun >= 0) { HunCount = arr[Hun]; arr[Hun] = 0; } var count = 0; for (var i = 0; i < arr.length; i++) { count += arr[i]; } var min_needhun = 0x1f; if (special.H_7pair&&count + HunCount == 14) { var needhun = 0; for (var i = 0; i < arr.length; i++) { var c = arr[i]; if (c % 2==1) { needhun+=2; } } if (needhun<min_needhun) { min_needhun = needhun; } } if (special.H_13one&&count + HunCount == 14) { var ones = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33]; for (var i = 0; i < ones.length; ++i) { if (arr[ones[i]] == 0) { needhun+=2; } } if (needhun < min_needhun) { min_needhun = needhun; } } for (var i = 0; i < 4; i++) { var needhun = 0; for (var j = 0; j < 4; j++) { needhun += getneedhun(arr, j, j != i); } if (needhun < min_needhun) { min_needhun = needhun; } } return min_needhun - HunCount; }
注:七小對&十三么相對來說需要的混子數要少,原因在於當手牌過散時每張廢牌都意味著需要2張混牌,而七小對&十三么則只把廢牌當作所需要替換的1張牌處理,這使得大部分散牌更適用於七小對&十三么。故needhun需要+2以作平衡
var ret_needhun = 0x1a;
var ret_pai = list[0];
var ret_tinglist = [];
for (var k = 0; k < list.length; k++) {
arr[list[k]]--;
var Tinglist = new Array();
var canting = CanTingPai(arr, hun, Tinglist, special);
if (canting)
{
//聽牌數比對,也可以按其他方式比對,比如所聽的牌接下來的剩餘牌
if (ret_tinglist.length < Tinglist.length)
{
ret_tinglist = Tinglist;
ret_needhun = 1;
ret_pai = list[k];
}
}
else if (ret_tinglist.length==0)
{
var needhun = get_needhun_for_hu(arr, hun, special);
if (needhun < ret_needhun) {
ret_needhun = needhun;
ret_pai = list[k];
}
}
arr[list[k]]++;
}
return ret_pai;
}
關於判聽的演算法有很多,大家可以參考我的前幾篇麻將演算法部落格。
如果不是聽牌的狀態且needhun和ret_needhun相同時可以根據不同的麻將玩法再融入自己的想法策略DIY
比如:優先打風牌、優先打19等。
else if (needhun == ret_needhun)
{
if (list[k] > 26)//風牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
if ((list[k] % 9 < 1 || list[k] % 9 > 7 )&& ret_pai<=26)//邊牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
}
再比如:優先打無關聯單一的牌
exports.is_nexus = function(i, arr) {
if (i > 26) {
return arr[i] > 0;
}
else if (i % 9 == 8) {
return arr[i] > 0 || arr[i - 1] > 0 || arr[i - 2] > 0;
}
else if (i % 9 == 7) {
return arr[i] > 0 || arr[i - 1] > 0 || arr[i - 2] > 0 || arr[i + 1] > 0;
}
else if (i % 9 == 0) {
return arr[i] > 0 || arr[i + 1] > 0 || arr[i + 2] > 0;
}
else if (i % 9 == 1) {
return arr[i] > 0 || arr[i + 1] > 0 || arr[i + 2] > 0 || arr[i - 1] > 0;
}
else {
return arr[i] > 0 || arr[i + 1] > 0 || arr[i + 2] > 0 || arr[i - 1] > 0 || arr[i - 2] > 0;
}
}
完整方法程式碼:exports.GetRobotChupai = function (list, special, hun) {
if (hun == null) {
hun = -1;
}
var arr = [];
var Tingobj = [];
for (var i = 0; i < special.mj_count; i++) {
arr[i] = 0;
}
for (var j = 0; j < list.length; j++) {
Tingobj[j] = {};
if (arr[list[j]] == null) {
arr[list[j]] = 1;
}
else {
arr[list[j]]++;
}
}
var ret_needhun = 0x1a;//假設所有牌都需要2個混子補缺,即:13*2=26
var ret_pai = list[0];
var ret_tinglist = [];
var has_single = false;
for (var k = 0; k < list.length; k++) {
if (list[k] == hun) {
continue;
}
arr[list[k]]--;
var Tinglist = new Array();
var canting = exports.CanTingPai(arr, hun, Tinglist, special);
if (canting) {
//聽牌數比對,也可以按其他方式比對,比如所聽的牌接下來的剩餘牌
if (ret_tinglist.length < Tinglist.length) {
ret_tinglist = Tinglist;
ret_needhun = 1;
ret_pai = list[k];
}
}
else if (ret_tinglist.length == 0) {
var needhun = get_needhun_for_hu(arr, hun, special);
if (!exports.is_nexus(list[k], arr))//如果是單一的手牌優先考慮
{
if (!has_single)//如果之前沒有過單一的牌
{
ret_needhun = needhun;
ret_pai = list[k];
}
has_single = true;
if (list[k] > 26)//風牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
if ((list[k] % 9 < 1 || list[k] % 9 > 7) && ret_pai <= 26)//邊牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
}
else if (!has_single)//如果不是單一的手牌,且之前也沒有過單一的牌
{
if (needhun < ret_needhun)//判斷此張牌需要的混牌數
{
ret_needhun = needhun;
ret_pai = list[k];
}
else if (needhun == ret_needhun) {
if (list[k] > 26)//風牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
if ((list[k] % 9 < 1 || list[k] % 9 > 7) && ret_pai <= 26)//邊牌優先打
{
ret_needhun = needhun;
ret_pai = list[k];
}
}
}
}
arr[list[k]]++;
}
return ret_pai;
}
如果你的手牌list是根據抓牌的順序而沒有進行過排序的話,也可以根據抓牌的順序作為優先順序參考等。
測試截圖:
真實人機對戰測試:(防作弊自摸胡、帶賴子)