AlphaJong

A Mahjong Soul Bot.

  1. // ==UserScript==
  2. // @name AlphaJong
  3. // @namespace alphajong
  4. // @version 1.3.1
  5. // @description A Mahjong Soul Bot.
  6. // @author Jimboom7
  7. // @match https://mahjongsoul.game.yo-star.com/*
  8. // @match https://majsoul.com/*
  9. // @match https://game.maj-soul.com/*
  10. // @match https://majsoul.union-game.com/*
  11. // @match https://game.mahjongsoul.com/*
  12. // ==/UserScript==
  13.  
  14.  
  15. //################################
  16. // PARAMETERS
  17. // Contains Parameters to change the playstile of the bot. Usually no need to change anything.
  18. //################################
  19.  
  20. /* PERFORMANCE MODE
  21. * Range 0 to 4. Decrease calculation time at the cost of efficiency (2 equals the time of ai version 1.2.1 and before).
  22. * 4 = Highest Precision and Calculation Time. 0 = Lowest Precision and Calculation Time.
  23. * Note: The bot will automatically decrease the performance mode when it approaches the time limit.
  24. * Note 2: Firefox is usually able to run the script faster than Chrome.
  25. */
  26. var PERFORMANCE_MODE = 3;
  27.  
  28. //HAND EVALUATION CONSTANTS
  29. var EFFICIENCY = 1.0; // Lower: Slower and more expensive hands. Higher: Faster and cheaper hands. Default: 1.0, Minimum: 0
  30. var SAFETY = 1.0; // Lower: The bot will not pay much attention to safety. Higher: The bot will try to play safer. Default: 1.0, Minimum: 0
  31. var SAKIGIRI = 1.0; //Lower: Don't place much importance on Sakigiri. Higher: Try to Sakigiri more often. Default: 1.0, Minimum: 0
  32.  
  33. //CALL CONSTANTS
  34. var CALL_PON_CHI = 1.0; //Lower: Call Pon/Chi less often. Higher: Call Pon/Chi more often. Default: 1.0, Minimum: 0
  35. var CALL_KAN = 1.0; //Lower: Call Kan less often. Higher: Call Kan more often. Default: 1.0, Minimum: 0
  36.  
  37. //STRATEGY CONSTANTS
  38. var RIICHI = 1.0; //Lower: Call Riichi less often. Higher: Call Riichi more often. Default: 1.0, Minimum: 0
  39. var CHIITOITSU = 5; //Number of Pairs in Hand to go for chiitoitsu. Default: 5
  40. var THIRTEEN_ORPHANS = 10; //Number of Honor/Terminals in hand to go for 13 orphans. Default: 10
  41. var KEEP_SAFETILE = false; //If set to true the bot will keep 1 safetile
  42.  
  43. //MISC
  44. var LOG_AMOUNT = 3; //Amount of Messages to log for Tile Priorities
  45. var DEBUG_BUTTON = false; //Display a Debug Button in the GUI
  46. var USE_EMOJI = true; //use EMOJI to show tile
  47. var CHANGE_RECOMMEND_TILE_COLOR = true; // change current recommend tile color
  48.  
  49.  
  50.  
  51. //### GLOBAL VARIABLES DO NOT CHANGE ###
  52. var run = false; //Is the bot running
  53. var threadIsRunning = false;
  54. const AIMODE = { //ENUM of AI mode
  55. AUTO: 0,
  56. HELP: 1,
  57. }
  58. const AIMODE_NAME = [ //Name of AI mode
  59. "Auto",
  60. "Help",
  61. ]
  62. const STRATEGIES = { //ENUM of strategies
  63. GENERAL: 'General',
  64. CHIITOITSU: 'Chiitoitsu',
  65. FOLD: 'Fold',
  66. THIRTEEN_ORPHANS: 'Thirteen_Orphans'
  67. }
  68. var strategy = STRATEGIES.GENERAL; //Current strategy
  69. var strategyAllowsCalls = true; //Does the current strategy allow calls?
  70. var isClosed = true; //Is own hand closed?
  71. var dora = []; //Array of Tiles (index, type, dora)
  72. var ownHand = []; //index, type, dora
  73. var discards = []; //Later: Change to array for each player
  74. var calls = []; //Calls/Melds of each player
  75. var availableTiles = []; //Tiles that are available
  76. var seatWind = 1; //1: East,... 4: North
  77. var roundWind = 1; //1: East,... 4: North
  78. var tilesLeft = 0; //tileCounter
  79. var visibleTiles = []; //Tiles that are visible
  80. var errorCounter = 0; //Counter to check if bot is working
  81. var lastTilesLeft = 0; //Counter to check if bot is working
  82. var isConsideringCall = false;
  83. var riichiTiles = [null, null, null, null]; // Track players discarded tiles on riichi
  84. var functionsExtended = false;
  85. var playerDiscardSafetyList = [[], [], [], []];
  86. var totalPossibleWaits = {};
  87. var timeSave = 0;
  88. var showingStrategy = false; //Current in own turn?
  89.  
  90. // Display
  91. var tileEmojiList = [
  92. ["red🀝" ,"🀙" ,"🀚" ,"🀛" ,"🀜" ,"🀝" ,"🀞" ,"🀟" ,"🀠" ,"🀡"],
  93. ["red🀋" ,"🀇" ,"🀈" ,"🀉" ,"🀊" ,"🀋" ,"🀌" ,"🀍" ,"🀎" ,"🀏"],
  94. ["red🀔" ,"🀐" ,"🀑" ,"🀒" ,"🀓" ,"🀔" ,"🀕" ,"🀖" ,"🀗" ,"🀘"],
  95. ["", "🀀" ,"🀁" ,"🀂" ,"🀃" ,"🀆" ,"🀅" ,"🀄"]];
  96.  
  97.  
  98. //LOCAL STORAGE
  99. var AUTORUN = window.localStorage.getItem("alphajongAutorun") == "true";
  100. var ROOM = window.localStorage.getItem("alphajongRoom");
  101.  
  102. ROOM = ROOM == null ? 2 : ROOM
  103.  
  104. var MODE = window.localStorage.getItem("alphajongAIMode")
  105. MODE = MODE == null ? AIMODE.AUTO : parseInt(MODE);
  106.  
  107.  
  108. //################################
  109. // GUI
  110. // Adds elements like buttons to control the bot
  111. //################################
  112.  
  113. var guiDiv = document.createElement("div");
  114. var guiSpan = document.createElement("span");
  115. var startButton = document.createElement("button");
  116. var aimodeCombobox = document.createElement("select");
  117. var autorunCheckbox = document.createElement("input");
  118. var roomCombobox = document.createElement("select");
  119. var currentActionOutput = document.createElement("input");
  120. var debugButton = document.createElement("button");
  121. var hideButton = document.createElement("button");
  122.  
  123. function initGui() {
  124. if (getRooms() == null) { // Wait for minimal loading to be done
  125. setTimeout(initGui, 1000);
  126. return;
  127. }
  128.  
  129. guiDiv.style.position = "absolute";
  130. guiDiv.style.zIndex = "100001"; //On top of the game
  131. guiDiv.style.left = "0px";
  132. guiDiv.style.top = "0px";
  133. guiDiv.style.width = "100%";
  134. guiDiv.style.textAlign = "center";
  135. guiDiv.style.fontSize = "20px";
  136.  
  137. guiSpan.style.backgroundColor = "rgba(255,255,255,0.5)";
  138. guiSpan.style.padding = "5px";
  139.  
  140. startButton.innerHTML = "Start Bot";
  141. if (window.localStorage.getItem("alphajongAutorun") == "true") {
  142. startButton.innerHTML = "Stop Bot";
  143. }
  144. startButton.style.marginRight = "15px";
  145. startButton.onclick = function () {
  146. toggleRun();
  147. };
  148. guiSpan.appendChild(startButton);
  149.  
  150. refreshAIMode();
  151. aimodeCombobox.style.marginRight = "15px";
  152. aimodeCombobox.onchange = function() {
  153. aiModeChange();
  154. };
  155. guiSpan.appendChild(aimodeCombobox);
  156.  
  157. autorunCheckbox.type = "checkbox";
  158. autorunCheckbox.id = "autorun";
  159. autorunCheckbox.onclick = function () {
  160. autorunCheckboxClick();
  161. };
  162. if (window.localStorage.getItem("alphajongAutorun") == "true") {
  163. autorunCheckbox.checked = true;
  164. }
  165. guiSpan.appendChild(autorunCheckbox);
  166. var checkboxLabel = document.createElement("label");
  167. checkboxLabel.htmlFor = "autorun";
  168. checkboxLabel.appendChild(document.createTextNode('Autostart'));
  169. checkboxLabel.style.marginRight = "15px";
  170. guiSpan.appendChild(checkboxLabel);
  171.  
  172. refreshRoomSelection();
  173.  
  174. roomCombobox.style.marginRight = "15px";
  175. roomCombobox.onchange = function () {
  176. roomChange();
  177. };
  178.  
  179. if (window.localStorage.getItem("alphajongAutorun") != "true") {
  180. roomCombobox.disabled = true;
  181. }
  182. guiSpan.appendChild(roomCombobox);
  183.  
  184. currentActionOutput.readOnly = "true";
  185. currentActionOutput.size = "20";
  186. currentActionOutput.style.marginRight = "15px";
  187. showCrtActionMsg("Bot is not running.");
  188. if (window.localStorage.getItem("alphajongAutorun") == "true") {
  189. showCrtActionMsg("Bot started.");
  190. }
  191. guiSpan.appendChild(currentActionOutput);
  192.  
  193. debugButton.innerHTML = "Debug";
  194. debugButton.onclick = function () {
  195. showDebugString();
  196. };
  197. if (DEBUG_BUTTON) {
  198. guiSpan.appendChild(debugButton);
  199. }
  200.  
  201. hideButton.innerHTML = "Hide GUI";
  202. hideButton.onclick = function () {
  203. toggleGui();
  204. };
  205. guiSpan.appendChild(hideButton);
  206.  
  207. guiDiv.appendChild(guiSpan);
  208. document.body.appendChild(guiDiv);
  209. toggleGui();
  210. }
  211.  
  212. function toggleGui() {
  213. if (guiDiv.style.display == "block") {
  214. guiDiv.style.display = "none";
  215. }
  216. else {
  217. guiDiv.style.display = "block";
  218. }
  219. }
  220.  
  221. function showDebugString() {
  222. alert("If you notice a bug while playing please go to the correct turn in the replay (before the bad discard), press this button, copy the Debug String from the textbox and include it in your issue on github.");
  223. if (isInGame()) {
  224. setData();
  225. showCrtActionMsg(getDebugString());
  226. }
  227. }
  228.  
  229. function aiModeChange() {
  230. window.localStorage.setItem("alphajongAIMode", aimodeCombobox.value);
  231. MODE = parseInt(aimodeCombobox.value);
  232.  
  233. setAutoCallWin(MODE === AIMODE.AUTO);
  234. }
  235.  
  236. function roomChange() {
  237. window.localStorage.setItem("alphajongRoom", roomCombobox.value);
  238. ROOM = roomCombobox.value;
  239. }
  240.  
  241. function hideButtonClick() {
  242. guiDiv.style.display = "none";
  243. }
  244.  
  245. function autorunCheckboxClick() {
  246. if (autorunCheckbox.checked) {
  247. roomCombobox.disabled = false;
  248. window.localStorage.setItem("alphajongAutorun", "true");
  249. AUTORUN = true;
  250. }
  251. else {
  252. roomCombobox.disabled = true;
  253. window.localStorage.setItem("alphajongAutorun", "false");
  254. AUTORUN = false;
  255. }
  256. }
  257.  
  258. // Refresh the AI mode
  259. function refreshAIMode() {
  260. aimodeCombobox.innerHTML = AIMODE_NAME[MODE];
  261. for (let i = 0; i < AIMODE_NAME.length; i++) {
  262. var option = document.createElement("option");
  263. option.text = AIMODE_NAME[i];
  264. option.value = i;
  265. aimodeCombobox.appendChild(option);
  266. }
  267. aimodeCombobox.value = MODE;
  268. }
  269.  
  270. // Refresh the contents of the Room Selection Combobox with values appropiate for the rank
  271. function refreshRoomSelection() {
  272. roomCombobox.innerHTML = ""; // Clear old entries
  273. getRooms().forEach(function (room) {
  274. if (isInRank(room.id) && room.mode != 0) { // Rooms with mode = 0 are 1 Game only, not sure why they are in the code but not selectable in the UI...
  275. var option = document.createElement("option");
  276. option.text = getRoomName(room);
  277. option.value = room.id;
  278. roomCombobox.appendChild(option);
  279. }
  280. });
  281. roomCombobox.value = ROOM;
  282. }
  283.  
  284. // Show msg to currentActionOutput
  285. function showCrtActionMsg(msg) {
  286. if (!showingStrategy) {
  287. currentActionOutput.value = msg;
  288. }
  289. }
  290.  
  291. // Apend msg to currentActionOutput
  292. function showCrtStrategyMsg(msg) {
  293. showingStrategy = true;
  294. currentActionOutput.value = msg;
  295. }
  296.  
  297. function clearCrtStrategyMsg() {
  298. showingStrategy = false;
  299. currentActionOutput.value = "";
  300. }
  301.  
  302. //################################
  303. // API (MAHJONG SOUL)
  304. // Returns data from Mahjong Souls Javascript
  305. //################################
  306.  
  307.  
  308. function preventAFK() {
  309. if (typeof GameMgr == 'undefined') {
  310. return;
  311. }
  312. GameMgr.Inst._pre_mouse_point.x = Math.floor(Math.random() * 100) + 1;
  313. GameMgr.Inst._pre_mouse_point.y = Math.floor(Math.random() * 100) + 1;
  314. GameMgr.Inst.clientHeatBeat(); // Prevent Client-side AFK
  315. app.NetAgent.sendReq2Lobby('Lobby', 'heatbeat', { no_operation_counter: 0 }); //Prevent Server-side AFK
  316.  
  317. if (typeof view == 'undefined' || typeof view.DesktopMgr == 'undefined' ||
  318. typeof view.DesktopMgr.Inst == 'undefined' || view.DesktopMgr.Inst == null) {
  319. return;
  320. }
  321. view.DesktopMgr.Inst.hangupCount = 0;
  322. //uiscript.UI_Hangup_Warn.Inst.locking
  323. }
  324.  
  325. function hasFinishedMainLobbyLoading() {
  326. if (typeof GameMgr == 'undefined') {
  327. return false;
  328. }
  329. return GameMgr.Inst.login_loading_end || isInGame();
  330. }
  331.  
  332. function searchForGame() {
  333. uiscript.UI_PiPeiYuYue.Inst.addMatch(ROOM);
  334.  
  335. // Direct way to search for a game, without UI:
  336. // app.NetAgent.sendReq2Lobby('Lobby', 'startUnifiedMatch', {match_sid: 1 + ":" + ROOM, client_version_string: GameMgr.Inst.getClientVersion()});
  337. }
  338.  
  339. function getOperationList() {
  340. return view.DesktopMgr.Inst.oplist;
  341. }
  342.  
  343. function getOperations() {
  344. return mjcore.E_PlayOperation;
  345. }
  346.  
  347. function getDora() {
  348. return view.DesktopMgr.Inst.dora;
  349. }
  350.  
  351. function getPlayerHand() {
  352. return view.DesktopMgr.Inst.players[0].hand;
  353. }
  354.  
  355. function getDiscardsOfPlayer(player) {
  356. player = getCorrectPlayerNumber(player);
  357. return view.DesktopMgr.Inst.players[player].container_qipai;
  358. }
  359.  
  360. function getCallsOfPlayer(player) {
  361. player = getCorrectPlayerNumber(player);
  362.  
  363. var callArray = [];
  364. //Mark the tiles with the player who discarded the tile
  365. for (let ming of view.DesktopMgr.Inst.players[player].container_ming.mings) {
  366. for (var i = 0; i < ming.pais.length; i++) {
  367. ming.pais[i].from = ming.from[i];
  368. if (i == 3) {
  369. ming.pais[i].kan = true;
  370. }
  371. else {
  372. ming.pais[i].kan = false;
  373. }
  374. callArray.push(ming.pais[i]);
  375. }
  376. }
  377.  
  378. return callArray;
  379. }
  380.  
  381. function getNumberOfKitaOfPlayer(player) {
  382. player = getCorrectPlayerNumber(player);
  383.  
  384. return view.DesktopMgr.Inst.players[player].container_babei.pais.length;
  385. }
  386.  
  387. function getTilesLeft() {
  388. return view.DesktopMgr.Inst.left_tile_count;
  389. }
  390.  
  391. function localPosition2Seat(player) {
  392. player = getCorrectPlayerNumber(player);
  393. return view.DesktopMgr.Inst.localPosition2Seat(player);
  394. }
  395.  
  396. function seat2LocalPosition(playerSeat) {
  397. return view.DesktopMgr.Inst.seat2LocalPosition(playerSeat);
  398. }
  399.  
  400. function getCurrentPlayer() {
  401. return view.DesktopMgr.Inst.index_player;
  402. }
  403.  
  404. function getSeatWind(player) {
  405. if (getNumberOfPlayers() == 3) {
  406. return ((3 + localPosition2Seat(player) - view.DesktopMgr.Inst.index_ju) % 3) + 1;
  407. }
  408. else {
  409. return ((4 + localPosition2Seat(player) - view.DesktopMgr.Inst.index_ju) % 4) + 1;
  410. }
  411. }
  412.  
  413. function getRound() {
  414. return view.DesktopMgr.Inst.index_ju + 1;
  415. }
  416.  
  417. function getRoundWind() {
  418. return view.DesktopMgr.Inst.index_change + 1;
  419. }
  420.  
  421. function setAutoCallWin(win) {
  422. if (!isInGame())
  423. return;
  424.  
  425. view.DesktopMgr.Inst.setAutoHule(win);
  426. //view.DesktopMgr.Inst.setAutoNoFulu(true) //Auto No Chi/Pon/Kan
  427. try {
  428. uiscript.UI_DesktopInfo.Inst.refreshFuncBtnShow(uiscript.UI_DesktopInfo.Inst._container_fun.getChildByName("btn_autohu"), view.DesktopMgr.Inst.auto_hule); //Refresh GUI Button
  429. }
  430. catch {
  431. return;
  432. }
  433. }
  434.  
  435. function getTileForCall() {
  436. if (view.DesktopMgr.Inst.lastqipai == null) {
  437. return { index: 0, type: 0, dora: false, doraValue: 0 };
  438. }
  439. var tile = view.DesktopMgr.Inst.lastqipai.val;
  440. tile.doraValue = getTileDoraValue(tile);
  441. return tile;
  442. }
  443.  
  444. function makeCall(type) {
  445. if (MODE === AIMODE.AUTO) {
  446. app.NetAgent.sendReq2MJ('FastTest', 'inputChiPengGang', { type: type, index: 0, timeuse: Math.random() * 2 + 1 });
  447. view.DesktopMgr.Inst.WhenDoOperation();
  448. } else {
  449. showCrtStrategyMsg(`Accept: Call ${getCallNameByType(type)};`);
  450. }
  451. }
  452.  
  453. function makeCallWithOption(type, option) {
  454. if (MODE === AIMODE.AUTO) {
  455. app.NetAgent.sendReq2MJ('FastTest', 'inputChiPengGang', { type: type, index: option, timeuse: Math.random() * 2 + 1 });
  456. view.DesktopMgr.Inst.WhenDoOperation();
  457. } else {
  458. showCrtStrategyMsg(`Accept ${option}: Call ${getCallNameByType(type)};`);
  459. }
  460. }
  461.  
  462. function declineCall(operation) {
  463. if (MODE === AIMODE.AUTO) {
  464. try {
  465. if (operation == getOperationList()[getOperationList().length - 1].type) { //Is last operation -> Send decline Command
  466. app.NetAgent.sendReq2MJ('FastTest', 'inputChiPengGang', { cancel_operation: true, timeuse: 2 });
  467. view.DesktopMgr.Inst.WhenDoOperation();
  468. }
  469. }
  470. catch {
  471. log("Failed to decline the Call. Maybe someone else was faster?");
  472. }
  473. } else {
  474. showCrtStrategyMsg(`Decline: Call ${getCallNameByType(operation)};`);
  475. }
  476. }
  477.  
  478. function sendRiichiCall(tile, moqie) {
  479. if (MODE === AIMODE.AUTO) {
  480. app.NetAgent.sendReq2MJ('FastTest', 'inputOperation', { type: mjcore.E_PlayOperation.liqi, tile: tile, moqie: moqie, timeuse: Math.random() * 2 + 1 }); //Moqie: Throwing last drawn tile (Riichi -> false)
  481. } else {
  482. let tileName = getTileEmojiByName(tile);
  483. showCrtStrategyMsg(`Riichi: ${tileName};`);
  484. }
  485. }
  486.  
  487. function sendKitaCall() {
  488. if (MODE === AIMODE.AUTO) {
  489. var moqie = view.DesktopMgr.Inst.mainrole.last_tile.val.toString() == "4z";
  490. app.NetAgent.sendReq2MJ('FastTest', 'inputOperation', { type: mjcore.E_PlayOperation.babei, moqie: moqie, timeuse: Math.random() * 2 + 1 });
  491. view.DesktopMgr.Inst.WhenDoOperation();
  492. } else {
  493. showCrtStrategyMsg(`Accept: Kita;`);
  494. }
  495. }
  496.  
  497. function sendAbortiveDrawCall() {
  498. if (MODE === AIMODE.AUTO) {
  499. app.NetAgent.sendReq2MJ('FastTest', 'inputOperation', { type: mjcore.E_PlayOperation.jiuzhongjiupai, index: 0, timeuse: Math.random() * 2 + 1 });
  500. view.DesktopMgr.Inst.WhenDoOperation();
  501. } else {
  502. showCrtStrategyMsg(`Accept: Kyuushu Kyuuhai;`);
  503. }
  504. }
  505.  
  506. function callDiscard(tileNumber) {
  507. if (MODE === AIMODE.AUTO) {
  508. try {
  509. if (view.DesktopMgr.Inst.players[0].hand[tileNumber].valid) {
  510. view.DesktopMgr.Inst.players[0]._choose_pai = view.DesktopMgr.Inst.players[0].hand[tileNumber];
  511. view.DesktopMgr.Inst.players[0].DoDiscardTile();
  512. }
  513. }
  514. catch {
  515. log("Failed to discard the tile.");
  516. }
  517. } else {
  518. let tileID = ownHand[tileNumber];
  519. let tileName = getTileName(tileID, false);
  520. showCrtStrategyMsg(`Discard: ${tileName};`);
  521. if (CHANGE_RECOMMEND_TILE_COLOR) {
  522. view.DesktopMgr.Inst.mainrole.hand.forEach(
  523. tile => tile.val.toString() == tileID ?
  524. tile._SetColor(new Laya.Vector4(0.5, 0.8, 0.9, 1))
  525. : tile._SetColor(new Laya.Vector4(1, 1, 1, 1)));
  526. }
  527. }
  528. }
  529.  
  530. function getPlayerLinkState(player) {
  531. player = getCorrectPlayerNumber(player);
  532. return view.DesktopMgr.player_link_state[localPosition2Seat(player)];
  533. }
  534.  
  535. function getNumberOfTilesInHand(player) {
  536. player = getCorrectPlayerNumber(player);
  537. return view.DesktopMgr.Inst.players[player].hand.length;
  538. }
  539.  
  540. function isEndscreenShown() {
  541. return this != null && view != null && view.DesktopMgr != null &&
  542. view.DesktopMgr.Inst != null && view.DesktopMgr.Inst.gameEndResult != null;
  543. }
  544.  
  545. function isDisconnect() {
  546. return uiscript.UI_Hanguplogout.Inst != null && uiscript.UI_Hanguplogout.Inst._me.visible;
  547. }
  548.  
  549. function isPlayerRiichi(player) {
  550. var player_correct = getCorrectPlayerNumber(player);
  551. return view.DesktopMgr.Inst.players[player_correct].liqibang._activeInHierarchy || getDiscardsOfPlayer(player).last_is_liqi;
  552. }
  553.  
  554. function isInGame() {
  555. try {
  556. return this != null && view != null && view.DesktopMgr != null &&
  557. view.DesktopMgr.Inst != null && view.DesktopMgr.player_link_state != null &&
  558. view.DesktopMgr.Inst.active && !isEndscreenShown()
  559. }
  560. catch {
  561. return false;
  562. }
  563. }
  564.  
  565. function doesPlayerExist(player) {
  566. return typeof view.DesktopMgr.Inst.players[player].hand != 'undefined' && view.DesktopMgr.Inst.players[player].hand != null;
  567. }
  568.  
  569. function getPlayerScore(player) {
  570. player = getCorrectPlayerNumber(player);
  571. return view.DesktopMgr.Inst.players[player].score;
  572. }
  573.  
  574. //Needs to be called before calls array is updated
  575. function hasPlayerHandChanged(player) {
  576. var player_correct = getCorrectPlayerNumber(player);
  577. for (let hand of view.DesktopMgr.Inst.players[player_correct].hand) {
  578. if (hand.old != true) {
  579. return true;
  580. }
  581. }
  582. return getCallsOfPlayer(player).length > calls[player].length;
  583. }
  584.  
  585. //Sets a variable for each pai in a players hand
  586. function rememberPlayerHand(player) {
  587. var player_correct = getCorrectPlayerNumber(player);
  588. for (let tile of view.DesktopMgr.Inst.players[player_correct].hand) {
  589. tile.old = true;
  590. }
  591. }
  592.  
  593. function isEastRound() {
  594. return view.DesktopMgr.Inst.game_config.mode.mode % 10 == 1;
  595. }
  596.  
  597. // Is the player able to join a given room
  598. function isInRank(room) {
  599. var roomData = cfg.desktop.matchmode.get(room);
  600. try {
  601. var rank = GameMgr.Inst.account_data[roomData.mode < 10 ? "level" : "level3"].id; // 4 player or 3 player rank
  602. return (roomData.room == 100) || (roomData.level_limit <= rank && roomData.level_limit_ceil >= rank); // room 100 is casual mode
  603. }
  604. catch {
  605. return roomData.room == 100 || roomData.level_limit > 0; // Display the Casual Rooms and all ranked rooms (no special rooms)
  606. }
  607. }
  608.  
  609. // Map of all Rooms
  610. function getRooms() {
  611. try {
  612. return cfg.desktop.matchmode;
  613. }
  614. catch {
  615. return null;
  616. }
  617. }
  618.  
  619. // Returns the room of the current game as a number: Bronze = 1, Silver = 2 etc.
  620. function getCurrentRoom() {
  621. try {
  622. var currentRoom = view.DesktopMgr.Inst.game_config.meta.mode_id;
  623. return getRooms().map_[currentRoom].room;
  624. }
  625. catch {
  626. return 0;
  627. }
  628. }
  629.  
  630. // Client language: ["chs", "chs_t", "en", "jp"]
  631. function getLanguage() {
  632. return GameMgr.client_language;
  633. }
  634.  
  635. // Name of a room in client language
  636. function getRoomName(room) {
  637. return room["room_name_" + getLanguage()] + " (" + game.Tools.room_mode_desc(room.mode) + ")";
  638. }
  639.  
  640. //How much seconds left for a turn (base value, 20 at start)
  641. function getOverallTimeLeft() {
  642. try {
  643. return uiscript.UI_DesktopInfo.Inst._timecd._add;
  644. }
  645. catch {
  646. return 20;
  647. }
  648. }
  649.  
  650. //How much time was left in the last turn?
  651. function getLastTurnTimeLeft() {
  652. try {
  653. return uiscript.UI_DesktopInfo.Inst._timecd._pre_sec;
  654. }
  655. catch {
  656. return 25;
  657. }
  658. }
  659.  
  660. // Extend some internal MJSoul functions with additional code
  661. function extendMJSoulFunctions() {
  662. if (functionsExtended) {
  663. return;
  664. }
  665. trackDiscardTiles();
  666. functionsExtended = true;
  667. }
  668.  
  669. // Track which tiles the players discarded (for push/fold judgement and tracking the riichi tile)
  670. function trackDiscardTiles() {
  671. for (var i = 1; i < getNumberOfPlayers(); i++) {
  672. var player = getCorrectPlayerNumber(i);
  673. view.DesktopMgr.Inst.players[player].container_qipai.AddQiPai = (function (_super) { // Extend the MJ-Soul Discard function
  674. return function () {
  675. if (arguments[1]) { // Contains true when Riichi
  676. riichiTiles[seat2LocalPosition(this.player.seat)] = arguments[0]; // Track tile in riichiTiles Variable
  677. }
  678. setData(false);
  679. visibleTiles.push(arguments[0]);
  680. var danger = getTileDanger(arguments[0], seat2LocalPosition(this.player.seat));
  681. if (arguments[2] && danger < 0.01) { // Ignore Tsumogiri of a safetile, set it to average danger
  682. danger = 0.05;
  683. }
  684. playerDiscardSafetyList[seat2LocalPosition(this.player.seat)].push(danger);
  685. return _super.apply(this, arguments); // Call original function
  686. };
  687. })(view.DesktopMgr.Inst.players[player].container_qipai.AddQiPai);
  688. }
  689. }
  690.  
  691. //################################
  692. // UTILS
  693. // Contains utility functions
  694. //################################
  695.  
  696. //Return the number of players in the game (3 or 4)
  697. function getNumberOfPlayers() {
  698. if (!doesPlayerExist(1) || !doesPlayerExist(2) || !doesPlayerExist(3)) {
  699. return 3;
  700. }
  701. return 4;
  702. }
  703.  
  704. //Correct the player numbers
  705. //Only necessary for 3 player games
  706. function getCorrectPlayerNumber(player) {
  707. if (getNumberOfPlayers() == 4) {
  708. return player;
  709. }
  710. if (!doesPlayerExist(1)) {
  711. if (player > 0) {
  712. return player + 1;
  713. }
  714. }
  715. if (!doesPlayerExist(2)) {
  716. if (player > 1) {
  717. return player + 1;
  718. }
  719. }
  720. return player;
  721. }
  722.  
  723. function isSameTile(tile1, tile2, checkDora = false) {
  724. if (typeof tile1 == 'undefined' || typeof tile2 == 'undefined') {
  725. return false;
  726. }
  727. if (checkDora) {
  728. return tile1.index == tile2.index && tile1.type == tile2.type && tile1.dora == tile2.dora;
  729. }
  730. return tile1.index == tile2.index && tile1.type == tile2.type;
  731. }
  732.  
  733. //Return number of doras in tiles
  734. function getNumberOfDoras(tiles) {
  735. var dr = 0;
  736. for (let tile of tiles) {
  737. dr += tile.doraValue;
  738. }
  739. return dr;
  740. }
  741.  
  742. //Pairs in tiles
  743. function getPairs(tiles) {
  744. var sortedTiles = sortTiles(tiles);
  745.  
  746. var pairs = [];
  747. var oldIndex = 0;
  748. var oldType = 0;
  749. sortedTiles.forEach(function (tile) {
  750. if (oldIndex != tile.index || oldType != tile.type) {
  751. var ts = getTilesInTileArray(sortedTiles, tile.index, tile.type);
  752. if ((ts.length >= 2)) {
  753. pairs.push({ tile1: ts[0], tile2: ts[1] }); //Grabs highest dora tiles first
  754. }
  755. oldIndex = tile.index;
  756. oldType = tile.type;
  757. }
  758. });
  759. return pairs;
  760. }
  761.  
  762. //Pairs in tiles as array
  763. function getPairsAsArray(tiles) {
  764. var pairs = getPairs(tiles);
  765. var pairList = [];
  766. pairs.forEach(function (pair) {
  767. pairList.push(pair.tile1);
  768. pairList.push(pair.tile2);
  769. });
  770. return pairList;
  771. }
  772.  
  773. //Return doubles in tiles
  774. function getDoubles(tiles) {
  775. tiles = sortTiles(tiles);
  776. var doubles = [];
  777. for (let i = 0; i < tiles.length - 1; i++) {
  778. if (tiles[i].type == tiles[i + 1].type && (
  779. tiles[i].index == tiles[i + 1].index ||
  780. (tiles[i].type != 3 &&
  781. tiles[i].index + 2 >= tiles[i + 1].index))) {
  782. doubles.push(tiles[i]);
  783. doubles.push(tiles[i + 1]);
  784. i++;
  785. }
  786. }
  787. return doubles;
  788. }
  789.  
  790. //Return all triplets/3-sequences and pairs as a tile array
  791. function getTriplesAndPairs(tiles) {
  792. var sequences = getSequences(tiles);
  793. var triplets = getTriplets(tiles);
  794. var pairs = getPairs(tiles);
  795. return getBestCombinationOfTiles(tiles, sequences.concat(triplets).concat(pairs), { triples: [], pairs: [], shanten: 8 });
  796. }
  797.  
  798. //Return all triplets/3-tile-sequences as a tile array
  799. function getTriples(tiles) {
  800. var sequences = getSequences(tiles);
  801. var triplets = getTriplets(tiles);
  802. return getBestCombinationOfTiles(tiles, sequences.concat(triplets), { triples: [], pairs: [], shanten: 8 }).triples;
  803. }
  804.  
  805. //Return all triplets in tile array
  806. function getTriplets(tiles) {
  807. var sortedTiles = sortTiles(tiles);
  808.  
  809. var triples = [];
  810. var oldIndex = 0;
  811. var oldType = 0;
  812. sortedTiles.forEach(function (tile) {
  813. if (oldIndex != tile.index || oldType != tile.type) {
  814. var ts = getTilesInTileArray(sortedTiles, tile.index, tile.type);
  815. if ((ts.length >= 3)) {
  816. triples.push({ tile1: ts[0], tile2: ts[1], tile3: ts[2] }); //Grabs highest dora tiles first because of sorting
  817. }
  818. oldIndex = tile.index;
  819. oldType = tile.type;
  820. }
  821. });
  822. return triples;
  823. }
  824.  
  825. //Triplets in tiles as array
  826. function getTripletsAsArray(tiles) {
  827. var triplets = getTriplets(tiles);
  828. var tripletsList = [];
  829. triplets.forEach(function (triplet) {
  830. tripletsList.push(triplet.tile1);
  831. tripletsList.push(triplet.tile2);
  832. tripletsList.push(triplet.tile3);
  833. });
  834. return tripletsList;
  835. }
  836.  
  837. //Returns the best combination of sequences.
  838. //Small Bug: Can return red dora tiles multiple times, but doesn't matter for the current use cases
  839. function getBestSequenceCombination(inputHand) {
  840. return getBestCombinationOfTiles(inputHand, getSequences(inputHand), { triples: [], pairs: [], shanten: 8 }).triples;
  841. }
  842.  
  843. //Check if there is already a red dora tile in the tiles array.
  844. //More or less a workaround for a problem with the getBestCombinationOfTiles function...
  845. function pushTileAndCheckDora(tiles, arrayToPush, tile) {
  846. if (tile.dora && tiles.some(t => t.type == tile.type && t.dora)) {
  847. var nonDoraTile = { ...tile };
  848. nonDoraTile.dora = false;
  849. nonDoraTile.doraValue = getTileDoraValue(nonDoraTile);
  850. arrayToPush.push(nonDoraTile);
  851. return nonDoraTile;
  852. }
  853. arrayToPush.push(tile);
  854. return tile;
  855. }
  856.  
  857. //Return the best combination of 3-tile Sequences, Triplets and pairs in array of tiles
  858. //Recursive Function, weird code that can probably be optimized
  859. function getBestCombinationOfTiles(inputTiles, possibleCombinations, chosenCombinations) {
  860. var originalC = { triples: [...chosenCombinations.triples], pairs: [...chosenCombinations.pairs], shanten: chosenCombinations.shanten };
  861. for (var i = 0; i < possibleCombinations.length; i++) {
  862. var cs = { triples: [...originalC.triples], pairs: [...originalC.pairs], shanten: originalC.shanten };
  863. var tiles = possibleCombinations[i];
  864. var hand = [...inputTiles];
  865. if (!("tile3" in tiles)) { // Pairs
  866. if (tiles.tile1.index == tiles.tile2.index && getNumberOfTilesInTileArray(hand, tiles.tile1.index, tiles.tile1.type) < 2) {
  867. continue;
  868. }
  869. }
  870. else if (getNumberOfTilesInTileArray(hand, tiles.tile1.index, tiles.tile1.type) == 0 ||
  871. getNumberOfTilesInTileArray(hand, tiles.tile2.index, tiles.tile2.type) == 0 ||
  872. getNumberOfTilesInTileArray(hand, tiles.tile3.index, tiles.tile3.type) == 0 ||
  873. (tiles.tile1.index == tiles.tile2.index && getNumberOfTilesInTileArray(hand, tiles.tile1.index, tiles.tile1.type) < 3)) {
  874. continue;
  875. }
  876. if ("tile3" in tiles) {
  877. var tt = pushTileAndCheckDora(cs.pairs.concat(cs.triples), cs.triples, tiles.tile1);
  878. hand = removeTilesFromTileArray(hand, [tt]);
  879. tt = pushTileAndCheckDora(cs.pairs.concat(cs.triples), cs.triples, tiles.tile2);
  880. hand = removeTilesFromTileArray(hand, [tt]);
  881. tt = pushTileAndCheckDora(cs.pairs.concat(cs.triples), cs.triples, tiles.tile3);
  882. hand = removeTilesFromTileArray(hand, [tt]);
  883. }
  884. else {
  885. var tt = pushTileAndCheckDora(cs.pairs.concat(cs.triples), cs.pairs, tiles.tile1);
  886. hand = removeTilesFromTileArray(hand, [tt]);
  887. tt = pushTileAndCheckDora(cs.pairs.concat(cs.triples), cs.pairs, tiles.tile2);
  888. hand = removeTilesFromTileArray(hand, [tt]);
  889. }
  890.  
  891. if (PERFORMANCE_MODE - timeSave <= 3) {
  892. var anotherChoice = getBestCombinationOfTiles(hand, possibleCombinations.slice(i + 1), cs);
  893. if (anotherChoice.triples.length > chosenCombinations.triples.length ||
  894. (anotherChoice.triples.length == chosenCombinations.triples.length &&
  895. anotherChoice.pairs.length > chosenCombinations.pairs.length) ||
  896. (anotherChoice.triples.length == chosenCombinations.triples.length &&
  897. anotherChoice.pairs.length == chosenCombinations.pairs.length &&
  898. getNumberOfDoras(anotherChoice.triples.concat(anotherChoice.pairs)) > getNumberOfDoras(chosenCombinations.triples.concat(chosenCombinations.pairs)))) {
  899. chosenCombinations = anotherChoice;
  900. }
  901. }
  902. else {
  903. if (cs.triples.length >= chosenCombinations.triples.length) {
  904. var doubles = getDoubles(hand); //This is costly, so only do it when performance mode is at maximum
  905. cs.shanten = calculateShanten(parseInt(cs.triples.length / 3), parseInt(cs.pairs.length / 2), parseInt(doubles.length / 2));
  906. }
  907. else {
  908. cs.shanten = 8;
  909. }
  910.  
  911. var anotherChoice = getBestCombinationOfTiles(hand, possibleCombinations.slice(i + 1), cs);
  912. if (anotherChoice.shanten < chosenCombinations.shanten || anotherChoice.shanten == chosenCombinations.shanten && (anotherChoice.triples.length > chosenCombinations.triples.length ||
  913. (anotherChoice.triples.length == chosenCombinations.triples.length &&
  914. anotherChoice.pairs.length > chosenCombinations.pairs.length) ||
  915. (anotherChoice.triples.length == chosenCombinations.triples.length &&
  916. anotherChoice.pairs.length == chosenCombinations.pairs.length &&
  917. getNumberOfDoras(anotherChoice.triples.concat(anotherChoice.pairs)) > getNumberOfDoras(chosenCombinations.triples.concat(chosenCombinations.pairs))))) {
  918. chosenCombinations = anotherChoice;
  919. }
  920. }
  921. }
  922.  
  923. return chosenCombinations;
  924. }
  925.  
  926. //Return all 3-tile Sequences in tile array
  927. function getSequences(tiles) {
  928. var sortedTiles = sortTiles(tiles);
  929. var sequences = [];
  930. for (var index = 0; index <= 7; index++) {
  931. for (var type = 0; type <= 2; type++) {
  932. var tiles1 = getTilesInTileArray(sortedTiles, index, type);
  933. var tiles2 = getTilesInTileArray(sortedTiles, index + 1, type);
  934. var tiles3 = getTilesInTileArray(sortedTiles, index + 2, type);
  935.  
  936. var i = 0;
  937. while (tiles1.length > i && tiles2.length > i && tiles3.length > i) {
  938. sequences.push({ tile1: tiles1[i], tile2: tiles2[i], tile3: tiles3[i] });
  939. i++;
  940. }
  941. }
  942. }
  943. return sequences;
  944. }
  945.  
  946. //Return tile array without given tiles
  947. function removeTilesFromTileArray(inputTiles, tiles) {
  948. var tileArray = [...inputTiles];
  949.  
  950. for (let tile of tiles) {
  951. for (var j = 0; j < tileArray.length; j++) {
  952. if (isSameTile(tile, tileArray[j])) {
  953. tileArray.splice(j, 1);
  954. break;
  955. }
  956. }
  957. }
  958.  
  959. return tileArray;
  960. }
  961.  
  962. //Sort tiles
  963. function sortTiles(inputTiles) {
  964. var tiles = [...inputTiles];
  965. tiles = tiles.sort(function (p1, p2) { //Sort dora value descending
  966. return p2.doraValue - p1.doraValue;
  967. });
  968. tiles = tiles.sort(function (p1, p2) { //Sort index ascending
  969. return p1.index - p2.index;
  970. });
  971. tiles = tiles.sort(function (p1, p2) { //Sort type ascending
  972. return p1.type - p2.type;
  973. });
  974. return tiles;
  975. }
  976.  
  977. //Return number of specific tiles available
  978. function getNumberOfTilesAvailable(index, type) {
  979. if (index < 1 || index > 9 || type < 0 || type > 3 || (type == 3 && index > 7)) {
  980. return 0;
  981. }
  982. if (getNumberOfPlayers() == 3 && (index > 1 && index < 9 && type == 1)) {
  983. return 0;
  984. }
  985.  
  986. return 4 - visibleTiles.filter(tile => tile.index == index && tile.type == type).length;
  987. }
  988.  
  989. //Return if a tile is furiten
  990. function isTileFuriten(index, type) {
  991. for (var i = 1; i < getNumberOfPlayers(); i++) { //Check if melds from other player contain discarded tiles of player 0
  992. if (calls[i].some(tile => tile.index == index && tile.type == type && tile.from == localPosition2Seat(0))) {
  993. return true;
  994. }
  995. }
  996. return discards[0].some(tile => tile.index == index && tile.type == type);
  997. }
  998.  
  999. //Return number of specific non furiten tiles available
  1000. function getNumberOfNonFuritenTilesAvailable(index, type) {
  1001. if (isTileFuriten(index, type)) {
  1002. return 0;
  1003. }
  1004. return getNumberOfTilesAvailable(index, type);
  1005. }
  1006.  
  1007. //Return number of specific tile in tile array
  1008. function getNumberOfTilesInTileArray(tileArray, index, type) {
  1009. return getTilesInTileArray(tileArray, index, type).length;
  1010. }
  1011.  
  1012. //Return specific tiles in tile array
  1013. function getTilesInTileArray(tileArray, index, type) {
  1014. return tileArray.filter(tile => tile.index == index && tile.type == type);
  1015. }
  1016.  
  1017. //Update the available tile pool
  1018. function updateAvailableTiles() {
  1019. visibleTiles = dora.concat(ownHand, discards[0], discards[1], discards[2], discards[3], calls[0], calls[1], calls[2], calls[3]);
  1020. visibleTiles = visibleTiles.filter(tile => typeof tile != 'undefined');
  1021. availableTiles = [];
  1022. for (var i = 0; i <= 3; i++) {
  1023. for (var j = 1; j <= 9; j++) {
  1024. if (i == 3 && j == 8) {
  1025. break;
  1026. }
  1027. for (var k = 1; k <= getNumberOfTilesAvailable(j, i); k++) {
  1028. var isRed = (j == 5 && i != 3 && visibleTiles.concat(availableTiles).filter(tile => tile.type == i && tile.dora).length == 0) ? true : false;
  1029. availableTiles.push({
  1030. index: j,
  1031. type: i,
  1032. dora: isRed,
  1033. doraValue: getTileDoraValue({ index: j, type: i, dora: isRed })
  1034. });
  1035. }
  1036. }
  1037. }
  1038. for (let vis of visibleTiles) {
  1039. vis.doraValue = getTileDoraValue(vis);
  1040. }
  1041. }
  1042.  
  1043. //Return sum of red dora/dora indicators for tile
  1044. function getTileDoraValue(tile) {
  1045. var dr = 0;
  1046.  
  1047. if (getNumberOfPlayers() == 3) {
  1048. if (tile.type == 3 && tile.index == 4) { //North Tiles
  1049. dr = 1;
  1050. }
  1051. }
  1052.  
  1053. for (let d of dora) {
  1054. if (d.type == tile.type && getHigherTileIndex(d) == tile.index) {
  1055. dr++;
  1056. }
  1057. }
  1058.  
  1059. if (tile.dora) {
  1060. return dr + 1;
  1061. }
  1062. return dr;
  1063. }
  1064.  
  1065. //Helper function for dora indicators
  1066. function getHigherTileIndex(tile) {
  1067. if (tile.type == 3) {
  1068. if (tile.index == 4) {
  1069. return 1;
  1070. }
  1071. return tile.index == 7 ? 5 : tile.index + 1;
  1072. }
  1073. if (getNumberOfPlayers() == 3 && tile.index == 1 && tile.type == 1) {
  1074. return 9; // 3 player mode: 1 man indicator means 9 man is dora
  1075. }
  1076. return tile.index == 9 ? 1 : tile.index + 1;
  1077. }
  1078.  
  1079. //Returns true if DEBUG flag is set
  1080. function isDebug() {
  1081. return typeof DEBUG != 'undefined';
  1082. }
  1083.  
  1084. //Adds calls of player 0 to the hand
  1085. function getHandWithCalls(inputHand) {
  1086. return inputHand.concat(calls[0]);
  1087. }
  1088.  
  1089. //Adds a tile if not in array
  1090. function pushTileIfNotExists(tiles, index, type) {
  1091. if (tiles.findIndex(t => t.index == index && t.type == type) === -1) {
  1092. var tile = { index: index, type: type, dora: false };
  1093. tile.doraValue = getTileDoraValue(tile);
  1094. tiles.push(tile);
  1095. }
  1096. }
  1097.  
  1098. //Returns true if player can call riichi
  1099. function canRiichi() {
  1100. if (isDebug()) {
  1101. return false;
  1102. }
  1103. var operations = getOperationList();
  1104. for (let op of operations) {
  1105. if (op.type == getOperations().liqi) {
  1106. return true;
  1107. }
  1108. }
  1109. return false;
  1110. }
  1111.  
  1112. function getUradoraChance() {
  1113. if (getNumberOfPlayers() == 4) {
  1114. return dora.length * 0.4;
  1115. }
  1116. else {
  1117. return dora.length * 0.5;
  1118. }
  1119. }
  1120.  
  1121. //Returns tiles that can form a triple in one turn for a given tile array
  1122. function getUsefulTilesForTriple(tileArray) {
  1123. var tiles = [];
  1124. for (let tile of tileArray) {
  1125. var amount = getNumberOfTilesInTileArray(tileArray, tile.index, tile.type);
  1126. if (tile.type == 3 && amount >= 2) {
  1127. pushTileIfNotExists(tiles, tile.index, tile.type);
  1128. continue;
  1129. }
  1130.  
  1131. if (amount >= 2) {
  1132. pushTileIfNotExists(tiles, tile.index, tile.type);
  1133. }
  1134.  
  1135. var amountLower = getNumberOfTilesInTileArray(tileArray, tile.index - 1, tile.type);
  1136. var amountLower2 = getNumberOfTilesInTileArray(tileArray, tile.index - 2, tile.type);
  1137. var amountUpper = getNumberOfTilesInTileArray(tileArray, tile.index + 1, tile.type);
  1138. var amountUpper2 = getNumberOfTilesInTileArray(tileArray, tile.index + 2, tile.type);
  1139. if (tile.index > 1 && (amount == amountLower + 1 && (amountUpper > 0 || amountLower2 > 0))) { //No need to check if index in bounds
  1140. pushTileIfNotExists(tiles, tile.index - 1, tile.type);
  1141. }
  1142.  
  1143. if (tile.index < 9 && (amount == amountUpper + 1 && (amountLower > 0 || amountUpper2 > 0))) {
  1144. pushTileIfNotExists(tiles, tile.index + 1, tile.type);
  1145. }
  1146. }
  1147. return tiles;
  1148. }
  1149.  
  1150. //Returns tiles that can form at least a double in one turn for a given tile array
  1151. function getUsefulTilesForDouble(tileArray) {
  1152. var tiles = [];
  1153. for (let tile of tileArray) {
  1154. pushTileIfNotExists(tiles, tile.index, tile.type);
  1155. if (tile.type == 3) {
  1156. continue;
  1157. }
  1158.  
  1159. if (tile.index - 1 >= 1) {
  1160. pushTileIfNotExists(tiles, tile.index - 1, tile.type);
  1161. }
  1162. if (tile.index + 1 <= 9) {
  1163. pushTileIfNotExists(tiles, tile.index + 1, tile.type);
  1164. }
  1165.  
  1166. if (PERFORMANCE_MODE - timeSave <= 2) {
  1167. continue;
  1168. }
  1169. if (tile.index - 2 >= 1) {
  1170. pushTileIfNotExists(tiles, tile.index - 2, tile.type);
  1171. }
  1172. if (tile.index + 2 <= 9) {
  1173. pushTileIfNotExists(tiles, tile.index + 2, tile.type);
  1174. }
  1175. }
  1176. return tiles;
  1177. }
  1178.  
  1179. // Returns Tile[], where all are terminal/honors.
  1180. function getAllTerminalHonorFromHand(hand) {
  1181. return hand.filter(tile => isTerminalOrHonor(tile));
  1182. }
  1183.  
  1184. //Honor tile or index 1/9
  1185. function isTerminalOrHonor(tile) {
  1186. // Honor tiles
  1187. if (tile.type == 3) {
  1188. return true;
  1189. }
  1190.  
  1191. // 1 or 9.
  1192. if (tile.index == 1 || tile.index == 9) {
  1193. return true;
  1194. }
  1195.  
  1196. return false;
  1197. }
  1198.  
  1199. // Returns a number how "good" the wait is. An average wait is 1, a bad wait (like a middle tile) is lower, a good wait (like an honor tile) is higher.
  1200. function getWaitQuality(tile) {
  1201. var quality = 1.3 - (getDealInChanceForTileAndPlayer(0, tile, 1) * 5);
  1202. quality = quality < 0.7 ? 0.7 : quality;
  1203. return quality;
  1204. }
  1205.  
  1206. //Calculate the shanten number. Based on this: https://www.youtube.com/watch?v=69Xhu-OzwHM
  1207. //Fast and accurate, but original hand needs to have 14 or more tiles.
  1208. function calculateShanten(triples, pairs, doubles) {
  1209. if (isWinningHand(triples, pairs)) {
  1210. return -1;
  1211. }
  1212. if ((triples * 3) + (pairs * 2) + (doubles * 2) > 14) {
  1213. doubles = parseInt((13 - ((triples * 3) + (pairs * 2))) / 2);
  1214. }
  1215. var shanten = 8 - (2 * triples) - (pairs + doubles);
  1216. if (triples + pairs + doubles >= 5 && pairs == 0) {
  1217. shanten++;
  1218. }
  1219. if (triples + pairs + doubles >= 6) {
  1220. shanten += triples + pairs + doubles - 5;
  1221. }
  1222. if (shanten < 0) {
  1223. return 0;
  1224. }
  1225. return shanten;
  1226. }
  1227.  
  1228. // Calculate Score for given han and fu. For higher han values the score is "fluid" to better account for situations where the exact han value is unknown
  1229. // (like when an opponent has around 5.5 han => 10k)
  1230. function calculateScore(player, han, fu = 30) {
  1231. var score = (fu * Math.pow(2, 2 + han) * 4);
  1232.  
  1233. if (han > 4) {
  1234. score = 8000;
  1235. }
  1236.  
  1237. if (han > 5) {
  1238. score = 8000 + ((han - 5) * 4000);
  1239. }
  1240. if (han > 6) {
  1241. score = 12000 + ((han - 6) * 2000);
  1242. }
  1243. if (han > 8) {
  1244. score = 16000 + ((han - 8) * 2666);
  1245. }
  1246. if (han > 11) {
  1247. score = 24000 + ((han - 11) * 4000);
  1248. }
  1249. if (han >= 13) {
  1250. score = 32000;
  1251. }
  1252.  
  1253. if (getSeatWind(player) == 1) { //Is Dealer
  1254. score *= 1.5;
  1255. }
  1256.  
  1257. if (getNumberOfPlayers() == 3) {
  1258. score *= 0.75;
  1259. }
  1260.  
  1261. return score;
  1262. }
  1263.  
  1264. //Calculate the Fu Value for given parameters. Not 100% accurate, but good enough
  1265. function calculateFu(triples, openTiles, pair, waitTiles, winningTile, ron = true) {
  1266. var fu = 20;
  1267.  
  1268. var sequences = getSequences(triples);
  1269. var closedTriplets = getTriplets(triples);
  1270. var openTriplets = getTriplets(openTiles);
  1271.  
  1272. var kans = removeTilesFromTileArray(openTiles, getTriples(openTiles));
  1273.  
  1274. closedTriplets.forEach(function (t) {
  1275. if (isTerminalOrHonor(t.tile1)) {
  1276. if (!isSameTile(t.tile1, winningTile)) {
  1277. fu += 8;
  1278. }
  1279. else { //Ron on that tile: counts as open
  1280. fu += 4;
  1281. }
  1282. }
  1283. else {
  1284. if (!isSameTile(t.tile1, winningTile)) {
  1285. fu += 4;
  1286. }
  1287. else { //Ron on that tile: counts as open
  1288. fu += 2;
  1289. }
  1290. }
  1291. });
  1292.  
  1293. openTriplets.forEach(function (t) {
  1294. if (isTerminalOrHonor(t.tile1)) {
  1295. fu += 4;
  1296. }
  1297. else {
  1298. fu += 2;
  1299. }
  1300. });
  1301.  
  1302. //Kans: Add to existing fu of pon
  1303. kans.forEach(function (tile) {
  1304. if (openTiles.filter(t => isSameTile(t, tile) && t.from != localPosition2Seat(0)).length > 0) { //Is open
  1305. if (isTerminalOrHonor(tile)) {
  1306. fu += 12;
  1307. }
  1308. else {
  1309. fu += 6;
  1310. }
  1311. }
  1312. else { //Closed Kans
  1313. if (isTerminalOrHonor(tile)) {
  1314. fu += 28;
  1315. }
  1316. else {
  1317. fu += 14;
  1318. }
  1319. }
  1320. });
  1321.  
  1322.  
  1323. if (typeof pair[0] != 'undefined' && isValueTile(pair[0])) {
  1324. fu += 2;
  1325. if (pair[0].index == seatWind && seatWind == roundWind) {
  1326. fu += 2;
  1327. }
  1328. }
  1329.  
  1330. if (fu == 20 && (sequences.findIndex(function (t) { //Is there a way to interpret the wait as ryanmen when at 20 fu? -> dont add fu
  1331. return (isSameTile(t.tile1, winningTile) && t.tile3.index < 9) || (isSameTile(t.tile3, winningTile) && t.tile1.index > 1);
  1332. }) >= 0)) {
  1333. fu += 0;
  1334. } //if we are at more than 20 fu: check if the wait can be interpreted in other ways to add more fu
  1335. else if ((waitTiles.length != 2 || waitTiles[0].type != waitTiles[1].type || Math.abs(waitTiles[0].index - waitTiles[1].index) != 1)) {
  1336. if (closedTriplets.findIndex(function (t) { return isSameTile(t.tile1, winningTile); }) < 0) { // 0 fu for shanpon
  1337. fu += 2;
  1338. }
  1339. }
  1340.  
  1341. if (ron && isClosed) {
  1342. fu += 10;
  1343. }
  1344.  
  1345. return Math.ceil(fu / 10) * 10;
  1346. }
  1347.  
  1348. //Is the tile a dragon or valuable wind?
  1349. function isValueTile(tile) {
  1350. return tile.type == 3 && (tile.index > 4 || tile.index == seatWind || tile.index == roundWind);
  1351. }
  1352.  
  1353. //Return a danger value which is the threshold for folding (danger higher than this value -> fold)
  1354. function getFoldThreshold(tilePrio, hand) {
  1355. var handScore = tilePrio.score.open * 1.3; // Raise this value a bit so open hands dont get folded too quickly
  1356. if (isClosed) {
  1357. handScore = tilePrio.score.riichi;
  1358. }
  1359.  
  1360. var waits = tilePrio.waits;
  1361. var shape = tilePrio.shape;
  1362.  
  1363. // Formulas are based on this table: https://docs.google.com/spreadsheets/d/172LFySNLUtboZUiDguf8I3QpmFT-TApUfjOs5iRy3os/edit#gid=212618921
  1364. // TODO: Maybe switch to this: https://riichi-mahjong.com/2020/01/28/mahjong-strategy-push-or-fold-4-maximizing-game-ev/
  1365. if (tilePrio.shanten == 0) {
  1366. var foldValue = (waits + shape) * handScore / 38;
  1367. if (tilesLeft < 8) { //Try to avoid no ten penalty
  1368. foldValue += 200 - (parseInt(tilesLeft / 4) * 100);
  1369. }
  1370. }
  1371. else if (tilePrio.shanten == 1 && strategy == STRATEGIES.GENERAL) {
  1372. shape = shape < 0.4 ? shape = 0.4 : shape;
  1373. shape = shape > 2 ? shape = 2 : shape;
  1374. var foldValue = shape * handScore / 45;
  1375. }
  1376. else {
  1377. if (getCurrentDangerLevel() > 3000 && strategy == STRATEGIES.GENERAL) {
  1378. return 0;
  1379. }
  1380. var foldValue = (((6 - (tilePrio.shanten - tilePrio.efficiency)) * 2000) + handScore) / 500;
  1381. }
  1382.  
  1383. if (isLastGame()) { //Fold earlier when first/later when last in last game
  1384. if (getDistanceToLast() > 0) {
  1385. foldValue *= 1.3; //Last Place -> Later Fold
  1386. }
  1387. else if (getDistanceToFirst() < 0) {
  1388. var dist = (getDistanceToFirst() / 30000) > -0.5 ? getDistanceToFirst() / 30000 : -0.5;
  1389. foldValue *= 1 + dist; //First Place -> Easier Fold
  1390. }
  1391. }
  1392.  
  1393. foldValue *= 1 - (((getWallSize() / 2) - tilesLeft) / (getWallSize() * 2)); // up to 25% more/less fold when early/lategame.
  1394.  
  1395. foldValue *= seatWind == 1 ? 1.2 : 1; //Push more as dealer (it's already in the handScore, but because of Tsumo Malus pushing is even better)
  1396.  
  1397. var safeTiles = 0;
  1398. for (let tile of hand) { // How many safe tiles do we currently have?
  1399. if (getTileDanger(tile) < 20) {
  1400. safeTiles++;
  1401. }
  1402. if (safeTiles == 2) {
  1403. break;
  1404. }
  1405. }
  1406. foldValue *= 1 + (0.5 - (safeTiles / 4)); // 25% less likely to fold when only 1 safetile, or 50% when 0 safetiles
  1407.  
  1408. foldValue *= 2 - (hand.length / 14); // Less likely to fold when fewer tiles in hand (harder to defend)
  1409.  
  1410. foldValue /= SAFETY;
  1411.  
  1412. foldValue = foldValue < 0 ? 0 : foldValue;
  1413.  
  1414. return Number(foldValue).toFixed(2);
  1415. }
  1416.  
  1417. //Return true if danger is too high in relation to the value of the hand
  1418. function shouldFold(tile, highestPrio = false) {
  1419. if (tile.shanten * 4 > tilesLeft) {
  1420. if (highestPrio) {
  1421. log("Hand is too far from tenpai before end of game. Fold!");
  1422. strategy = STRATEGIES.FOLD;
  1423. strategyAllowsCalls = false;
  1424. }
  1425. return true;
  1426. }
  1427.  
  1428. var foldThreshold = getFoldThreshold(tile, ownHand);
  1429. if (highestPrio) {
  1430. log("Would fold this hand above " + foldThreshold + " danger for " + getTileName(tile.tile) + " discard.");
  1431. }
  1432.  
  1433. if (tile.danger > foldThreshold) {
  1434. if (highestPrio) {
  1435. log("Tile Danger " + Number(tile.danger).toFixed(2) + " of " + getTileName(tile.tile, false) + " is too dangerous.");
  1436. strategyAllowsCalls = false; //Don't set the strategy to full fold, but prevent calls
  1437. }
  1438. return true;
  1439. }
  1440. return false;
  1441. }
  1442.  
  1443. //Decide whether to call Riichi
  1444. //Based on: https://mahjong.guide/2018/01/28/mahjong-fundamentals-5-riichi/
  1445. function shouldRiichi(tilePrio) {
  1446. var badWait = tilePrio.waits < 5 - RIICHI;
  1447. var lotsOfDoraIndicators = tilePrio.dora.length >= 3;
  1448.  
  1449. //Chiitoitsu
  1450. if (strategy == STRATEGIES.CHIITOITSU) {
  1451. if (tilePrio.shape == 0) {
  1452. log("Decline Riichi because of chiitoitsu wait that can be improved!");
  1453. return false;
  1454. }
  1455. badWait = tilePrio.waits < 3 - RIICHI;
  1456. }
  1457.  
  1458. //Thirteen Orphans
  1459. if (strategy == STRATEGIES.THIRTEEN_ORPHANS) {
  1460. log("Decline Riichi because of Thirteen Orphan strategy.");
  1461. return false;
  1462. }
  1463.  
  1464. //Close to end of game
  1465. if (tilesLeft <= 7 - RIICHI) {
  1466. log("Decline Riichi because close to end of game.");
  1467. return false;
  1468. }
  1469.  
  1470. //No waits
  1471. if (tilePrio.waits < 1) {
  1472. log("Decline Riichi because of no waits.");
  1473. return false;
  1474. }
  1475.  
  1476. // Last Place (in last game) and Riichi is enough to get third
  1477. if (isLastGame() && getDistanceToLast() > 0 && getDistanceToLast() < tilePrio.score.riichi) {
  1478. log("Accept Riichi because of last place in last game.");
  1479. return true;
  1480. }
  1481.  
  1482. // Decline if last game and first place (either with 10000 points advantage or with a closed yaku)
  1483. if (isLastGame() && (getDistanceToFirst() < -10000 || (tilePrio.yaku.closed >= 1 && getDistanceToFirst() < 0))) {
  1484. log("Decline Riichi because of huge lead in last game.");
  1485. return false;
  1486. }
  1487.  
  1488. // Not Dealer & bad Wait & Riichi is only yaku
  1489. if (seatWind != 1 && badWait && tilePrio.score.riichi < 4000 - (RIICHI * 1000) && !lotsOfDoraIndicators && tilePrio.shape > 0.4) {
  1490. log("Decline Riichi because of worthless hand, bad waits and not dealer.");
  1491. return false;
  1492. }
  1493.  
  1494. // High Danger and hand not worth much or bad wait
  1495. if (tilePrio.score.riichi < (getCurrentDangerLevel() - (RIICHI * 1000)) * (1 + badWait)) {
  1496. log("Decline Riichi because of worthless hand and high danger.");
  1497. return false;
  1498. }
  1499.  
  1500. // Hand already has enough yaku and high value (Around 6000+ depending on the wait)
  1501. if (tilePrio.yaku.closed >= 1 && tilePrio.score.closed / (seatWind == 1 ? 1.5 : 1) > 4000 + (RIICHI * 1000) + (tilePrio.waits * 500)) {
  1502. log("Decline Riichi because of high value hand with enough yaku.");
  1503. return false;
  1504. }
  1505.  
  1506. // Hand already has high value and no yaku
  1507. if (tilePrio.yaku.closed < 0.9 && tilePrio.score.riichi > 5000 - (RIICHI * 1000)) {
  1508. log("Accept Riichi because of high value hand without yaku.");
  1509. return true;
  1510. }
  1511.  
  1512. // Number of Kans(Dora Indicators) -> more are higher chance for uradora
  1513. if (lotsOfDoraIndicators) {
  1514. log("Accept Riichi because of multiple dora indicators.");
  1515. return true;
  1516. }
  1517.  
  1518. // Don't Riichi when: Last round with bad waits & would lose place with -1000
  1519. if (isLastGame() && badWait && ((getDistanceToPlayer(1) >= -1000 && getDistanceToPlayer(1) <= 0) ||
  1520. (getDistanceToPlayer(2) >= -1000 && getDistanceToPlayer(2) <= 0) ||
  1521. (getNumberOfPlayers() > 3 && getDistanceToPlayer(3) >= -1000 && getDistanceToPlayer(3) <= 0))) {
  1522. log("Decline Riichi because distance to next player is < 1000 in last game.");
  1523. return false;
  1524. }
  1525.  
  1526. // Default: Just do it.
  1527. log("Accept Riichi by default.");
  1528. return true;
  1529. }
  1530.  
  1531. //Negative number: Distance to second
  1532. //Positive number: Distance to first
  1533. function getDistanceToFirst() {
  1534. if (getNumberOfPlayers() == 3) {
  1535. return Math.max(getPlayerScore(1), getPlayerScore(2)) - getPlayerScore(0);
  1536. }
  1537. return Math.max(getPlayerScore(1), getPlayerScore(2), getPlayerScore(3)) - getPlayerScore(0);
  1538. }
  1539.  
  1540. //Negative number: Distance to last
  1541. //Positive number: Distance to third
  1542. function getDistanceToLast() {
  1543. if (getNumberOfPlayers() == 3) {
  1544. return Math.min(getPlayerScore(1), getPlayerScore(2)) - getPlayerScore(0);
  1545. }
  1546. return Math.min(getPlayerScore(1), getPlayerScore(2), getPlayerScore(3)) - getPlayerScore(0);
  1547. }
  1548.  
  1549. //Positive: Other player is in front of you
  1550. function getDistanceToPlayer(player) {
  1551. if (getNumberOfPlayers() == 3 && player == 3) {
  1552. return 0;
  1553. }
  1554. return getPlayerScore(player) - getPlayerScore(0);
  1555. }
  1556.  
  1557. //Check if "All Last"
  1558. function isLastGame() {
  1559. if (isEastRound()) {
  1560. return getRound() == getNumberOfPlayers() || getRoundWind() > 1; //East 4(3) or South X
  1561. }
  1562. return (getRound() == getNumberOfPlayers() && getRoundWind() == 2) || getRoundWind() > 2; //South 4(3) or West X
  1563. }
  1564.  
  1565. //Check if Hand is complete
  1566. function isWinningHand(numberOfTriples, numberOfPairs) {
  1567. if (strategy == STRATEGIES.CHIITOITSU) {
  1568. return numberOfPairs == 7;
  1569. }
  1570. return numberOfTriples == 4 && numberOfPairs == 1;
  1571. }
  1572.  
  1573. //Return the number of tiles in the wall at the start of the round
  1574. function getWallSize() {
  1575. if (getNumberOfPlayers() == 3) {
  1576. return 55;
  1577. }
  1578. else {
  1579. return 70;
  1580. }
  1581. }
  1582.  
  1583. function getCallNameByType(type) {
  1584. switch (type) {
  1585. case 1: return "discard";
  1586. case 2: return "chi";
  1587. case 3: return "pon";
  1588. case 4: return "kan(ankan)";
  1589. case 5: return "kan(daiminkan)";
  1590. case 6: return "kan(shouminkan)";
  1591. case 7: return "riichi";
  1592. case 8: return "tsumo";
  1593. case 9: return "ron";
  1594. case 10: return "kyuushu kyuuhai";
  1595. case 11: return "kita";
  1596. default: return type;
  1597. }
  1598. }
  1599.  
  1600. function getTileEmoji(tileType, tileIdx, dora) {
  1601. if (dora) {
  1602. tileIdx = 0;
  1603. }
  1604. return tileEmojiList[tileType][tileIdx];
  1605. }
  1606.  
  1607. //Get Emoji str by tile name
  1608. function getTileEmojiByName(name) {
  1609. let tile = getTileFromString(name);
  1610. return getTileEmoji(tile.type, tile.index, tile.dora);
  1611. }
  1612.  
  1613. //################################
  1614. // LOGGING
  1615. // Contains logging functions
  1616. //################################
  1617.  
  1618. //Print string to HTML or console
  1619. function log(t) {
  1620. if (isDebug()) {
  1621. document.body.innerHTML += t + "<br>";
  1622. }
  1623. else {
  1624. console.log(t);
  1625. }
  1626. }
  1627.  
  1628. //Print all tiles in hand
  1629. function printHand(hand) {
  1630. var handString = getStringForTiles(hand);
  1631. log("Hand:" + handString);
  1632. }
  1633.  
  1634. //Get String for array of tiles
  1635. function getStringForTiles(tiles) {
  1636. var tilesString = "";
  1637. var oldType = "";
  1638. tiles.forEach(function (tile) {
  1639. if (getNameForType(tile.type) != oldType) {
  1640. tilesString += oldType;
  1641. oldType = getNameForType(tile.type);
  1642. }
  1643. if (tile.dora == 1) {
  1644. tilesString += "0";
  1645. }
  1646. else {
  1647. tilesString += tile.index;
  1648. }
  1649. });
  1650. tilesString += oldType;
  1651. return tilesString;
  1652. }
  1653.  
  1654. //Print tile name
  1655. function printTile(tile) {
  1656. log(getTileName(tile, false));
  1657. }
  1658.  
  1659. //Print given tile priorities
  1660. function printTilePriority(tiles) {
  1661. log("Overall: Value Open: <" + Number(tiles[0].score.open).toFixed(0) +
  1662. "> Closed Value: <" + Number(tiles[0].score.closed).toFixed(0) +
  1663. "> Riichi Value: <" + Number(tiles[0].score.riichi).toFixed(0) +
  1664. "> Shanten: <" + Number(tiles[0].shanten).toFixed(0) + ">");
  1665. for (var i = 0; i < tiles.length && i < LOG_AMOUNT; i++) {
  1666. log(getTileName(tiles[i].tile, false) +
  1667. ": Priority: <" + Number(tiles[i].priority).toFixed(3) +
  1668. "> Efficiency: <" + Number(tiles[i].efficiency).toFixed(3) +
  1669. "> Yaku Open: <" + Number(tiles[i].yaku.open).toFixed(3) +
  1670. "> Yaku Closed: <" + Number(tiles[i].yaku.closed).toFixed(3) +
  1671. "> Dora: <" + Number(tiles[i].dora).toFixed(3) +
  1672. "> Waits: <" + Number(tiles[i].waits).toFixed(3) +
  1673. "> Danger: <" + Number(tiles[i].danger).toFixed(2) + ">");
  1674. }
  1675. }
  1676.  
  1677. //Input string to get an array of tiles (e.g. "123m456p789s1z")
  1678. function getTilesFromString(inputString) {
  1679. var numbers = [];
  1680. var tiles = [];
  1681. for (let input of inputString) {
  1682. var type = 4;
  1683. switch (input) {
  1684. case "p":
  1685. type = 0;
  1686. break;
  1687. case "m":
  1688. type = 1;
  1689. break;
  1690. case "s":
  1691. type = 2;
  1692. break;
  1693. case "z":
  1694. type = 3;
  1695. break;
  1696. default:
  1697. numbers.push(input);
  1698. break;
  1699. }
  1700. if (type != "4") {
  1701. for (let number of numbers) {
  1702. if (parseInt(number) == 0) {
  1703. tiles.push({ index: 5, type: type, dora: true, doraValue: 1, valid: true });
  1704. }
  1705. else {
  1706. tiles.push({ index: parseInt(number), type: type, dora: false, doraValue: 0, valid: true });
  1707. }
  1708. }
  1709. numbers = [];
  1710. }
  1711. }
  1712. return tiles;
  1713. }
  1714.  
  1715. //Input string to get a tiles (e.g. "1m")
  1716. function getTileFromString(inputString) {
  1717. var type = 4;
  1718. var dr = false;
  1719. switch (inputString[1]) {
  1720. case "p":
  1721. type = 0;
  1722. break;
  1723. case "m":
  1724. type = 1;
  1725. break;
  1726. case "s":
  1727. type = 2;
  1728. break;
  1729. case "z":
  1730. type = 3;
  1731. break;
  1732. }
  1733. var index = inputString[0];
  1734. if (inputString[0] == "0") {
  1735. index = "5";
  1736. dr = true;
  1737. }
  1738. if (type != "4") {
  1739. var tile = { index: parseInt(index), type: type, dora: dr, valid: true };
  1740. tile.doraValue = getTileDoraValue(tile);
  1741. return tile;
  1742. }
  1743. return null;
  1744. }
  1745.  
  1746. //Returns the name for a tile
  1747. function getTileName(tile, useRaw = true) {
  1748. let name = "";
  1749. if (tile.dora == true) {
  1750. name = "0" + getNameForType(tile.type);
  1751. } else {
  1752. name = tile.index + getNameForType(tile.type);
  1753. }
  1754.  
  1755. if (!useRaw && USE_EMOJI) {
  1756. return `${getTileEmoji(tile.type, tile.index, tile.dora)}: ${name}`;
  1757. } else {
  1758. return name;
  1759. }
  1760. }
  1761.  
  1762. //Returns the corresponding char for a type
  1763. function getNameForType(type) {
  1764. switch (type) {
  1765. case 0:
  1766. return "p";
  1767. case 1:
  1768. return "m";
  1769. case 2:
  1770. return "s";
  1771. case 3:
  1772. return "z";
  1773. default:
  1774. return "?";
  1775. }
  1776. }
  1777.  
  1778. //returns a string for the current state of the game
  1779. function getDebugString() {
  1780. var debugString = "";
  1781. debugString += getStringForTiles(dora) + "|";
  1782. debugString += getStringForTiles(ownHand) + "|";
  1783. debugString += getStringForTiles(calls[0]) + "|";
  1784. debugString += getStringForTiles(calls[1]) + "|";
  1785. debugString += getStringForTiles(calls[2]) + "|";
  1786. if (getNumberOfPlayers() == 4) {
  1787. debugString += getStringForTiles(calls[3]) + "|";
  1788. }
  1789. debugString += getStringForTiles(discards[0]) + "|";
  1790. debugString += getStringForTiles(discards[1]) + "|";
  1791. debugString += getStringForTiles(discards[2]) + "|";
  1792. if (getNumberOfPlayers() == 4) {
  1793. debugString += getStringForTiles(discards[3]) + "|";
  1794. }
  1795. if (getNumberOfPlayers() == 4) {
  1796. debugString += (isPlayerRiichi(0) * 1) + "," + (isPlayerRiichi(1) * 1) + "," + (isPlayerRiichi(2) * 1) + "," + (isPlayerRiichi(3) * 1) + "|";
  1797. }
  1798. else {
  1799. debugString += (isPlayerRiichi(0) * 1) + "," + (isPlayerRiichi(1) * 1) + "," + (isPlayerRiichi(2) * 1) + "|";
  1800. }
  1801. debugString += seatWind + "|";
  1802. debugString += roundWind + "|";
  1803. debugString += tilesLeft;
  1804. return debugString;
  1805. }
  1806.  
  1807.  
  1808. //################################
  1809. // YAKU
  1810. // Contains the yaku calculations
  1811. //################################
  1812.  
  1813. //Returns the closed and open yaku value of the hand
  1814. function getYaku(inputHand, inputCalls, triplesAndPairs = null) {
  1815.  
  1816. //Remove 4th tile from Kans, which could lead to false yaku calculation
  1817. inputCalls = inputCalls.filter(tile => !tile.kan);
  1818.  
  1819. var hand = inputHand.concat(inputCalls); //Add calls to hand
  1820.  
  1821. var yakuOpen = 0;
  1822. var yakuClosed = 0;
  1823.  
  1824.  
  1825. // ### 1 Han ###
  1826.  
  1827. if (triplesAndPairs == null) { //Can be set as a parameter to save calculation time if already precomputed
  1828. triplesAndPairs = getTriplesAndPairs(hand);
  1829. }
  1830. else {
  1831. triplesAndPairs.triples = triplesAndPairs.triples.concat(inputCalls);
  1832. }
  1833. var triplets = getTripletsAsArray(hand);
  1834. var sequences = getBestSequenceCombination(removeTilesFromTileArray(inputHand, triplets.concat(triplesAndPairs.pairs))).concat(getBestSequenceCombination(inputCalls));
  1835.  
  1836. //Pinfu is applied in ai_offense when fu is 30, same with Riichi.
  1837. //There's no certain way to check for it here, so ignore it
  1838.  
  1839. //Yakuhai
  1840. //Wind/Dragon Triples
  1841. //Open
  1842. if (strategy != STRATEGIES.CHIITOITSU) {
  1843. var yakuhai = getYakuhai(triplesAndPairs.triples);
  1844. yakuOpen += yakuhai.open;
  1845. yakuClosed += yakuhai.closed;
  1846. }
  1847.  
  1848. //Tanyao
  1849. //Open
  1850. var tanyao = getTanyao(hand, triplesAndPairs, inputCalls);
  1851. yakuOpen += tanyao.open;
  1852. yakuClosed += tanyao.closed;
  1853.  
  1854. //Iipeikou (Identical Sequences in same type)
  1855. //Closed
  1856. if (strategy != STRATEGIES.CHIITOITSU) {
  1857. var iipeikou = getIipeikou(sequences);
  1858. yakuOpen += iipeikou.open;
  1859. yakuClosed += iipeikou.closed;
  1860.  
  1861. // ### 2 Han ###
  1862.  
  1863. //Chiitoitsu
  1864. //7 Pairs
  1865. //Closed
  1866. // -> Not necessary, because own strategy
  1867.  
  1868. //Sanankou
  1869. //3 concealed triplets
  1870. //Open*
  1871. var sanankou = getSanankou(inputHand);
  1872. yakuOpen += sanankou.open;
  1873. yakuClosed += sanankou.closed;
  1874.  
  1875. //Sankantsu
  1876. //3 Kans
  1877. //Open
  1878. //-> TODO: Should not influence score, but Kan calling.
  1879.  
  1880. //Toitoi
  1881. //All Triplets
  1882. //Open
  1883. var toitoi = getToitoi(triplets);
  1884. yakuOpen += toitoi.open;
  1885. yakuClosed += toitoi.closed;
  1886.  
  1887. //Sanshoku Doukou
  1888. //3 same index triplets in all 3 types
  1889. //Open
  1890. var sanshokuDouko = getSanshokuDouko(triplets);
  1891. yakuOpen += sanshokuDouko.open;
  1892. yakuClosed += sanshokuDouko.closed;
  1893.  
  1894. //Sanshoku Doujun
  1895. //3 same index straights in all types
  1896. //Open/-1 Han after call
  1897. var sanshoku = getSanshokuDoujun(sequences);
  1898. yakuOpen += sanshoku.open;
  1899. yakuClosed += sanshoku.closed;
  1900.  
  1901. //Shousangen
  1902. //Little 3 Dragons (2 Triplets + Pair)
  1903. //Open
  1904. var shousangen = getShousangen(hand);
  1905. yakuOpen += shousangen.open;
  1906. yakuClosed += shousangen.closed;
  1907. }
  1908.  
  1909. //Chanta
  1910. //Half outside Hand (including terminals)
  1911. //Open/-1 Han after call
  1912. var chanta = getChanta(triplets, sequences, triplesAndPairs.pairs);
  1913. yakuOpen += chanta.open;
  1914. yakuClosed += chanta.closed;
  1915.  
  1916. //Honrou
  1917. //All Terminals and Honors (means: Also 4 triplets)
  1918. //Open
  1919. var honrou = getHonrou(triplets);
  1920. yakuOpen += honrou.open;
  1921. yakuClosed += honrou.closed;
  1922.  
  1923. //Ittsuu
  1924. //Pure Straight
  1925. //Open/-1 Han after call
  1926. var ittsuu = getIttsuu(sequences);
  1927. yakuOpen += ittsuu.open;
  1928. yakuClosed += ittsuu.closed;
  1929.  
  1930. //3 Han
  1931.  
  1932. //Ryanpeikou
  1933. //2 times identical sequences (2 Iipeikou)
  1934. //Closed
  1935.  
  1936. //Junchan
  1937. //All Terminals
  1938. //Open/-1 Han after call
  1939. var junchan = getJunchan(triplets, sequences, triplesAndPairs.pairs);
  1940. yakuOpen += junchan.open;
  1941. yakuClosed += junchan.closed;
  1942.  
  1943. //Honitsu
  1944. //Half Flush
  1945. //Open/-1 Han after call
  1946. var honitsu = getHonitsu(hand);
  1947. yakuOpen += honitsu.open;
  1948. yakuClosed += honitsu.closed;
  1949.  
  1950. //6 Han
  1951.  
  1952. //Chinitsu
  1953. //Full Flush
  1954. //Open/-1 Han after call
  1955. var chinitsu = getChinitsu(hand);
  1956. yakuOpen += chinitsu.open;
  1957. yakuClosed += chinitsu.closed;
  1958.  
  1959. //Yakuman
  1960.  
  1961. //Daisangen
  1962. //Big Three Dragons
  1963. //Open
  1964. var daisangen = getDaisangen(hand);
  1965. yakuOpen += daisangen.open;
  1966. yakuClosed += daisangen.closed;
  1967.  
  1968. //Suuankou
  1969. //4 Concealed Triplets
  1970. //Closed
  1971.  
  1972. //Tsuuiisou
  1973. //All Honours
  1974. //Open
  1975.  
  1976. //Ryuuiisou
  1977. //All Green
  1978. //Open
  1979.  
  1980. //Chinroutou
  1981. //All Terminals
  1982. //Open
  1983.  
  1984. //Suushiihou
  1985. //Four Little Winds
  1986. //Open
  1987.  
  1988. //Suukantsu
  1989. //4 Kans
  1990. //Open
  1991.  
  1992. //Chuuren poutou
  1993. //9 Gates
  1994. //Closed
  1995.  
  1996. //Kokushi musou
  1997. //Thirteen Orphans
  1998. //Closed
  1999.  
  2000. //Double Yakuman
  2001.  
  2002. //Suuankou tanki
  2003. //4 Concealed Triplets Single Wait
  2004. //Closed
  2005.  
  2006. //Kokushi musou juusan menmachi
  2007. //13 Wait Thirteen Orphans
  2008. //Closed
  2009.  
  2010. //Junsei chuuren poutou
  2011. //True Nine Gates
  2012. //Closed
  2013.  
  2014. //Daisuushii
  2015. //Four Big Winds
  2016. //Open
  2017.  
  2018.  
  2019. return { open: yakuOpen, closed: yakuClosed };
  2020. }
  2021.  
  2022. //Yakuhai
  2023. function getYakuhai(triples) {
  2024. var yakuhai = 0;
  2025. yakuhai = parseInt(triples.filter(tile => tile.type == 3 && (tile.index > 4 || tile.index == seatWind || tile.index == roundWind)).length / 3);
  2026. yakuhai += parseInt(triples.filter(tile => tile.type == 3 && tile.index == seatWind && tile.index == roundWind).length / 3);
  2027. return { open: yakuhai, closed: yakuhai };
  2028. }
  2029.  
  2030. //Tanyao
  2031. function getTanyao(hand, triplesAndPairs, inputCalls) {
  2032. if (hand.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length <= hand.length - 14 &&
  2033. inputCalls.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0 &&
  2034. triplesAndPairs.pairs.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0 &&
  2035. triplesAndPairs.triples.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0) {
  2036. return { open: 1, closed: 1 };
  2037. }
  2038. return { open: 0, closed: 0 };
  2039. }
  2040.  
  2041. //Iipeikou
  2042. function getIipeikou(triples) {
  2043. for (let triple of triples) {
  2044. var tiles1 = getNumberOfTilesInTileArray(triples, triple.index, triple.type);
  2045. var tiles2 = getNumberOfTilesInTileArray(triples, triple.index + 1, triple.type);
  2046. var tiles3 = getNumberOfTilesInTileArray(triples, triple.index + 2, triple.type);
  2047. if (tiles1 == 2 && tiles2 == 2 && tiles3 == 2) {
  2048. return { open: 0, closed: 1 };
  2049. }
  2050. }
  2051. return { open: 0, closed: 0 };
  2052. }
  2053.  
  2054. //Sanankou
  2055. function getSanankou(hand) {
  2056. if (!isConsideringCall) {
  2057. var concealedTriples = getTripletsAsArray(hand);
  2058. if (parseInt(concealedTriples.length / 3) >= 3) {
  2059. return { open: 2, closed: 2 };
  2060. }
  2061. }
  2062.  
  2063. return { open: 0, closed: 0 };
  2064. }
  2065.  
  2066. //Toitoi
  2067. function getToitoi(triplets) {
  2068. if (parseInt(triplets.length / 3) >= 4) {
  2069. return { open: 2, closed: 2 };
  2070. }
  2071.  
  2072. return { open: 0, closed: 0 };
  2073. }
  2074.  
  2075. //Sanshoku Douko
  2076. function getSanshokuDouko(triplets) {
  2077. for (var i = 1; i <= 9; i++) {
  2078. if (triplets.filter(tile => tile.index == i && tile.type < 3).length >= 9) {
  2079. return { open: 2, closed: 2 };
  2080. }
  2081. }
  2082. return { open: 0, closed: 0 };
  2083. }
  2084.  
  2085. //Sanshoku Doujun
  2086. function getSanshokuDoujun(sequences) {
  2087. for (var i = 1; i <= 7; i++) {
  2088. var seq = sequences.filter(tile => tile.index == i || tile.index == i + 1 || tile.index == i + 2);
  2089. if (seq.length >= 9 && seq.filter(tile => tile.type == 0).length >= 3 &&
  2090. seq.filter(tile => tile.type == 1).length >= 3 && seq.filter(tile => tile.type == 2).length >= 3) {
  2091. return { open: 1, closed: 2 };
  2092. }
  2093. }
  2094. return { open: 0, closed: 0 };
  2095. }
  2096.  
  2097. //Shousangen
  2098. function getShousangen(hand) {
  2099. if (hand.filter(tile => tile.type == 3 && tile.index >= 5).length == 8 &&
  2100. hand.filter(tile => tile.type == 3 && tile.index == 5).length < 4 &&
  2101. hand.filter(tile => tile.type == 3 && tile.index == 6).length < 4 &&
  2102. hand.filter(tile => tile.type == 3 && tile.index == 7).length < 4) {
  2103. return { open: 2, closed: 2 };
  2104. }
  2105. return { open: 0, closed: 0 };
  2106. }
  2107.  
  2108. //Daisangen
  2109. function getDaisangen(hand) {
  2110. if (hand.filter(tile => tile.type == 3 && tile.index == 5).length >= 3 &&
  2111. hand.filter(tile => tile.type == 3 && tile.index == 6).length >= 3 &&
  2112. hand.filter(tile => tile.type == 3 && tile.index == 7).length >= 3) {
  2113. return { open: 10, closed: 10 }; //Yakuman -> 10?
  2114. }
  2115. return { open: 0, closed: 0 };
  2116. }
  2117.  
  2118. //Chanta
  2119. function getChanta(triplets, sequences, pairs) {
  2120. if ((triplets.concat(pairs)).filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length +
  2121. (sequences.filter(tile => tile.index == 1 || tile.index == 9).length * 3) >= 13) {
  2122. return { open: 1, closed: 2 };
  2123. }
  2124. return { open: 0, closed: 0 };
  2125. }
  2126.  
  2127. //Honrou
  2128. function getHonrou(triplets) {
  2129. if (triplets.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length >= 13) {
  2130. return { open: 3, closed: 2 }; // - Added to Chanta
  2131. }
  2132. return { open: 0, closed: 0 };
  2133. }
  2134.  
  2135. //Junchan
  2136. function getJunchan(triplets, sequences, pairs) {
  2137. if ((triplets.concat(pairs)).filter(tile => tile.type != 3 && (tile.index == 1 || tile.index == 9)).length +
  2138. (sequences.filter(tile => tile.index == 1 || tile.index == 9).length * 3) >= 13) {
  2139. return { open: 1, closed: 1 }; // - Added to Chanta
  2140. }
  2141. return { open: 0, closed: 0 };
  2142. }
  2143.  
  2144. //Ittsuu
  2145. function getIttsuu(triples) {
  2146. for (var j = 0; j <= 2; j++) {
  2147. for (var i = 1; i <= 9; i++) {
  2148. if (!triples.some(tile => tile.type == j && tile.index == i)) {
  2149. break;
  2150. }
  2151. if (i == 9) {
  2152. return { open: 1, closed: 2 };
  2153. }
  2154. }
  2155. }
  2156. return { open: 0, closed: 0 };
  2157. }
  2158.  
  2159. //Honitsu
  2160. function getHonitsu(hand) {
  2161. var pinzu = hand.filter(tile => tile.type == 3 || tile.type == 0).length;
  2162. var manzu = hand.filter(tile => tile.type == 3 || tile.type == 1).length;
  2163. var souzu = hand.filter(tile => tile.type == 3 || tile.type == 2).length;
  2164. if (pinzu >= 14 || pinzu >= hand.length ||
  2165. manzu >= 14 || manzu >= hand.length ||
  2166. souzu >= 14 || souzu >= hand.length) {
  2167. return { open: 2, closed: 3 };
  2168. }
  2169. return { open: 0, closed: 0 };
  2170. }
  2171.  
  2172. //Chinitsu
  2173. function getChinitsu(hand) {
  2174. var pinzu = hand.filter(tile => tile.type == 0).length;
  2175. var manzu = hand.filter(tile => tile.type == 1).length;
  2176. var souzu = hand.filter(tile => tile.type == 2).length;
  2177. if (pinzu >= 14 || pinzu >= hand.length ||
  2178. manzu >= 14 || manzu >= hand.length ||
  2179. souzu >= 14 || souzu >= hand.length) {
  2180. return { open: 3, closed: 3 }; //Score gets added to honitsu -> 5/6 han
  2181. }
  2182. return { open: 0, closed: 0 };
  2183. }
  2184.  
  2185. //################################
  2186. // AI OFFENSE
  2187. // Offensive part of the AI
  2188. //################################
  2189.  
  2190. //Look at Hand etc. and decide for a strategy.
  2191. function determineStrategy() {
  2192.  
  2193. if (strategy != STRATEGIES.FOLD) {
  2194. var handTriples = parseInt(getTriples(getHandWithCalls(ownHand)).length / 3);
  2195. var pairs = getPairsAsArray(ownHand).length / 2;
  2196.  
  2197. if ((pairs == 6 || (pairs >= CHIITOITSU && handTriples < 2)) && isClosed) {
  2198. strategy = STRATEGIES.CHIITOITSU;
  2199. strategyAllowsCalls = false;
  2200. }
  2201. else if (canDoThirteenOrphans()) {
  2202. strategy = STRATEGIES.THIRTEEN_ORPHANS;
  2203. strategyAllowsCalls = false;
  2204. }
  2205. else {
  2206. if (strategy == STRATEGIES.THIRTEEN_ORPHANS ||
  2207. strategy == STRATEGIES.CHIITOITSU) {
  2208. strategyAllowsCalls = true; //Don't reset this value when bot is playing defensively without a full fold
  2209. }
  2210. strategy = STRATEGIES.GENERAL;
  2211. }
  2212. }
  2213. log("Strategy: " + strategy);
  2214. }
  2215.  
  2216. //Call a Chi/Pon
  2217. //combination example: Array ["6s|7s", "7s|9s"]
  2218. async function callTriple(combinations, operation) {
  2219.  
  2220. log("Consider call on " + getTileName(getTileForCall()));
  2221.  
  2222. var handValue = getHandValues(ownHand);
  2223.  
  2224. if (!strategyAllowsCalls && (tilesLeft > 4 || handValue.shanten > 1)) { //No Calls allowed
  2225. log("Strategy allows no calls! Declined!");
  2226. declineCall(operation);
  2227. return false;
  2228. }
  2229.  
  2230. //Find best Combination
  2231. var comb = -1;
  2232. var bestCombShanten = 9;
  2233. var bestDora = 0;
  2234.  
  2235. for (var i = 0; i < combinations.length; i++) {
  2236. var callTiles = combinations[i].split("|");
  2237. callTiles = callTiles.map(t => getTileFromString(t));
  2238.  
  2239. var newHand = removeTilesFromTileArray(ownHand, callTiles);
  2240. var newHandTriples = getTriplesAndPairs(newHand);
  2241. var doubles = getDoubles(removeTilesFromTileArray(newHand, newHandTriples.triples.concat(newHandTriples.pairs)));
  2242. var shanten = calculateShanten(parseInt(newHandTriples.triples.length / 3), parseInt(newHandTriples.pairs.length / 2), parseInt(doubles.length / 2));
  2243.  
  2244. if (shanten < bestCombShanten || (shanten == bestCombShanten && getNumberOfDoras(callTiles) > bestDora)) {
  2245. comb = i;
  2246. bestDora = getNumberOfDoras(callTiles);
  2247. bestCombShanten = shanten;
  2248. }
  2249. }
  2250.  
  2251. log("Best Combination: " + combinations[comb]);
  2252.  
  2253. var callTiles = combinations[comb].split("|");
  2254. callTiles = callTiles.map(t => getTileFromString(t));
  2255.  
  2256. var wasClosed = isClosed;
  2257. calls[0].push(callTiles[0]); //Simulate "Call" for hand value calculation
  2258. calls[0].push(callTiles[1]);
  2259. calls[0].push(getTileForCall());
  2260. isClosed = false;
  2261. newHand = removeTilesFromTileArray(ownHand, callTiles); //Remove called tiles from hand
  2262. var tilePrios = await getTilePriorities(newHand);
  2263. tilePrios = sortOutUnsafeTiles(tilePrios);
  2264. var nextDiscard = getDiscardTile(tilePrios); //Calculate next discard
  2265. newHand = removeTilesFromTileArray(newHand, [nextDiscard]); //Remove discard from hand
  2266. var newHandValue = getHandValues(newHand, nextDiscard); //Get Value of that hand
  2267. newHandTriples = getTriplesAndPairs(newHand); //Get Triples, to see if discard would make the hand worse
  2268. calls[0].pop();
  2269. calls[0].pop();
  2270. calls[0].pop();
  2271. isClosed = wasClosed;
  2272.  
  2273. var newHonorPairs = newHandTriples.pairs.filter(t => t.type == 3).length / 2;
  2274. var newPairs = newHandTriples.pairs.length / 2;
  2275.  
  2276. if (isSameTile(nextDiscard, getTileForCall()) ||
  2277. (callTiles[0].index == getTileForCall().index - 2 && isSameTile(nextDiscard, { index: callTiles[0].index - 1, type: callTiles[0].type })) ||
  2278. (callTiles[1].index == getTileForCall().index + 2 && isSameTile(nextDiscard, { index: callTiles[1].index + 1, type: callTiles[1].type }))) {
  2279. declineCall(operation);
  2280. log("Next discard would be the same tile. Call declined!");
  2281. return false;
  2282. }
  2283.  
  2284. if (strategy == STRATEGIES.FOLD || tilePrios.filter(t => t.safe).length == 0) {
  2285. log("Would fold next discard! Declined!");
  2286. declineCall(operation);
  2287. return false;
  2288. }
  2289.  
  2290. if (tilesLeft <= 4 && handValue.shanten == 1 && newHandValue.shanten == 0) { //Call to get tenpai at end of game
  2291. log("Accept call to be tenpai at end of game!");
  2292. makeCallWithOption(operation, comb);
  2293. return true;
  2294. }
  2295.  
  2296. if (newHandValue.yaku.open < 0.15 && //Yaku chance is too bad
  2297. newHandTriples.pairs.filter(t => isValueTile(t) && getNumberOfTilesAvailable(t.index, t.type) >= 2).length < 2) { //And no value honor pair
  2298. log("Not enough Yaku! Declined! " + newHandValue.yaku.open + " < 0.15");
  2299. declineCall(operation);
  2300. return false;
  2301. }
  2302.  
  2303. if (handValue.waits > 0 && newHandValue.waits < handValue.waits + 1) { //Call results in worse waits
  2304. log("Call would result in less waits! Declined!");
  2305. declineCall(operation);
  2306. return false;
  2307. }
  2308.  
  2309. if (isClosed && newHandValue.score.open < 1500 - (CALL_PON_CHI * 200) && newHandValue.shanten >= 2 + CALL_PON_CHI && seatWind != 1 &&// Hand is worthless and slow and not dealer. Should prevent cheap yakuhai or tanyao calls
  2310. !(newHonorPairs >= 1 && newPairs >= 2)) {
  2311. log("Hand is cheap and slow! Declined!");
  2312. declineCall(operation);
  2313. return false;
  2314. }
  2315.  
  2316. if (seatWind == 1) { //Remove dealer bonus for the following checks
  2317. handValue.score.closed /= 1.5;
  2318. handValue.score.open /= 1.5;
  2319. newHandValue.score.open /= 1.5;
  2320. }
  2321.  
  2322. if (newHandValue.shanten > handValue.shanten) { //Call would make shanten worse
  2323. log("Call would increase shanten! Declined!");
  2324. declineCall(operation);
  2325. return false;
  2326. }
  2327. else if (newHandValue.shanten == handValue.shanten) { //When it does not improve shanten
  2328. if (!isClosed && newHandValue.priority > handValue.priority * 1.5) { //When the call improves the hand
  2329. log("Call accepted because hand is already open and it improves the hand!");
  2330. }
  2331. else {
  2332. declineCall(operation);
  2333. log("Call declined because it does not benefit the hand!");
  2334. return false;
  2335. }
  2336. }
  2337. else { //When it improves shanten
  2338. var isBadWait = (callTiles[0].index == callTiles[1].index || Math.abs(callTiles[0].index - callTiles[1].index) == 2 || // Pon or Kanchan
  2339. callTiles[0].index >= 8 && callTiles[1].index >= 8 || callTiles[0].index <= 2 && callTiles[1].index <= 2); //Penchan
  2340.  
  2341. if (handValue.shanten >= 5 - CALL_PON_CHI && seatWind == 1) { //Very slow hand & dealer? -> Go for a fast win
  2342. log("Call accepted because of slow hand and dealer position!");
  2343. }
  2344. else if (!isClosed && newHandValue.score.open > handValue.score.open * 0.9) { //Hand is already open and it reduces shanten while not much value is lost
  2345. log("Call accepted because hand is already open!");
  2346. }
  2347. else if (newHandValue.score.open >= 4500 - (CALL_PON_CHI * 500) &&
  2348. newHandValue.score.open > handValue.score.closed * 0.7) { //High value hand? -> Go for a fast win
  2349. log("Call accepted because of high value hand!");
  2350. }
  2351. else if (newHandValue.score.open >= handValue.score.closed * 1.75 && //Call gives additional value to hand
  2352. ((newHandValue.score.open >= (2000 - (CALL_PON_CHI * 200) - ((3 - newHandValue.shanten) * 200))) || //And either hand is not extremely cheap...
  2353. newHonorPairs >= 1)) { //Or there are some honor pairs in hand (=can be called easily or act as safe discards)
  2354. log("Call accepted because it boosts the value of the hand!");
  2355. }
  2356. else if (newHandValue.score.open > handValue.score.open * 0.9 && //Call loses not much value
  2357. newHandValue.score.open > handValue.score.closed * 0.7 &&
  2358. ((isBadWait && (newHandValue.score.open >= (1000 - (CALL_PON_CHI * 100) - ((3 - newHandValue.shanten) * 100)))) || // And it's a bad wait while the hand is not extremely cheap
  2359. (!isBadWait && (newHandValue.score.open >= (2000 - (CALL_PON_CHI * 200) - ((3 - newHandValue.shanten) * 200)))) || //Or it was a good wait and the hand is at least a bit valuable
  2360. newHonorPairs >= 2) && //Or multiple honor pairs
  2361. ((newHandTriples.pairs.filter(t => isValueTile(t) && getNumberOfTilesAvailable(t.index, t.type) >= 1)).length >= 2 && (newPairs >= 2 || newHandValue.shanten > 1))) {//And would open hand anyway with honor call
  2362. log("Call accepted because it reduces shanten!");
  2363. }
  2364. else if (newHandValue.shanten == 0 && newHandValue.score.open > handValue.score.closed * 0.9 &&
  2365. newHandValue.waits > 2 && isBadWait) {// Make hand ready and eliminate a bad wait
  2366. log("Call accepted because it eliminates a bad wait and makes the hand ready!");
  2367. }
  2368. else if ((0.5 - (tilesLeft / getWallSize())) +
  2369. (0.25 - (newHandValue.shanten / 4)) +
  2370. (newHandValue.shanten > 0 ? ((newPairs - newHandValue.shanten - 0.5) / 2) : 0) +
  2371. ((newHandValue.score.open / 3000) - 0.5) +
  2372. (((newHandValue.score.open / handValue.score.closed) * 0.75) - 0.75) +
  2373. ((isBadWait / 2) - 0.25) >=
  2374. 1 - (CALL_PON_CHI / 2)) { //The call is good in multiple aspects
  2375. log("Call accepted because it's good in multiple aspects");
  2376. }
  2377. else { //Decline
  2378. declineCall(operation);
  2379. log("Call declined because it does not benefit the hand!");
  2380. return false;
  2381. }
  2382. }
  2383.  
  2384. makeCallWithOption(operation, comb);
  2385. return true;
  2386. }
  2387.  
  2388. //Call Tile for Kan
  2389. function callDaiminkan() {
  2390. if (!isClosed) {
  2391. callKan(getOperations().ming_gang, getTileForCall());
  2392. }
  2393. else { //Always decline with closed hand
  2394. declineCall(getOperations().ming_gang);
  2395. }
  2396. }
  2397.  
  2398. //Add from Hand to existing Pon
  2399. function callShouminkan() {
  2400. callKan(getOperations().add_gang, getTileForCall());
  2401. }
  2402.  
  2403. //Closed Kan
  2404. function callAnkan(combination) {
  2405. callKan(getOperations().an_gang, getTileFromString(combination[0]));
  2406. }
  2407.  
  2408. //Needs a semi good hand to call Kans and other players are not dangerous
  2409. function callKan(operation, tileForCall) {
  2410. log("Consider Kan.");
  2411. var tiles = getHandValues(getHandWithCalls(ownHand));
  2412.  
  2413. var newTiles = getHandValues(getHandWithCalls(removeTilesFromTileArray(ownHand, [tileForCall]))); //Check if efficiency goes down without additional tile
  2414.  
  2415. if (isPlayerRiichi(0) ||
  2416. (strategyAllowsCalls &&
  2417. tiles.shanten <= (tilesLeft / (getWallSize() / 2)) + CALL_KAN &&
  2418. getCurrentDangerLevel() < 1000 + (CALL_KAN * 500) &&
  2419. tiles.shanten >= newTiles.shanten &&
  2420. tiles.efficiency * 0.9 <= newTiles.efficiency)) {
  2421. makeCall(operation);
  2422. log("Kan accepted!");
  2423. }
  2424. else {
  2425. if (operation == getOperations().ming_gang) { // Decline call for closed/added Kans is not working, just skip it and discard normally
  2426. declineCall(operation);
  2427. }
  2428. log("Kan declined!");
  2429. }
  2430. }
  2431.  
  2432. function callRon() {
  2433. makeCall(getOperations().rong);
  2434. }
  2435.  
  2436. function callTsumo() {
  2437. makeCall(getOperations().zimo);
  2438. }
  2439.  
  2440. function callKita() { // 3 player only
  2441. if (strategy != STRATEGIES.THIRTEEN_ORPHANS && strategy != STRATEGIES.FOLD) {
  2442. if (getNumberOfTilesInTileArray(ownHand, 4, 3) > 1) { //More than one north tile: Check if it's okay to call kita
  2443. var handValue = getHandValues(ownHand);
  2444. var newHandValue = getHandValues(removeTilesFromTileArray(ownHand, [{ index: 4, type: 3, dora: false }]));
  2445. if (handValue.shanten <= 1 && newHandValue.shanten > handValue.shanten) {
  2446. return false;
  2447. }
  2448. }
  2449. sendKitaCall();
  2450. return true;
  2451. }
  2452. return false;
  2453. }
  2454.  
  2455. function callAbortiveDraw() { // Kyuushu Kyuuhai, 9 Honors or Terminals in starting Hand
  2456. if (canDoThirteenOrphans()) {
  2457. return;
  2458. }
  2459. var handValue = getHandValues(ownHand);
  2460. if (handValue.shanten >= 4) { //Hand is bad -> abort game
  2461. sendAbortiveDrawCall();
  2462. }
  2463. }
  2464.  
  2465. function callRiichi(tiles) {
  2466. var operations = getOperationList();
  2467. var combination = [];
  2468. for (let op of operations) {
  2469. if (op.type == getOperations().liqi) { //Get possible tiles for discard in riichi
  2470. combination = op.combination;
  2471. }
  2472. }
  2473. log(JSON.stringify(combination));
  2474. for (let tile of tiles) {
  2475. for (let comb of combination) {
  2476. if (comb.charAt(0) == "0") { //Fix for Dora Tiles
  2477. combination.push("5" + comb.charAt(1));
  2478. }
  2479. if (getTileName(tile.tile) == comb) {
  2480. if (shouldRiichi(tile)) {
  2481. var moqie = false;
  2482. if (getTileName(tile.tile) == getTileName(ownHand[ownHand.length - 1])) { //Is last tile?
  2483. moqie = true;
  2484. }
  2485. log("Discard: " + getTileName(tile.tile, false));
  2486. sendRiichiCall(comb, moqie);
  2487. return true;
  2488. }
  2489. else {
  2490. return false;
  2491. }
  2492. }
  2493. }
  2494. }
  2495. log("Riichi declined because Combination not found!");
  2496. return false;
  2497. }
  2498.  
  2499. //Discard the safest tile, but consider slightly riskier tiles with same shanten
  2500. function discardFold(tiles) {
  2501. if (strategy != STRATEGIES.FOLD) { //Not in full Fold mode yet: Discard a relatively safe tile with high priority
  2502. for (let tile of tiles) {
  2503. var foldThreshold = getFoldThreshold(tile, ownHand);
  2504. if (tile.shanten == Math.min(...tiles.map(t => t.shanten)) && //If next tile same shanten as the best tile
  2505. tile.danger < Math.min(...tiles.map(t => t.danger)) * 1.1 && //And the tile is not much more dangerous than the safest tile
  2506. tile.danger <= foldThreshold * 2) {
  2507. log("Tile Priorities: ");
  2508. printTilePriority(tiles);
  2509. discardTile(tile.tile);
  2510. return tile.tile;
  2511. }
  2512. }
  2513. // No safe tile with good shanten found: Full Fold.
  2514. log("Hand is very dangerous, full fold.");
  2515. strategyAllowsCalls = false;
  2516. }
  2517.  
  2518. tiles.sort(function (p1, p2) {
  2519. return p1.danger - p2.danger;
  2520. });
  2521. log("Fold Tile Priorities: ");
  2522. printTilePriority(tiles);
  2523.  
  2524. discardTile(tiles[0].tile);
  2525. return tiles[0].tile;
  2526. }
  2527.  
  2528. //Remove the given Tile from Hand
  2529. function discardTile(tile) {
  2530. if (!tile.valid) {
  2531. return;
  2532. }
  2533. log("Discard: " + getTileName(tile, false));
  2534. for (var i = 0; i < ownHand.length; i++) {
  2535. if (isSameTile(ownHand[i], tile, true)) {
  2536. discards[0].push(ownHand[i]);
  2537. if (!isDebug()) {
  2538. callDiscard(i);
  2539. }
  2540. else {
  2541. ownHand.splice(i, 1);
  2542. }
  2543. break;
  2544. }
  2545. }
  2546. }
  2547.  
  2548. //Simulates discarding every tile and calculates hand value.
  2549. //Asynchronous to give the browser time to "breath"
  2550. async function getTilePriorities(inputHand) {
  2551.  
  2552. if (isDebug()) {
  2553. log("Dora: " + getTileName(dora[0], false));
  2554. printHand(inputHand);
  2555. }
  2556.  
  2557. var tiles = [];
  2558. if (strategy == STRATEGIES.CHIITOITSU) {
  2559. tiles = chiitoitsuPriorities();
  2560. }
  2561. else if (strategy == STRATEGIES.THIRTEEN_ORPHANS) {
  2562. tiles = thirteenOrphansPriorities();
  2563. }
  2564. else {
  2565. for (var i = 0; i < inputHand.length; i++) { //Create 13 Tile hands
  2566.  
  2567. var hand = [...inputHand];
  2568. hand.splice(i, 1);
  2569.  
  2570. if (tiles.filter(t => isSameTile(t.tile, inputHand[i], true)).length > 0) { //Skip same tiles in hand
  2571. continue;
  2572. }
  2573.  
  2574. tiles.push(getHandValues(hand, inputHand[i]));
  2575.  
  2576. await new Promise(r => setTimeout(r, 10)); //Sleep a short amount of time to not completely block the browser
  2577. }
  2578. }
  2579.  
  2580. tiles.sort(function (p1, p2) {
  2581. return p2.priority - p1.priority;
  2582. });
  2583. return Promise.resolve(tiles);
  2584. }
  2585.  
  2586. /*
  2587. Calculates Values for all tiles in the hand.
  2588. As the Core of the AI this function is really complex. The simple explanation:
  2589. It simulates the next two turns, calculates all the important stuff (shanten, dora, yaku, waits etc.) and produces a priority for each tile based on the expected value/shanten in two turns.
  2590.  
  2591. In reality it would take far too much time to calculate all the possibilites (availableTiles * (availableTiles - 1) * 2 which can be up to 30000 possibilities).
  2592. Therefore most of the complexity comes from tricks to reduce the runtime:
  2593. At first all the tiles are computed that could improve the hand in the next two turns (which is usually less than 1000).
  2594. Duplicates (for example 3m -> 4m and 4m -> 3m) are marked and will only be computed once, but with twice the value.
  2595. The rest is some math to produce the same result which would result in actually simulating everything (like adding the original value of the hand for all the useless combinations).
  2596. */
  2597. function getHandValues(hand, discardedTile) {
  2598. var shanten = 8; //No check for Chiitoitsu in this function, so this is maximum
  2599.  
  2600. var callTriples = parseInt(getTriples(calls[0]).length / 3);
  2601.  
  2602. var triplesAndPairs = getTriplesAndPairs(hand);
  2603.  
  2604. var triples = triplesAndPairs.triples;
  2605. var pairs = triplesAndPairs.pairs;
  2606. var doubles = getDoubles(removeTilesFromTileArray(hand, triples.concat(pairs)));
  2607.  
  2608. var baseShanten = calculateShanten(parseInt(triples.length / 3) + callTriples, parseInt(pairs.length / 2), parseInt(doubles.length / 2));
  2609.  
  2610. if (typeof discardedTile != 'undefined') { //When deciding whether to call for a tile there is no discarded tile in the evaluation
  2611. hand.push(discardedTile); //Calculate original values
  2612. var originalCombinations = getTriplesAndPairs(hand);
  2613. var originalTriples = originalCombinations.triples;
  2614. var originalPairs = originalCombinations.pairs;
  2615. var originalDoubles = getDoubles(removeTilesFromTileArray(hand, originalTriples.concat(originalPairs)));
  2616.  
  2617. var originalShanten = calculateShanten(parseInt(originalTriples.length / 3) + callTriples, parseInt(originalPairs.length / 2), parseInt(originalDoubles.length / 2));
  2618. hand.pop();
  2619. }
  2620. else {
  2621. var originalShanten = baseShanten;
  2622. }
  2623.  
  2624. var expectedScore = { open: 0, closed: 0, riichi: 0 }; //For the expected score (only looking at hands that improve the current hand)
  2625. var yaku = { open: 0, closed: 0 }; //Expected Yaku
  2626. var doraValue = 0; //Expected Dora
  2627. var waits = 0; //Waits when in Tenpai
  2628. var shape = 0; //When 1 shanten: Contains a value that indicates how good the shape of the hand is
  2629. var fu = 0;
  2630.  
  2631. var kita = 0;
  2632. if (getNumberOfPlayers() == 3) {
  2633. kita = getNumberOfKitaOfPlayer(0) * getTileDoraValue({ index: 4, type: 3 });
  2634. }
  2635.  
  2636. var waitTiles = [];
  2637. var tileCombinations = []; //List of combinations for second step to save calculation time
  2638.  
  2639. // STEP 1: Create List of combinations of tiles that can improve the hand
  2640. var newTiles1 = getUsefulTilesForDouble(hand); //For every tile: Find tiles that make them doubles or triples
  2641. for (let newTile of newTiles1) {
  2642.  
  2643. var numberOfTiles1 = getNumberOfTilesAvailable(newTile.index, newTile.type);
  2644. if (numberOfTiles1 <= 0) { //Skip if tile is dead
  2645. continue;
  2646. }
  2647.  
  2648. hand.push(newTile);
  2649. var newTiles2 = getUsefulTilesForDouble(hand).filter(t => getNumberOfTilesAvailable(t.index, t.type) > 0);
  2650. if (PERFORMANCE_MODE - timeSave <= 1) { //In Low Spec Mode: Ignore some combinations that are unlikely to improve the hand -> Less calculation time
  2651. newTiles2 = getUsefulTilesForTriple(hand).filter(t => getNumberOfTilesAvailable(t.index, t.type) > 0);
  2652. if (PERFORMANCE_MODE - timeSave <= 0) { //Ignore even more tiles for extremenly low spec...
  2653. newTiles2 = newTiles2.filter(t => t.type == newTile.type);
  2654. }
  2655. }
  2656.  
  2657. var newTiles2Objects = [];
  2658. for (let t of newTiles2) {
  2659. var dupl1 = tileCombinations.find(tc => isSameTile(tc.tile1, t)); //Check if combination is already in the array
  2660. var skip = false;
  2661. if (typeof dupl1 != 'undefined') {
  2662. var duplicateCombination = dupl1.tiles2.find(t2 => isSameTile(t2.tile2, newTile));
  2663. if (typeof duplicateCombination != 'undefined') { //If already exists: Set flag to count it twice and set flag to skip the current one
  2664. duplicateCombination.duplicate = true;
  2665. skip = true;
  2666. }
  2667. }
  2668. newTiles2Objects.push({ tile2: t, winning: false, furiten: false, triplesAndPairs: null, duplicate: false, skip: skip });
  2669. }
  2670.  
  2671. tileCombinations.push({ tile1: newTile, tiles2: newTiles2Objects, winning: false, furiten: false, triplesAndPairs: null });
  2672. hand.pop();
  2673. }
  2674.  
  2675. //STEP 2: Check if some of these tiles or combinations are winning or in furiten. We need to know this in advance for Step 3
  2676. for (let tileCombination of tileCombinations) {
  2677. //Simulate only the first tile drawn for now
  2678. var tile1 = tileCombination.tile1;
  2679. hand.push(tile1);
  2680.  
  2681. var triplesAndPairs2 = getTriplesAndPairs(hand);
  2682.  
  2683. var winning = isWinningHand(parseInt((triplesAndPairs2.triples.length / 3)) + callTriples, triplesAndPairs2.pairs.length / 2);
  2684. if (winning) {
  2685. waitTiles.push(tile1);
  2686. //Mark this tile in other combinations as not duplicate and no skip
  2687. for (let tc of tileCombinations) {
  2688. tc.tiles2.forEach(function (t2) {
  2689. if (isSameTile(tile1, t2.tile2)) {
  2690. t2.duplicate = false;
  2691. t2.skip = false;
  2692. }
  2693. });
  2694. }
  2695. }
  2696. var furiten = (winning && (isTileFuriten(tile1.index, tile1.type) || isSameTile(discardedTile, tile1)));
  2697. tileCombination.winning = winning;
  2698. tileCombination.furiten = furiten;
  2699. tileCombination.triplesAndPairs = triplesAndPairs2; //The triplesAndPairs function is really slow, so save this result for later
  2700.  
  2701. hand.pop();
  2702. }
  2703.  
  2704. var tile1Furiten = tileCombinations.filter(t => t.furiten).length > 0;
  2705. for (let tileCombination of tileCombinations) { //Now again go through all the first tiles, but also the second tiles
  2706. hand.push(tileCombination.tile1);
  2707. for (let tile2Data of tileCombination.tiles2) {
  2708. if (tile2Data.skip || (tileCombination.winning && !tile1Furiten)) { //Ignore second tile if marked as skip(is a duplicate) or already winning with tile 1
  2709. continue;
  2710. }
  2711. hand.push(tile2Data.tile2);
  2712.  
  2713. var triplesAndPairs3 = getTriplesAndPairs(hand);
  2714.  
  2715. var winning2 = isWinningHand(parseInt((triplesAndPairs3.triples.length / 3)) + callTriples, triplesAndPairs3.pairs.length / 2);
  2716. var furiten2 = winning2 && (isTileFuriten(tile2Data.tile2.index, tile2Data.tile2.type) || isSameTile(discardedTile, tile2Data.tile2));
  2717. tile2Data.winning = winning2;
  2718. tile2Data.furiten = furiten2;
  2719. tile2Data.triplesAndPairs = triplesAndPairs3;
  2720.  
  2721. hand.pop();
  2722. }
  2723. hand.pop();
  2724. }
  2725.  
  2726. var numberOfTotalCombinations = 0;
  2727. var numberOfTotalWaitCombinations = 0;
  2728.  
  2729. //STEP 3: Check the values when these tiles are drawn.
  2730. for (let tileCombination of tileCombinations) {
  2731. var tile1 = tileCombination.tile1;
  2732. var numberOfTiles1 = getNumberOfTilesAvailable(tile1.index, tile1.type);
  2733.  
  2734. //Simulate only the first tile drawn for now
  2735. hand.push(tile1);
  2736.  
  2737. var triplesAndPairs2 = tileCombination.triplesAndPairs;
  2738. var triples2 = triplesAndPairs2.triples;
  2739. var pairs2 = triplesAndPairs2.pairs;
  2740.  
  2741. if (!isClosed && (!tileCombination.winning) &&
  2742. getNumberOfTilesInTileArray(triples2, tile1.index, tile1.type) == 3) {
  2743. numberOfTiles1 *= 2; //More value to possible triples when hand is open (can call pons from all players)
  2744. }
  2745.  
  2746. var factor;
  2747. var thisShanten = 8;
  2748. if (tileCombination.winning && !tile1Furiten) { //Hand is winning: Add the values of the hand for most possible ways to draw this:
  2749. factor = numberOfTiles1 * (availableTiles.length - 1); //Number of ways to draw this tile first and then any of the other tiles
  2750. //Number of ways to draw a random tile which we don't have in the array and then the winning tile. We only look at the "good tile -> winning tile" combination later.
  2751. factor += (availableTiles.length - tileCombinations.reduce((pv, cv) => pv + getNumberOfTilesAvailable(cv.tile1.index, cv.tile1.type), 0)) * numberOfTiles1;
  2752. thisShanten = (-1 - baseShanten);
  2753. }
  2754. else { // This tile is not winning
  2755. // For all the tiles we don't consider as a second draw (because they're useless): The shanten value for this tile -> useless tile is just the value after the first draw
  2756. var doubles2 = getDoubles(removeTilesFromTileArray(hand, triples2.concat(pairs2)));
  2757. factor = numberOfTiles1 * ((availableTiles.length - 1) - tileCombination.tiles2.reduce(function (pv, cv) { // availableTiles - useful tiles (which we will check later)
  2758. if (isSameTile(tile1, cv.tile2)) {
  2759. return pv + getNumberOfTilesAvailable(cv.tile2.index, cv.tile2.type) - 1;
  2760. }
  2761. return pv + getNumberOfTilesAvailable(cv.tile2.index, cv.tile2.type);
  2762. }, 0));
  2763. if (tile1Furiten) {
  2764. thisShanten = 0 - baseShanten;
  2765. }
  2766. else {
  2767. thisShanten = (calculateShanten(parseInt(triples2.length / 3) + callTriples, parseInt(pairs2.length / 2), parseInt(doubles2.length / 2)) - baseShanten);
  2768. }
  2769. }
  2770.  
  2771. shanten += thisShanten * factor;
  2772.  
  2773. if (tileCombination.winning) { //For winning tiles: Add waits, fu and the Riichi value
  2774. var thisDora = getNumberOfDoras(triples2.concat(pairs2, calls[0]));
  2775. var thisYaku = getYaku(hand, calls[0], triplesAndPairs2);
  2776. var thisWait = numberOfTiles1 * getWaitQuality(tile1);
  2777. var thisFu = calculateFu(triples2, calls[0], pairs2, removeTilesFromTileArray(hand, triples.concat(pairs).concat(tile1)), tile1);
  2778. if (isClosed || thisYaku.open >= 1 || tilesLeft <= 4) {
  2779. if (tile1Furiten && tilesLeft > 4) {
  2780. thisWait = numberOfTiles1 / 6;
  2781. }
  2782. waits += thisWait;
  2783. fu += thisFu * thisWait * factor;
  2784. if (thisFu == 30 && isClosed) {
  2785. thisYaku.closed += 1;
  2786. }
  2787. doraValue += thisDora * factor;
  2788. yaku.open += thisYaku.open * factor;
  2789. yaku.closed += thisYaku.closed * factor;
  2790. expectedScore.open += calculateScore(0, thisYaku.open + thisDora + kita, thisFu) * factor;
  2791. expectedScore.closed += calculateScore(0, thisYaku.closed + thisDora + kita, thisFu) * factor;
  2792. numberOfTotalCombinations += factor;
  2793. }
  2794.  
  2795. expectedScore.riichi += calculateScore(0, thisYaku.closed + thisDora + kita + 1 + 0.2 + getUradoraChance(), thisFu) * thisWait * factor;
  2796. numberOfTotalWaitCombinations += factor * thisWait;
  2797. if (!tile1Furiten) {
  2798. hand.pop();
  2799. continue; //No need to check this tile in combination with any of the other tiles, if this is drawn first and already wins
  2800. }
  2801. }
  2802.  
  2803. var tile2Furiten = tileCombination.tiles2.filter(t => t.furiten).length > 0;
  2804.  
  2805. for (let tile2Data of tileCombination.tiles2) {//Look at second tiles if not already winning
  2806. var tile2 = tile2Data.tile2;
  2807. var numberOfTiles2 = getNumberOfTilesAvailable(tile2.index, tile2.type);
  2808. if (isSameTile(tile1, tile2)) {
  2809. if (numberOfTiles2 == 1) {
  2810. continue;
  2811. }
  2812. numberOfTiles2--;
  2813. }
  2814.  
  2815. if (tile2Data.skip) {
  2816. continue;
  2817. }
  2818.  
  2819. var combFactor = numberOfTiles1 * numberOfTiles2; //Number of ways to draw tile 1 first and then tile 2
  2820. if (tile2Data.duplicate) {
  2821. combFactor *= 2;
  2822. }
  2823.  
  2824. hand.push(tile2); //Simulate second draw
  2825.  
  2826. var triplesAndPairs3 = tile2Data.triplesAndPairs;
  2827. var triples3 = triplesAndPairs3.triples;
  2828. var pairs3 = triplesAndPairs3.pairs;
  2829.  
  2830. var thisShanten = 8;
  2831. var winning = isWinningHand(parseInt((triples3.length / 3)) + callTriples, pairs3.length / 2);
  2832.  
  2833. var thisDora = getNumberOfDoras(triples3.concat(pairs3, calls[0]));
  2834. var thisYaku = getYaku(hand, calls[0], triplesAndPairs3);
  2835.  
  2836. if (!isClosed && (!winning || tile2Furiten) &&
  2837. getNumberOfTilesInTileArray(triples3, tile2.index, tile2.type) == 3) {
  2838. combFactor *= 2; //More value to possible triples when hand is open (can call pons from all players)
  2839. }
  2840.  
  2841. if (winning && !tile2Furiten) { //If this tile combination wins in 2 turns: calculate shape etc.
  2842. thisShanten = -1 - baseShanten;
  2843. if (waitTiles.filter(t => isSameTile(t, tile2)).length == 0) {
  2844. var newShape = numberOfTiles2 * getWaitQuality(tile2) * ((numberOfTiles1) / availableTiles.length);
  2845. if (tile2Data.duplicate) {
  2846. newShape += numberOfTiles1 * getWaitQuality(tile1) * ((numberOfTiles2) / availableTiles.length);
  2847. }
  2848. shape += newShape;
  2849. }
  2850.  
  2851. var secondDiscard = removeTilesFromTileArray(hand, triples3.concat(pairs3))[0];
  2852. if (!tile2Data.duplicate) {
  2853. var newFu = calculateFu(triples3, calls[0], pairs3, removeTilesFromTileArray(hand, triples.concat(pairs).concat(tile2).concat(secondDiscard)), tile2);
  2854. if (newFu == 30 && isClosed) {
  2855. thisYaku.closed += 1;
  2856. }
  2857. }
  2858. else { //Calculate Fu for drawing both tiles in different orders
  2859. var newFu = calculateFu(triples3, calls[0], pairs3, removeTilesFromTileArray(hand, triples.concat(pairs).concat(tile2).concat(secondDiscard)), tile2);
  2860. var newFu2 = calculateFu(triples3, calls[0], pairs3, removeTilesFromTileArray(hand, triples.concat(pairs).concat(tile1).concat(secondDiscard)), tile1);
  2861. if (newFu == 30 && isClosed) {
  2862. thisYaku.closed += 0.5;
  2863. }
  2864. if (newFu2 == 30 && isClosed) {
  2865. thisYaku.closed += 0.5;
  2866. }
  2867. }
  2868. }
  2869. else { //Not winning? Calculate shanten correctly
  2870. if (winning && (tile2Furiten || (!isClosed && thisYaku.open < 1))) { //Furiten/No Yaku: We are 0 shanten
  2871. thisShanten = 0 - baseShanten;
  2872. }
  2873. else {
  2874. var numberOfDoubles = getDoubles(removeTilesFromTileArray(hand, triples3.concat(pairs3))).length;
  2875. var numberOfPairs = pairs3.length;
  2876. thisShanten = calculateShanten(parseInt(triples3.length / 3) + callTriples, parseInt(numberOfPairs / 2), parseInt(numberOfDoubles / 2)) - baseShanten;
  2877. if (thisShanten == -1) { //Give less prio to tile combinations that only improve the hand by 1 shanten in two turns.
  2878. thisShanten = -0.5;
  2879. }
  2880. }
  2881. }
  2882. shanten += thisShanten * combFactor;
  2883.  
  2884. if (winning || thisShanten < 0) {
  2885. doraValue += thisDora * combFactor;
  2886. yaku.open += thisYaku.open * combFactor;
  2887. yaku.closed += thisYaku.closed * combFactor;
  2888. expectedScore.open += calculateScore(0, thisYaku.open + thisDora + kita) * combFactor;
  2889. expectedScore.closed += calculateScore(0, thisYaku.closed + thisDora + kita) * combFactor;
  2890. numberOfTotalCombinations += combFactor;
  2891. }
  2892.  
  2893. hand.pop();
  2894. }
  2895.  
  2896. hand.pop();
  2897. }
  2898.  
  2899. var allCombinations = availableTiles.length * (availableTiles.length - 1);
  2900. shanten /= allCombinations; //Divide by total amount of possible draw combinations
  2901.  
  2902. if (numberOfTotalCombinations > 0) {
  2903. expectedScore.open /= numberOfTotalCombinations; //Divide by the total combinations we checked, to get the average expected value
  2904. expectedScore.closed /= numberOfTotalCombinations;
  2905. doraValue /= numberOfTotalCombinations;
  2906. yaku.open /= numberOfTotalCombinations;
  2907. yaku.closed /= numberOfTotalCombinations;
  2908. }
  2909. if (numberOfTotalWaitCombinations > 0) {
  2910. expectedScore.riichi /= numberOfTotalWaitCombinations;
  2911. fu /= numberOfTotalWaitCombinations;
  2912. }
  2913. if (waitTiles.length > 0) {
  2914. waits *= (waitTiles.length * 0.15) + 0.75; //Waiting on multiple tiles is better
  2915. }
  2916.  
  2917. fu = fu <= 30 ? 30 : fu;
  2918. fu = fu > 110 ? 30 : fu;
  2919.  
  2920. var efficiency = (shanten + (baseShanten - originalShanten)) * -1; //Percent Number that indicates how big the chance is to improve the hand (in regards to efficiency). Negative for increasing shanten with the discard
  2921. if (originalShanten == 0) { //Already in Tenpai: Look at waits instead
  2922. if (baseShanten == 0) {
  2923. efficiency = (waits + shape) / 10;
  2924. }
  2925. else {
  2926. efficiency = ((shanten / 1.7) * -1);
  2927. }
  2928. }
  2929.  
  2930. if (baseShanten > 0) { //When not tenpai
  2931. expectedScore.riichi = calculateScore(0, yaku.closed + doraValue + kita + 1 + 0.2 + getUradoraChance());
  2932. }
  2933.  
  2934. var danger = 0;
  2935. var sakigiri = 0;
  2936. if (typeof discardedTile != 'undefined') { //When deciding whether to call for a tile there is no discarded tile in the evaluation
  2937. danger = getTileDanger(discardedTile);
  2938. sakigiri = getSakigiriValue(hand, discardedTile);
  2939. }
  2940.  
  2941. var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);
  2942.  
  2943. var riichiPriority = 0;
  2944. if (originalShanten == 0) { //Already in Tenpai: Look at waits instead
  2945. riichiEfficiency = waits / 10;
  2946. riichiPriority = calculateTilePriority(riichiEfficiency, expectedScore, danger - sakigiri);
  2947. }
  2948.  
  2949. return {
  2950. tile: discardedTile, priority: priority, riichiPriority: riichiPriority, shanten: baseShanten, efficiency: efficiency,
  2951. score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: shape, danger: danger, fu: fu
  2952. };
  2953. }
  2954.  
  2955. //Calculates a relative priority based on how "good" the given values are.
  2956. //The resulting priority value is useless as an absolute value, only use it relatively to compare with other values of the same hand.
  2957. function calculateTilePriority(efficiency, expectedScore, danger) {
  2958. var score = expectedScore.open;
  2959. if (isClosed) {
  2960. score = expectedScore.closed;
  2961. }
  2962.  
  2963. var placementFactor = 1;
  2964.  
  2965. if (isLastGame() && getDistanceToFirst() < 0) { //First Place in last game:
  2966. placementFactor = 1.5;
  2967. }
  2968.  
  2969. //Basically the formula should be efficiency multiplied by score (=expected value of the hand)
  2970. //But it's generally better to just win even with a small score to prevent others from winning (and no-ten penalty)
  2971. //That's why efficiency is weighted a bit higher with Math.pow.
  2972. var weightedEfficiency = Math.pow(Math.abs(efficiency), 0.3 + EFFICIENCY * placementFactor);
  2973. weightedEfficiency = efficiency < 0 ? -weightedEfficiency : weightedEfficiency;
  2974.  
  2975. score -= (danger * 2 * SAFETY);
  2976.  
  2977. if (weightedEfficiency < 0) { //Hotfix for negative efficiency (increasing shanten)
  2978. score = 50000 - score;
  2979. }
  2980.  
  2981. return weightedEfficiency * score;
  2982. }
  2983.  
  2984. //Get Chiitoitsu Priorities -> Look for Pairs
  2985. function chiitoitsuPriorities() {
  2986.  
  2987. var tiles = [];
  2988.  
  2989. var originalPairs = getPairsAsArray(ownHand);
  2990.  
  2991. var originalShanten = 6 - (originalPairs.length / 2);
  2992.  
  2993. for (var i = 0; i < ownHand.length; i++) { //Create 13 Tile hands, check for pairs
  2994. var newHand = [...ownHand];
  2995. newHand.splice(i, 1);
  2996. var pairs = getPairsAsArray(newHand);
  2997. var pairsValue = pairs.length / 2;
  2998. var handWithoutPairs = removeTilesFromTileArray(newHand, pairs);
  2999.  
  3000. var baseDora = getNumberOfDoras(pairs);
  3001. var doraValue = 0;
  3002. var baseShanten = 6 - pairsValue;
  3003.  
  3004. var waits = 0;
  3005. var shanten = 0;
  3006.  
  3007. var baseYaku = getYaku(newHand, calls[0]);
  3008. var yaku = { open: 0, closed: 0 };
  3009.  
  3010. var shape = 0;
  3011.  
  3012. //Possible Value, Yaku and Dora after Draw
  3013. handWithoutPairs.forEach(function (tile) {
  3014. var currentHand = [...handWithoutPairs];
  3015. currentHand.push(tile);
  3016. var numberOfTiles = getNumberOfNonFuritenTilesAvailable(tile.index, tile.type);
  3017. var chance = (numberOfTiles + (getWaitQuality(tile) / 10)) / availableTiles.length;
  3018. var pairs2 = getPairsAsArray(currentHand);
  3019. if (pairs2.length > 0) { //If the tiles improves the hand: Calculate the expected values
  3020. shanten += ((6 - (pairsValue + (pairs2.length / 2))) - baseShanten) * chance;
  3021. doraValue += getNumberOfDoras(pairs2) * chance;
  3022. var y2 = getYaku(currentHand.concat(pairs), calls[0]);
  3023. yaku.open += (y2.open - baseYaku.open) * chance;
  3024. yaku.closed += (y2.closed - baseYaku.closed) * chance;
  3025. if (pairsValue + (pairs2.length / 2) == 7) { //Winning hand
  3026. waits = numberOfTiles * getWaitQuality(tile);
  3027. doraValue = getNumberOfDoras(pairs2);
  3028. if (tile.index < 3 || tile.index > 7 || tile.doraValue > 0 || getWaitQuality(tile) > 1.1 || //Good Wait
  3029. currentHand.filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0) { //Or Tanyao
  3030. shape = 1;
  3031. }
  3032. }
  3033. }
  3034. });
  3035. doraValue += baseDora;
  3036. yaku.open += baseYaku.open;
  3037. yaku.closed += baseYaku.closed + 2; //Add Chiitoitsu manually
  3038. if (getNumberOfPlayers() == 3) {
  3039. doraValue += getNumberOfKitaOfPlayer(0) * getTileDoraValue({ index: 4, type: 3 });
  3040. }
  3041.  
  3042. var expectedScore = {
  3043. open: 1000, closed: calculateScore(0, yaku.closed + doraValue, 25),
  3044. riichi: calculateScore(0, yaku.closed + doraValue + 1 + 0.2 + getUradoraChance(), 25)
  3045. };
  3046.  
  3047. var efficiency = (shanten + (baseShanten - originalShanten)) * -1;
  3048. if (originalShanten == 0) { //Already in Tenpai: Look at waits instead
  3049. efficiency = waits / 10;
  3050. }
  3051. var danger = getTileDanger(ownHand[i]);
  3052.  
  3053. var sakigiri = getSakigiriValue(newHand, ownHand[i]);
  3054.  
  3055. var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);
  3056. tiles.push({
  3057. tile: ownHand[i], priority: priority, riichiPriority: priority, shanten: baseShanten, efficiency: efficiency,
  3058. score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: shape, danger: danger, fu: 25
  3059. });
  3060. }
  3061.  
  3062. return tiles;
  3063. }
  3064.  
  3065. //Get Thirteen Orphans Priorities -> Look for Honors/1/9
  3066. //Returns Array of tiles with priorities (value, danger etc.)
  3067. function thirteenOrphansPriorities() {
  3068.  
  3069. var originalOwnTerminalHonors = getAllTerminalHonorFromHand(ownHand);
  3070. // Filter out all duplicate terminal/honors
  3071. var originalUniqueTerminalHonors = [];
  3072. originalOwnTerminalHonors.forEach(tile => {
  3073. if (!originalUniqueTerminalHonors.some(otherTile => isSameTile(tile, otherTile))) {
  3074. originalUniqueTerminalHonors.push(tile);
  3075. }
  3076. });
  3077. var originalShanten = 13 - originalUniqueTerminalHonors.length;
  3078. if (originalOwnTerminalHonors.length > originalUniqueTerminalHonors.length) { //At least one terminal/honor twice
  3079. originalShanten -= 1;
  3080. }
  3081.  
  3082. var tiles = [];
  3083. for (var i = 0; i < ownHand.length; i++) { //Simulate discard of every tile
  3084.  
  3085. var hand = [...ownHand];
  3086. hand.splice(i, 1);
  3087.  
  3088. var ownTerminalHonors = getAllTerminalHonorFromHand(hand);
  3089. // Filter out all duplicate terminal/honors
  3090. var uniqueTerminalHonors = [];
  3091. ownTerminalHonors.forEach(tile => {
  3092. if (!uniqueTerminalHonors.some(otherTile => isSameTile(tile, otherTile))) {
  3093. uniqueTerminalHonors.push(tile);
  3094. }
  3095. });
  3096. var shanten = 13 - uniqueTerminalHonors.length;
  3097. if (ownTerminalHonors.length > uniqueTerminalHonors.length) { //At least one terminal/honor twice
  3098. shanten -= 1;
  3099. }
  3100. var doraValue = getNumberOfDoras(hand);
  3101. var yaku = { open: 13, closed: 13 };
  3102. var waits = 0;
  3103. if (shanten == 0) {
  3104. var missingTile = getMissingTilesForThirteenOrphans(uniqueTerminalHonors)[0];
  3105. waits = getNumberOfNonFuritenTilesAvailable(missingTile.index, missingTile.type);
  3106. }
  3107.  
  3108. var efficiency = shanten == originalShanten ? 1 : 0;
  3109. var danger = getTileDanger(ownHand[i]);
  3110. var sakigiri = getSakigiriValue(hand, ownHand[i], danger);
  3111. var yakuman = calculateScore(0, 13);
  3112. var expectedScore = { open: 0, closed: yakuman, riichi: yakuman };
  3113. var priority = calculateTilePriority(efficiency, expectedScore, danger - sakigiri);
  3114.  
  3115. tiles.push({
  3116. tile: ownHand[i], priority: priority, riichiPriority: priority, shanten: shanten, efficiency: efficiency,
  3117. score: expectedScore, dora: doraValue, yaku: yaku, waits: waits, shape: 0, danger: danger, fu: 30
  3118. });
  3119.  
  3120. }
  3121.  
  3122. return tiles;
  3123. }
  3124.  
  3125. // Used during the match to see if its still viable to go for thirteen orphans.
  3126. function canDoThirteenOrphans() {
  3127.  
  3128. // PARAMETERS
  3129. var max_missing_orphans_count = 2; // If an orphan has been discarded more than this time (and is not in hand), we don't go for thirteen orphan.
  3130. // Ie. 'Red Dragon' is not in hand, but been discarded 3-times on field. We stop going for thirteen orphan.
  3131.  
  3132. if (!isClosed) { //Already called some tiles? Can't do thirteen orphans
  3133. return false;
  3134. }
  3135.  
  3136. var ownTerminalHonors = getAllTerminalHonorFromHand(ownHand);
  3137.  
  3138. // Filter out all duplicate terminal/honors
  3139. var uniqueTerminalHonors = [];
  3140. ownTerminalHonors.forEach(tile => {
  3141. if (!uniqueTerminalHonors.some(otherTile => isSameTile(tile, otherTile))) {
  3142. uniqueTerminalHonors.push(tile);
  3143. }
  3144. });
  3145.  
  3146. // Fails if we do not have enough unique orphans.
  3147. if (uniqueTerminalHonors.length < THIRTEEN_ORPHANS) {
  3148. return false;
  3149. }
  3150.  
  3151. // Get list of missing orphans.
  3152. var missingOrphans = getMissingTilesForThirteenOrphans(uniqueTerminalHonors);
  3153.  
  3154. if (missingOrphans.length == 1) {
  3155. max_missing_orphans_count = 3;
  3156. }
  3157.  
  3158. // Check if there are enough required orphans in the pool.
  3159. for (let uniqueOrphan of missingOrphans) {
  3160. if (4 - getNumberOfNonFuritenTilesAvailable(uniqueOrphan.index, uniqueOrphan.type) > max_missing_orphans_count) {
  3161. return false;
  3162. }
  3163. }
  3164.  
  3165. return true;
  3166. }
  3167.  
  3168. //Return a list of missing tiles for thirteen orphans
  3169. function getMissingTilesForThirteenOrphans(uniqueTerminalHonors) {
  3170. var thirteen_orphans_set = "19m19p19s1234567z";
  3171. var thirteenOrphansTiles = getTilesFromString(thirteen_orphans_set);
  3172. return thirteenOrphansTiles.filter(tile => !uniqueTerminalHonors.some(otherTile => isSameTile(tile, otherTile)));
  3173. }
  3174.  
  3175.  
  3176. //Discards the "best" tile
  3177. async function discard() {
  3178.  
  3179. var tiles = await getTilePriorities(ownHand);
  3180. tiles = sortOutUnsafeTiles(tiles);
  3181.  
  3182. if (KEEP_SAFETILE) {
  3183. tiles = keepSafetile(tiles);
  3184. }
  3185.  
  3186. if (strategy == STRATEGIES.FOLD || tiles.filter(t => t.safe).length == 0) {
  3187. return discardFold(tiles);
  3188. }
  3189.  
  3190. log("Tile Priorities: ");
  3191. printTilePriority(tiles);
  3192.  
  3193. var tile = getDiscardTile(tiles);
  3194.  
  3195. var riichi = false;
  3196. if (canRiichi()) {
  3197. tiles.sort(function (p1, p2) {
  3198. return p2.riichiPriority - p1.riichiPriority;
  3199. });
  3200. riichi = callRiichi(tiles);
  3201. }
  3202. if (!riichi) {
  3203. discardTile(tile);
  3204. }
  3205.  
  3206. return tile;
  3207. }
  3208.  
  3209. //Check all tiles for enough safety
  3210. function sortOutUnsafeTiles(tiles) {
  3211. for (let tile of tiles) {
  3212. if (tile == tiles[0]) {
  3213. var highestPrio = true;
  3214. }
  3215. else {
  3216. var highestPrio = false;
  3217. }
  3218. if (shouldFold(tile, highestPrio)) {
  3219. tile.safe = 0;
  3220. }
  3221. else {
  3222. tile.safe = 1;
  3223. }
  3224. }
  3225. tiles = tiles.sort(function (p1, p2) {
  3226. return p2.safe - p1.safe;
  3227. });
  3228. return tiles;
  3229. }
  3230.  
  3231. //If there is only 1 safetile in hand, don't discard it.
  3232. function keepSafetile(tiles) {
  3233. if (getCurrentDangerLevel() > 2000 || tiles[0].shanten <= 1) { //Don't keep a safetile when it's too dangerous or hand is close to tenpai
  3234. return tiles;
  3235. }
  3236. var safeTiles = 0;
  3237. for (let t of tiles) {
  3238. if (isSafeTile(1, t.tile) && isSafeTile(2, t.tile) && (getNumberOfPlayers() == 3 || isSafeTile(3, t.tile))) {
  3239. safeTiles++;
  3240. }
  3241. }
  3242. if (safeTiles > 1) {
  3243. return tiles;
  3244. }
  3245.  
  3246. if (getNumberOfPlayers() == 3) {
  3247. var tilesSafety = tiles.map(t => getWaitScoreForTileAndPlayer(1, t.tile, false) +
  3248. getWaitScoreForTileAndPlayer(2, t.tile, false));
  3249. }
  3250. else {
  3251. var tilesSafety = tiles.map(t => getWaitScoreForTileAndPlayer(1, t.tile, false) +
  3252. getWaitScoreForTileAndPlayer(2, t.tile, false) +
  3253. getWaitScoreForTileAndPlayer(3, t.tile, false));
  3254. }
  3255.  
  3256. var safetileIndex = tilesSafety.indexOf(Math.min(...tilesSafety));
  3257.  
  3258. tiles.push(tiles.splice(safetileIndex, 1)[0]);
  3259.  
  3260. return tiles;
  3261. }
  3262.  
  3263. //Input: Tile Priority List
  3264. //Output: Best Tile to discard. Usually the first tile in the list, but for open hands a valid yaku is taken into account
  3265. function getDiscardTile(tiles) {
  3266. var tile = tiles[0].tile;
  3267.  
  3268. if (tiles[0].valid && (tiles[0].yaku.open >= 1 || isClosed || tileLeft <= 4)) {
  3269. return tile;
  3270. }
  3271.  
  3272. var highestYaku = -1;
  3273. for (let t of tiles) {
  3274. var foldThreshold = getFoldThreshold(t, ownHand);
  3275. if (t.valid && t.yaku.open > highestYaku + 0.01 && t.yaku.open / 3.5 > highestYaku && t.danger <= foldThreshold) {
  3276. tile = t.tile;
  3277. highestYaku = t.yaku.open;
  3278. if (t.yaku.open >= 1) {
  3279. break;
  3280. }
  3281. }
  3282. }
  3283. if (getTileName(tile) != (getTileName(tiles[0].tile))) {
  3284. log("Hand is open, trying to keep at least 1 Yaku.");
  3285. }
  3286. return tile;
  3287. }
  3288.  
  3289. //################################
  3290. // AI DEFENSE
  3291. // Defensive part of the AI
  3292. //################################
  3293.  
  3294. //Returns danger of tile for all players (from a specific players perspective, see second param) as a number from 0-100+
  3295. //Takes into account Genbutsu (Furiten for opponents), Suji, Walls and general knowledge about remaining tiles.
  3296. //From the perspective of playerPerspective parameter
  3297. function getTileDanger(tile, playerPerspective = 0) {
  3298. var dangerPerPlayer = [0, 0, 0, 0];
  3299. for (var player = 0; player < getNumberOfPlayers(); player++) { //Foreach Player
  3300. if (player == playerPerspective) {
  3301. continue;
  3302. }
  3303.  
  3304. dangerPerPlayer[player] = getDealInChanceForTileAndPlayer(player, tile, playerPerspective);
  3305.  
  3306. if (playerPerspective == 0) { //Multiply with expected deal in value
  3307. dangerPerPlayer[player] *= getExpectedDealInValue(player);
  3308. }
  3309.  
  3310. }
  3311.  
  3312. var danger = dangerPerPlayer[0] + dangerPerPlayer[1] + dangerPerPlayer[2] + dangerPerPlayer[3];
  3313.  
  3314. if (getCurrentDangerLevel() < 2500) { //Scale it down for low danger levels
  3315. danger *= 1 - ((2500 - getCurrentDangerLevel()) / 2500);
  3316. }
  3317.  
  3318. return danger;
  3319. }
  3320.  
  3321. //Return the Danger value for a specific tile and player
  3322. function getTileDangerForPlayer(tile, player, playerPerspective = 0) {
  3323. var danger = 0;
  3324. if (getLastTileInDiscard(player, tile) != null) { // Check if tile in discard (Genbutsu)
  3325. return 0;
  3326. }
  3327.  
  3328. danger = getWaitScoreForTileAndPlayer(player, tile, true, playerPerspective == 0); //Suji, Walls and general knowledge about remaining tiles.
  3329.  
  3330. if (danger <= 0) {
  3331. return 0;
  3332. }
  3333.  
  3334. //Honor tiles are often a preferred wait
  3335. if (tile.type == 3) {
  3336. danger *= 1.3;
  3337. }
  3338.  
  3339. //Is Dora? -> 10% more dangerous
  3340. danger *= (1 + (getTileDoraValue(tile) / 10));
  3341.  
  3342. //Is close to Dora? -> 5% more dangerous
  3343. if (isTileCloseToDora(tile)) {
  3344. danger *= 1.05;
  3345. }
  3346.  
  3347. //Is the player doing a flush of that type? -> More dangerous
  3348. var honitsuChance = isDoingHonitsu(player, tile.type);
  3349. var otherHonitsu = Math.max(isDoingHonitsu(player, 0) || isDoingHonitsu(player, 1) || isDoingHonitsu(player, 2));
  3350. if (honitsuChance > 0) {
  3351. danger *= 1 + honitsuChance;
  3352. }
  3353. else if (otherHonitsu > 0) { //Is the player going for any other flush?
  3354. if (tile.type == 3) {
  3355. danger *= 1 + otherHonitsu; //Honor tiles are also dangerous
  3356. }
  3357. else {
  3358. danger *= 1 - otherHonitsu; //Other tiles are less dangerous
  3359. }
  3360. }
  3361.  
  3362. //Is the player doing a tanyao? Inner tiles are more dangerous, outer tiles are less dangerous
  3363. if (tile.type != 3 && tile.index < 9 && tile.index > 1) {
  3364. danger *= 1 + (isDoingTanyao(player) / 10);
  3365. }
  3366. else {
  3367. danger /= 1 + (isDoingTanyao(player) / 10);
  3368. }
  3369.  
  3370. //Does the player have no yaku yet? Yakuhai is likely -> Honor tiles are 10% more dangerous
  3371. if (!hasYaku(player)) {
  3372. if (tile.type == 3 && (tile.index > 4 || tile.index == getSeatWind(player) || tile.index == getRoundWind()) &&
  3373. getNumberOfTilesAvailable(tile.type, tile.index) > 2) {
  3374. danger *= 1.1;
  3375. }
  3376. }
  3377.  
  3378. //Is Tile close to the tile discarded on the riichi turn? -> 10% more dangerous
  3379. if (isPlayerRiichi(player) && riichiTiles[getCorrectPlayerNumber(player)] != null &&
  3380. typeof riichiTiles[getCorrectPlayerNumber(player)] != 'undefined') {
  3381. if (isTileCloseToOtherTile(tile, riichiTiles[getCorrectPlayerNumber(player)])) {
  3382. danger *= 1.1;
  3383. }
  3384. }
  3385.  
  3386. //Is Tile close to an early discard (first row)? -> 10% less dangerous
  3387. discards[player].slice(0, 6).forEach(function (earlyDiscard) {
  3388. if (isTileCloseToOtherTile(tile, earlyDiscard)) {
  3389. danger *= 0.9;
  3390. }
  3391. });
  3392.  
  3393. //Danger is at least 5
  3394. if (danger < 5) {
  3395. danger = 5;
  3396. }
  3397.  
  3398. return danger;
  3399. }
  3400.  
  3401. //Percentage to deal in with a tile
  3402. function getDealInChanceForTileAndPlayer(player, tile, playerPerspective = 0) {
  3403. var total = 0;
  3404. if (playerPerspective == 0) {
  3405. if (typeof totalPossibleWaits.turn == 'undefined' || totalPossibleWaits.turn != tilesLeft) {
  3406. totalPossibleWaits = { turn: tilesLeft, totalWaits: [0, 0, 0, 0] }; // Save it in a global variable to not calculate this expensive step multiple times per turn
  3407. for (let pl = 1; pl < getNumberOfPlayers(); pl++) {
  3408. totalPossibleWaits.totalWaits[pl] = getTotalPossibleWaits(pl);
  3409. }
  3410. }
  3411. total = totalPossibleWaits.totalWaits[player];
  3412. }
  3413. if (playerPerspective != 0) {
  3414. total = getTotalPossibleWaits(player);
  3415. }
  3416. return getTileDangerForPlayer(tile, player, playerPerspective) / total; //Then compare the given tile with it, this is our deal in percentage
  3417. }
  3418.  
  3419. //Total amount of waits possible
  3420. function getTotalPossibleWaits(player) {
  3421. var total = 0;
  3422. for (let i = 1; i <= 9; i++) { // Go through all tiles and check how many combinations there are overall for waits.
  3423. for (let j = 0; j <= 3; j++) {
  3424. if (j == 3 && i >= 8) {
  3425. break;
  3426. }
  3427. total += getTileDangerForPlayer({ index: i, type: j }, player);
  3428. }
  3429. }
  3430. return total;
  3431. }
  3432.  
  3433. //Returns the expected deal in calue
  3434. function getExpectedDealInValue(player) {
  3435. var tenpaiChance = isPlayerTenpai(player);
  3436.  
  3437. var value = getExpectedHandValue(player);
  3438.  
  3439. //DealInValue is probability of player being in tenpai multiplied by the value of the hand
  3440. return tenpaiChance * value;
  3441. }
  3442.  
  3443. //Calculate the expected Han of the hand
  3444. function getExpectedHandValue(player) {
  3445. var doraValue = getNumberOfDoras(calls[player]); //Visible Dora (melds)
  3446.  
  3447. doraValue += getExpectedDoraInHand(player); //Dora in hidden tiles (hand)
  3448.  
  3449. //Kita (3 player mode only)
  3450. if (getNumberOfPlayers() == 3) {
  3451. doraValue += (getNumberOfKitaOfPlayer(player) * getTileDoraValue({ index: 4, type: 3 })) * 1;
  3452. }
  3453.  
  3454. var hanValue = 0;
  3455. if (isPlayerRiichi(player)) {
  3456. hanValue += 1;
  3457. }
  3458.  
  3459. //Yakus (only for open hands)
  3460. hanValue += (Math.max(isDoingHonitsu(player, 0) * 2), (isDoingHonitsu(player, 1) * 2), (isDoingHonitsu(player, 2) * 2)) +
  3461. (isDoingToiToi(player) * 2) + (isDoingTanyao(player) * 1) + (isDoingYakuhai(player) * 1);
  3462.  
  3463. //Expect some hidden Yaku when more tiles are unknown. 1.3 Yaku for a fully concealed hand, less for open hands
  3464. if (calls[player].length == 0) {
  3465. hanValue += 1.3;
  3466. }
  3467. else {
  3468. hanValue += getNumberOfTilesInHand(player) / 15;
  3469. }
  3470.  
  3471. hanValue = hanValue < 1 ? 1 : hanValue;
  3472.  
  3473. return calculateScore(player, hanValue + doraValue);
  3474. }
  3475.  
  3476. //How many dora does the player have on average in his hidden tiles?
  3477. function getExpectedDoraInHand(player) {
  3478. var uradora = 0;
  3479. if (isPlayerRiichi(player)) { //amount of dora indicators multiplied by chance to hit uradora
  3480. uradora = getUradoraChance();
  3481. }
  3482. return (((getNumberOfTilesInHand(player) + (discards[player].length / 2)) / availableTiles.length) * getNumberOfDoras(availableTiles)) + uradora;
  3483. }
  3484.  
  3485. //Returns the current Danger level of the table
  3486. function getCurrentDangerLevel(forPlayer = 0) { //Most Dangerous Player counts extra
  3487. var i = 1;
  3488. var j = 2;
  3489. var k = 3;
  3490. if (forPlayer == 1) {
  3491. i = 0;
  3492. }
  3493. if (forPlayer == 2) {
  3494. j = 0;
  3495. }
  3496. if (forPlayer == 3) {
  3497. k = 0;
  3498. }
  3499. if (getNumberOfPlayers() == 3) {
  3500. return ((getExpectedDealInValue(i) + getExpectedDealInValue(j) + Math.max(getExpectedDealInValue(i), getExpectedDealInValue(j))) / 3);
  3501. }
  3502. return ((getExpectedDealInValue(i) + getExpectedDealInValue(j) + getExpectedDealInValue(k) + Math.max(getExpectedDealInValue(i), getExpectedDealInValue(j), getExpectedDealInValue(k))) / 4);
  3503. }
  3504.  
  3505. //Returns the number of turns ago when the tile was most recently discarded
  3506. function getMostRecentDiscardDanger(tile, player, includeOthers) {
  3507. var danger = 99;
  3508. for (var i = 0; i < getNumberOfPlayers(); i++) {
  3509. var r = getLastTileInDiscard(i, tile);
  3510. if (player == i && r != null) { //Tile is in own discards
  3511. return 0;
  3512. }
  3513. if (!includeOthers || player == 0) {
  3514. continue;
  3515. }
  3516. if (r != null && typeof (r.numberOfPlayerHandChanges) == 'undefined') {
  3517. danger = 0;
  3518. }
  3519. else if (r != null && r.numberOfPlayerHandChanges[player] < danger) {
  3520. danger = r.numberOfPlayerHandChanges[player];
  3521. }
  3522. }
  3523.  
  3524. return danger;
  3525. }
  3526.  
  3527. //Returns the position of a tile in discards
  3528. function getLastTileInDiscard(player, tile) {
  3529. for (var i = discards[player].length - 1; i >= 0; i--) {
  3530. if (isSameTile(discards[player][i], tile)) {
  3531. return discards[player][i];
  3532. }
  3533. }
  3534. return wasTileCalledFromOtherPlayers(player, tile);
  3535. }
  3536.  
  3537. //Checks if a tile has been called by someone
  3538. function wasTileCalledFromOtherPlayers(player, tile) {
  3539. for (var i = 0; i < getNumberOfPlayers(); i++) {
  3540. if (i == player) { //Skip own melds
  3541. continue;
  3542. }
  3543. for (let t of calls[i]) { //Look through all melds and check where the tile came from
  3544. if (t.from == localPosition2Seat(player) && isSameTile(tile, t)) {
  3545. t.numberOfPlayerHandChanges = [10, 10, 10, 10];
  3546. return t;
  3547. }
  3548. }
  3549. }
  3550. return null;
  3551. }
  3552.  
  3553. //Returns a number from 0 to 1 how likely it is that the player is tenpai
  3554. function isPlayerTenpai(player) {
  3555. var numberOfCalls = parseInt(calls[player].length / 3);
  3556. if (isPlayerRiichi(player) || numberOfCalls >= 4) {
  3557. return 1;
  3558. }
  3559.  
  3560. if (getPlayerLinkState(player) == 0) { //disconnect
  3561. return 0;
  3562. }
  3563.  
  3564. //Based on: https://pathofhouou.blogspot.com/2021/04/analysis-tenpai-chance-by-tedashis-and.html
  3565. //This is only accurate for high level games!
  3566. var tenpaiChanceList = [[], [], [], []];
  3567. tenpaiChanceList[0] = [0, 0.1, 0.2, 0.5, 1, 1.8, 2.8, 4.2, 5.8, 7.6, 9.5, 11.5, 13.5, 15.5, 17.5, 19.5, 21.7, 23.9, 25, 27, 29, 31, 33, 35, 37];
  3568. tenpaiChanceList[1] = [0.2, 0.9, 2.3, 4.7, 8.3, 12.7, 17.9, 23.5, 29.2, 34.7, 39.7, 43.9, 47.4, 50.3, 52.9, 55.2, 57.1, 59, 61, 63, 65, 67, 69];
  3569. tenpaiChanceList[2] = [0, 5.1, 10.5, 17.2, 24.7, 32.3, 39.5, 46.1, 52, 57.2, 61.5, 65.1, 67.9, 69.9, 71.4, 72.4, 73.3, 74.2, 75, 76, 77, 78, 79];
  3570. tenpaiChanceList[3] = [0, 0, 41.9, 54.1, 63.7, 70.9, 76, 79.9, 83, 85.1, 86.7, 87.9, 88.7, 89.2, 89.5, 89.4, 89.3, 89.2, 89.2, 89.2, 90, 90, 90];
  3571.  
  3572. var numberOfDiscards = discards[player].length;
  3573. for (var i = 0; i < getNumberOfPlayers(); i++) {
  3574. if (i == player) {
  3575. continue;
  3576. }
  3577. for (let t of calls[i]) { //Look through all melds and check where the tile came from
  3578. if (t.from == localPosition2Seat(player)) {
  3579. numberOfDiscards++;
  3580. }
  3581. }
  3582. }
  3583.  
  3584. if (numberOfDiscards > 20) {
  3585. numberOfDiscards = 20;
  3586. }
  3587.  
  3588. try {
  3589. var tenpaiChance = tenpaiChanceList[numberOfCalls][numberOfDiscards] / 100;
  3590. }
  3591. catch {
  3592. var tenpaiChance = 0.5;
  3593. }
  3594.  
  3595. tenpaiChance *= 1 + (isPlayerPushing(player) / 5);
  3596.  
  3597. //Player who is doing Honitsu starts discarding tiles of his own type => probably tenpai
  3598. if ((isDoingHonitsu(player, 0) && discards[player].slice(10).filter(tile => tile.type == 0).length > 0)) {
  3599. tenpaiChance *= 1 + (isDoingHonitsu(player, 0) / 1.5);
  3600. }
  3601. if ((isDoingHonitsu(player, 1) && discards[player].slice(10).filter(tile => tile.type == 1).length > 0)) {
  3602. tenpaiChance *= 1 + (isDoingHonitsu(player, 1) / 1.5);
  3603. }
  3604. if ((isDoingHonitsu(player, 2) && discards[player].slice(10).filter(tile => tile.type == 2).length > 0)) {
  3605. tenpaiChance *= 1 + (isDoingHonitsu(player, 2) / 1.5);
  3606. }
  3607.  
  3608. var room = getCurrentRoom();
  3609. if (room < 5 && room > 0) { //Below Throne Room: Less likely to be tenpai
  3610. tenpaiChance *= 1 - ((5 - room) * 0.1); //10% less likely for every rank lower than throne room to be tenpai
  3611. }
  3612.  
  3613. if (tenpaiChance > 1) {
  3614. tenpaiChance = 1;
  3615. }
  3616. else if (tenpaiChance < 0) {
  3617. tenpaiChance = 0;
  3618. }
  3619.  
  3620. return tenpaiChance;
  3621. }
  3622.  
  3623. //Returns a number from -1 (fold) to 1 (push).
  3624. function isPlayerPushing(player) {
  3625. var lastDiscardSafety = playerDiscardSafetyList[player].slice(-3).filter(v => v >= 0); //Check safety of last three discards. If dangerous: Not folding.
  3626.  
  3627. if (playerDiscardSafetyList[player].length < 3 || lastDiscardSafety.length == 0) {
  3628. return 0;
  3629. }
  3630.  
  3631. var pushValue = -1 + (lastDiscardSafety.reduce((v1, v2) => v1 + (v2 * 20), 0) / lastDiscardSafety.length);
  3632. if (pushValue > 1) {
  3633. pushValue = 1;
  3634. }
  3635. return pushValue;
  3636. }
  3637.  
  3638. //Is the player doing any of the most common yaku?
  3639. function hasYaku(player) {
  3640. return (isDoingHonitsu(player, 0) > 0 || isDoingHonitsu(player, 1) > 0 || isDoingHonitsu(player, 2) > 0 ||
  3641. isDoingToiToi(player) > 0 || isDoingTanyao(player) > 0 || isDoingYakuhai(player) > 0);
  3642. }
  3643.  
  3644. //Return a confidence between 0 and 1 for how predictable the strategy of another player is (many calls -> very predictable)
  3645. function getConfidenceInYakuPrediction(player) {
  3646. var confidence = Math.pow(parseInt(calls[player].length / 3), 2) / 10;
  3647. if (confidence > 1) {
  3648. confidence = 1;
  3649. }
  3650. return confidence;
  3651. }
  3652.  
  3653. //Returns a value between 0 and 1 for how likely the player could be doing honitsu
  3654. function isDoingHonitsu(player, type) {
  3655. if (parseInt(calls[player].length) == 0 || calls[player].some(tile => tile.type != type && tile.type != 3)) { //Calls of different type -> false
  3656. return 0;
  3657. }
  3658. if (parseInt(calls[player].length / 3) == 4) {
  3659. return 1;
  3660. }
  3661. var percentageOfDiscards = discards[player].slice(0, 10).filter(tile => tile.type == type).length / discards[player].slice(0, 10).length;
  3662. if (percentageOfDiscards > 0.2 || discards[player].slice(0, 10).length == 0) {
  3663. return 0;
  3664. }
  3665. var confidence = (Math.pow(parseInt(calls[player].length / 3), 2) / 10) - percentageOfDiscards + 0.1;
  3666. if (confidence > 1) {
  3667. confidence = 1;
  3668. }
  3669. return confidence;
  3670. }
  3671.  
  3672. //Returns a value between 0 and 1 for how likely the player could be doing toitoi
  3673. function isDoingToiToi(player) {
  3674. if (parseInt(calls[player].length) > 0 && getSequences(calls[player]).length == 0) { //Only triplets called
  3675. return getConfidenceInYakuPrediction(player) - 0.1;
  3676. }
  3677. return 0;
  3678. }
  3679.  
  3680. //Returns a value between 0 and 1 for how likely the player could be doing tanyao
  3681. function isDoingTanyao(player) {
  3682. if (parseInt(calls[player].length) > 0 && calls[player].filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length == 0 &&
  3683. (discards[player].slice(0, 5).filter(tile => tile.type == 3 || tile.index == 1 || tile.index == 9).length / discards[player].slice(0, 5).length) >= 0.6) { //only inner tiles called and lots of terminal/honor discards
  3684. return getConfidenceInYakuPrediction(player);
  3685. }
  3686. return 0;
  3687. }
  3688.  
  3689. //Returns how many Yakuhai the player has
  3690. function isDoingYakuhai(player) {
  3691. var yakuhai = parseInt(calls[player].filter(tile => tile.type == 3 && (tile.index > 4 || tile.index == getSeatWind(player) || tile.index == roundWind)).length / 3);
  3692. yakuhai += parseInt(calls[player].filter(tile => tile.type == 3 && tile.index == getSeatWind(player) && tile.index == roundWind).length / 3);
  3693. return yakuhai;
  3694. }
  3695.  
  3696. //Returns a score how likely this tile can form the last triple/pair for a player
  3697. //Suji, Walls and general knowledge about remaining tiles.
  3698. //If "includeOthers" parameter is set to true it will also check if other players recently discarded relevant tiles
  3699. function getWaitScoreForTileAndPlayer(player, tile, includeOthers, useKnowledgeOfOwnHand = true) {
  3700. var tile0 = getNumberOfTilesAvailable(tile.index, tile.type);
  3701. var tile0Public = tile0 + getNumberOfTilesInTileArray(ownHand, tile.index, tile.type);
  3702. if (!useKnowledgeOfOwnHand) {
  3703. tile0 = tile0Public;
  3704. }
  3705. var furitenFactor = getFuritenValue(player, tile, includeOthers);
  3706.  
  3707. if (furitenFactor == 0) {
  3708. return 0;
  3709. }
  3710.  
  3711. //Less priority on Ryanmen and Bridge Wait when player is doing Toitoi
  3712. var toitoiFactor = 1 - (isDoingToiToi(player) / 3);
  3713.  
  3714. var score = 0;
  3715.  
  3716. //Same tile
  3717. score += tile0 * tile0Public * furitenFactor * 2 * (2 - toitoiFactor);
  3718.  
  3719. if (getNumberOfTilesInHand(player) == 1 || tile.type == 3) {
  3720. return score;
  3721. }
  3722.  
  3723. var tileL3Public = getNumberOfTilesAvailable(tile.index - 3, tile.type) + getNumberOfTilesInTileArray(ownHand, tile.index - 3, tile.type);
  3724. var tileU3Public = getNumberOfTilesAvailable(tile.index + 3, tile.type) + getNumberOfTilesInTileArray(ownHand, tile.index + 3, tile.type);
  3725.  
  3726. var tileL2 = getNumberOfTilesAvailable(tile.index - 2, tile.type);
  3727. var tileL1 = getNumberOfTilesAvailable(tile.index - 1, tile.type);
  3728. var tileU1 = getNumberOfTilesAvailable(tile.index + 1, tile.type);
  3729. var tileU2 = getNumberOfTilesAvailable(tile.index + 2, tile.type);
  3730.  
  3731. if (!useKnowledgeOfOwnHand) {
  3732. tileL2 += getNumberOfTilesInTileArray(ownHand, tile.index - 2, tile.type);
  3733. tileL1 += getNumberOfTilesInTileArray(ownHand, tile.index - 1, tile.type);
  3734. tileU1 += getNumberOfTilesInTileArray(ownHand, tile.index + 1, tile.type);
  3735. tileU2 += getNumberOfTilesInTileArray(ownHand, tile.index + 2, tile.type);
  3736. }
  3737.  
  3738. var furitenFactorL = getFuritenValue(player, { index: tile.index - 3, type: tile.type }, includeOthers);
  3739. var furitenFactorU = getFuritenValue(player, { index: tile.index + 3, type: tile.type }, includeOthers);
  3740.  
  3741. //Ryanmen Waits
  3742. score += (tileL1 * tileL2) * (tile0Public + tileL3Public) * furitenFactorL * toitoiFactor;
  3743. score += (tileU1 * tileU2) * (tile0Public + tileU3Public) * furitenFactorU * toitoiFactor;
  3744.  
  3745. //Bridge Wait
  3746. score += (tileL1 * tileU1 * tile0Public) * furitenFactor * toitoiFactor;
  3747.  
  3748. return score;
  3749. }
  3750.  
  3751. //Returns 0 if tile is 100% furiten, 1 if not. Value between 0-1 is returned if furiten tile was not called some turns ago.
  3752. function getFuritenValue(player, tile, includeOthers) {
  3753. var danger = getMostRecentDiscardDanger(tile, player, includeOthers);
  3754. if (danger == 0) {
  3755. return 0;
  3756. }
  3757. else if (danger == 1) {
  3758. if (calls[player].length > 0) {
  3759. return 0.5;
  3760. }
  3761. return 0.95;
  3762. }
  3763. else if (danger == 2) {
  3764. if (calls[player].length > 0) {
  3765. return 0.8;
  3766. }
  3767. }
  3768. return 1;
  3769. }
  3770.  
  3771. //Sets tile safeties for discards
  3772. function updateDiscardedTilesSafety() {
  3773. for (var k = 1; k < getNumberOfPlayers(); k++) { //For all other players
  3774. for (var i = 0; i < getNumberOfPlayers(); i++) { //For all discard ponds
  3775. for (var j = 0; j < discards[i].length; j++) { //For every tile in it
  3776. if (typeof (discards[i][j].numberOfPlayerHandChanges) == 'undefined') {
  3777. discards[i][j].numberOfPlayerHandChanges = [0, 0, 0, 0];
  3778. }
  3779. if (hasPlayerHandChanged(k)) {
  3780. if (j == discards[i].length - 1 && k < i && (k <= seat2LocalPosition(getCurrentPlayer()) || seat2LocalPosition(getCurrentPlayer()) == 0)) { //Ignore tiles by players after hand change
  3781. continue;
  3782. }
  3783. discards[i][j].numberOfPlayerHandChanges[k]++;
  3784. }
  3785. }
  3786. }
  3787. rememberPlayerHand(k);
  3788. }
  3789. }
  3790.  
  3791. //Pretty simple (all 0), but should work in case of crash -> count intelligently upwards
  3792. function initialDiscardedTilesSafety() {
  3793. for (var k = 1; k < getNumberOfPlayers(); k++) { //For all other players
  3794. for (var i = 0; i < getNumberOfPlayers(); i++) { //For all discard ponds
  3795. for (var j = 0; j < discards[i].length; j++) { //For every tile in it
  3796. if (typeof (discards[i][j].numberOfPlayerHandChanges) == 'undefined') {
  3797. discards[i][j].numberOfPlayerHandChanges = [0, 0, 0, 0];
  3798. }
  3799. var bonus = 0;
  3800. if (k < i && (k <= seat2LocalPosition(getCurrentPlayer()) || seat2LocalPosition(getCurrentPlayer()) == 0)) {
  3801. bonus = 1;
  3802. }
  3803. discards[i][j].numberOfPlayerHandChanges[k] = discards[i].length - j - bonus;
  3804. }
  3805. }
  3806. }
  3807. }
  3808.  
  3809. //Returns a value which indicates how important it is to sakigiri the tile now
  3810. function getSakigiriValue(hand, tile) {
  3811. var sakigiri = 0;
  3812. for (let player = 1; player < getNumberOfPlayers(); player++) {
  3813. if (discards[player].length < 3) { // Not many discards yet (very early) => ignore Sakigiri
  3814. continue;
  3815. }
  3816.  
  3817. if (getExpectedDealInValue(player) > 150) { // Obviously don't sakigiri when the player could already be in tenpai
  3818. continue;
  3819. }
  3820.  
  3821. if (isSafeTile(player, tile)) { // Tile is safe
  3822. continue;
  3823. }
  3824.  
  3825. var safeTiles = 0;
  3826. for (let t of hand) { // How many safe tiles do we currently have?
  3827. if (isSafeTile(player, t)) {
  3828. safeTiles++;
  3829. }
  3830. }
  3831.  
  3832. var saki = (3 - safeTiles) * (SAKIGIRI * 4);
  3833. if (saki <= 0) { // 3 or more safe tiles: Sakigiri not necessary
  3834. continue;
  3835. }
  3836.  
  3837. if (getSeatWind(player) == 1) { // Player is dealer
  3838. saki *= 1.5;
  3839. }
  3840. sakigiri += saki;
  3841. }
  3842. return sakigiri;
  3843. }
  3844.  
  3845. //Returns true when the given tile is safe for a given player
  3846. function isSafeTile(player, tile) {
  3847. return getWaitScoreForTileAndPlayer(player, tile, false) < 20 || (tile.type == 3 && availableTiles.filter(t => isSameTile(t, tile)).length <= 2);
  3848. }
  3849.  
  3850. //Check if the tile is close to another tile
  3851. function isTileCloseToOtherTile(tile, otherTile) {
  3852. if (tile.type != 3 && tile.type == otherTile.type) {
  3853. return tile.index >= otherTile.index - 3 && tile.index <= otherTile.index + 3;
  3854. }
  3855. }
  3856.  
  3857. //Check if the tile is close to dora
  3858. function isTileCloseToDora(tile) {
  3859. for (let d of dora) {
  3860. var doraIndex = getHigherTileIndex(d);
  3861. if (tile.type == 3 && d.type == 3 && tile.index == doraIndex) {
  3862. return true;
  3863. }
  3864. if (tile.type != 3 && tile.type == d.type && tile.index >= doraIndex - 2 && tile.index <= doraIndex + 2) {
  3865. return true;
  3866. }
  3867. }
  3868. return false;
  3869. }
  3870.  
  3871. //################################
  3872. // MAIN
  3873. // Main Class, starts the bot and sets up all necessary variables.
  3874. //################################
  3875.  
  3876. //GUI can be re-opened by pressing + on the Numpad
  3877. if (!isDebug()) {
  3878. initGui();
  3879. window.onkeyup = function (e) {
  3880. var key = e.keyCode ? e.keyCode : e.which;
  3881.  
  3882. if (key == 107 || key == 65) { // Numpad + Key
  3883. toggleGui();
  3884. }
  3885. }
  3886.  
  3887. if (AUTORUN) {
  3888. log("Autorun start");
  3889. run = true;
  3890. setInterval(preventAFK, 30000);
  3891. }
  3892.  
  3893. log(`crt mode ${AIMODE_NAME[MODE]}`);
  3894.  
  3895. waitForMainLobbyLoad();
  3896. }
  3897.  
  3898. function toggleRun() {
  3899. clearCrtStrategyMsg();
  3900. if (run) {
  3901. log("AlphaJong deactivated!");
  3902. run = false;
  3903. startButton.innerHTML = "Start Bot";
  3904. }
  3905. else if (!run) {
  3906. log("AlphaJong activated!");
  3907. run = true;
  3908. startButton.innerHTML = "Stop Bot";
  3909. main();
  3910. }
  3911. }
  3912.  
  3913. function waitForMainLobbyLoad() {
  3914. if (isInGame()) { // In case game is already ongoing after reload
  3915. refreshRoomSelection();
  3916. main();
  3917. return;
  3918. }
  3919.  
  3920. if (!hasFinishedMainLobbyLoading()) { //Otherwise wait for Main Lobby to load and then search for game
  3921. log("Waiting for Main Lobby to load...");
  3922. showCrtActionMsg("Wait for Loading.");
  3923. setTimeout(waitForMainLobbyLoad, 2000);
  3924. return;
  3925. }
  3926. log("Main Lobby loaded!");
  3927. refreshRoomSelection();
  3928. startGame();
  3929. setTimeout(main, 10000);
  3930. log("Main Loop started.");
  3931. }
  3932.  
  3933. //Main Loop
  3934. function main() {
  3935. if (!run) {
  3936. showCrtActionMsg("Bot is not running.");
  3937. return;
  3938. }
  3939. if (!isInGame()) {
  3940. checkForEnd();
  3941. showCrtActionMsg("Waiting for Game to start.");
  3942. log("Game is not running, sleep 2 seconds.");
  3943. errorCounter++;
  3944. if (errorCounter > 90 && AUTORUN) { //3 minutes no game found -> reload page
  3945. goToLobby();
  3946. }
  3947. setTimeout(main, 2000); //Check every 2 seconds if ingame
  3948. return;
  3949. }
  3950.  
  3951. if (isDisconnect()) {
  3952. goToLobby();
  3953. }
  3954.  
  3955. var operations = getOperationList(); //Get possible Operations
  3956.  
  3957. if (operations == null || operations.length == 0) {
  3958. errorCounter++;
  3959. if (getTilesLeft() == lastTilesLeft) { //1 minute no tile drawn
  3960. if (errorCounter > 120) {
  3961. goToLobby();
  3962. }
  3963. }
  3964. else {
  3965. lastTilesLeft = getTilesLeft();
  3966. errorCounter = 0;
  3967. }
  3968. clearCrtStrategyMsg();
  3969. showCrtActionMsg("Waiting for own turn.");
  3970. setTimeout(main, 500);
  3971.  
  3972. if (MODE === AIMODE.HELP) {
  3973. oldOps = [];
  3974. }
  3975. return;
  3976. }
  3977.  
  3978. showCrtActionMsg("Calculating best move...");
  3979.  
  3980. setTimeout(mainOwnTurn, 200 + (Math.random() * 200));
  3981. }
  3982.  
  3983. var oldOps = []
  3984. function recordPlayerOps() {
  3985. oldOps = []
  3986.  
  3987. let ops = getOperationList();
  3988. for (let op of ops) {
  3989. oldOps.push(op.type)
  3990. }
  3991. }
  3992.  
  3993. function checkPlayerOpChanged() {
  3994. let ops = getOperationList();
  3995. if (ops.length !== oldOps.length) {
  3996. return true;
  3997. }
  3998.  
  3999. for (let i = 0; i < ops.length; i++) {
  4000. if (ops[i].type !== oldOps[i]) {
  4001. return true;
  4002. }
  4003. }
  4004.  
  4005. return false;
  4006. }
  4007.  
  4008. async function mainOwnTurn() {
  4009. if (threadIsRunning) {
  4010. return;
  4011. }
  4012. threadIsRunning = true;
  4013.  
  4014. //HELP MODE, if player not operate, just skip
  4015. if (MODE === AIMODE.HELP) {
  4016. if (!checkPlayerOpChanged()) {
  4017. setTimeout(main, 1000);
  4018. threadIsRunning = false;
  4019. return;
  4020. } else {
  4021. recordPlayerOps();
  4022. }
  4023. }
  4024.  
  4025. setData(); //Set current state of the board to local variables
  4026.  
  4027. var operations = getOperationList();
  4028.  
  4029. log("##### OWN TURN #####");
  4030. log("Debug String: " + getDebugString());
  4031. if (getNumberOfPlayers() == 3) {
  4032. log("Right Player Tenpai Chance: " + Number(isPlayerTenpai(1) * 100).toFixed(1) + "%, Expected Hand Value: " + Number(getExpectedHandValue(1).toFixed(0)));
  4033. log("Left Player Tenpai Chance: " + Number(isPlayerTenpai(2) * 100).toFixed(1) + "%, Expected Hand Value: " + Number(getExpectedHandValue(2).toFixed(0)));
  4034. }
  4035. else {
  4036. log("Shimocha Tenpai Chance: " + Number(isPlayerTenpai(1) * 100).toFixed(1) + "%, Expected Hand Value: " + Number(getExpectedHandValue(1).toFixed(0)));
  4037. log("Toimen Tenpai Chance: " + Number(isPlayerTenpai(2) * 100).toFixed(1) + "%, Expected Hand Value: " + Number(getExpectedHandValue(2).toFixed(0)));
  4038. log("Kamicha Tenpai Chance: " + Number(isPlayerTenpai(3) * 100).toFixed(1) + "%, Expected Hand Value: " + Number(getExpectedHandValue(3).toFixed(0)));
  4039. }
  4040.  
  4041. determineStrategy(); //Get the Strategy for the current situation. After calls so it does not reset folds
  4042.  
  4043. isConsideringCall = true;
  4044. for (let operation of operations) { //Priority Operations: Should be done before discard on own turn
  4045. if (getOperationList().length == 0) {
  4046. break;
  4047. }
  4048. switch (operation.type) {
  4049. case getOperations().an_gang: //From Hand
  4050. callAnkan(operation.combination);
  4051. break;
  4052. case getOperations().add_gang: //Add from Hand to Pon
  4053. callShouminkan();
  4054. break;
  4055. case getOperations().zimo:
  4056. callTsumo();
  4057. break;
  4058. case getOperations().rong:
  4059. callRon();
  4060. break;
  4061. case getOperations().babei:
  4062. if (callKita()) {
  4063. threadIsRunning = false;
  4064. setTimeout(main, 1000);
  4065. return;
  4066. }
  4067. break;
  4068. case getOperations().jiuzhongjiupai:
  4069. callAbortiveDraw();
  4070. break;
  4071. }
  4072. }
  4073.  
  4074. for (let operation of operations) {
  4075. if (getOperationList().length == 0) {
  4076. break;
  4077. }
  4078. switch (operation.type) {
  4079. case getOperations().dapai:
  4080. isConsideringCall = false;
  4081. await discard();
  4082. break;
  4083. case getOperations().eat:
  4084. await callTriple(operation.combination, getOperations().eat);
  4085. break;
  4086. case getOperations().peng:
  4087. await callTriple(operation.combination, getOperations().peng);
  4088. break;
  4089. case getOperations().ming_gang: //From others
  4090. callDaiminkan();
  4091. break;
  4092. }
  4093. }
  4094.  
  4095. log(" ");
  4096.  
  4097. if (MODE === AIMODE.AUTO) {
  4098. showCrtActionMsg("Own turn completed.");
  4099. }
  4100.  
  4101. if ((getOverallTimeLeft() < 8 && getLastTurnTimeLeft() - getOverallTimeLeft() <= 0) || //Not much overall time left and last turn took longer than the 5 second increment
  4102. (getOverallTimeLeft() < 4 && getLastTurnTimeLeft() - getOverallTimeLeft() <= 1)) {
  4103. timeSave++;
  4104. log("Low performance! Activating time save mode level: " + timeSave);
  4105. }
  4106. if (getOverallTimeLeft() > 15) { //Much time left (new round)
  4107. timeSave = 0;
  4108. }
  4109.  
  4110. threadIsRunning = false;
  4111.  
  4112. setTimeout(main, 1000);
  4113.  
  4114. }
  4115.  
  4116. //Set Data from real Game
  4117. function setData(mainUpdate = true) {
  4118.  
  4119. dora = getDora();
  4120.  
  4121. ownHand = [];
  4122. for (let tile of getPlayerHand()) { //Get own Hand
  4123. ownHand.push(tile.val);
  4124. ownHand[ownHand.length - 1].valid = tile.valid; //Is valid discard
  4125. }
  4126.  
  4127. discards = [];
  4128. for (var j = 0; j < getNumberOfPlayers(); j++) { //Get Discards for all Players
  4129. var temp_discards = [];
  4130. for (var i = 0; i < getDiscardsOfPlayer(j).pais.length; i++) {
  4131. temp_discards.push(getDiscardsOfPlayer(j).pais[i].val);
  4132. }
  4133. if (getDiscardsOfPlayer(j).last_pai != null) {
  4134. temp_discards.push(getDiscardsOfPlayer(j).last_pai.val);
  4135. }
  4136. discards.push(temp_discards);
  4137. }
  4138. if (mainUpdate) {
  4139. updateDiscardedTilesSafety();
  4140. }
  4141.  
  4142. calls = [];
  4143. for (var j = 0; j < getNumberOfPlayers(); j++) { //Get Calls for all Players
  4144. calls.push(getCallsOfPlayer(j));
  4145. }
  4146.  
  4147. isClosed = true;
  4148. for (let tile of calls[0]) { //Is hand closed? Also consider closed Kans
  4149. if (tile.from != localPosition2Seat(0)) {
  4150. isClosed = false;
  4151. break;
  4152. }
  4153. }
  4154. if (tilesLeft < getTilesLeft()) { //Check if new round/reload
  4155. if (MODE === AIMODE.AUTO) {
  4156. setAutoCallWin(true);
  4157. }
  4158. strategy = STRATEGIES.GENERAL;
  4159. strategyAllowsCalls = true;
  4160. initialDiscardedTilesSafety();
  4161. riichiTiles = [null, null, null, null];
  4162. playerDiscardSafetyList = [[], [], [], []];
  4163. extendMJSoulFunctions();
  4164. }
  4165.  
  4166. tilesLeft = getTilesLeft();
  4167.  
  4168. if (!isDebug()) {
  4169. seatWind = getSeatWind(0);
  4170. roundWind = getRoundWind();
  4171. }
  4172.  
  4173. updateAvailableTiles();
  4174. }
  4175.  
  4176. //Search for Game
  4177. function startGame() {
  4178. if (!isInGame() && run && AUTORUN) {
  4179. log("Searching for Game in Room " + ROOM);
  4180. showCrtActionMsg("Searching for Game...");
  4181. searchForGame();
  4182. }
  4183. }
  4184.  
  4185. //Check if End Screen is shown
  4186. function checkForEnd() {
  4187. if (isEndscreenShown() && AUTORUN) {
  4188. run = false;
  4189. setTimeout(goToLobby, 25000);
  4190. }
  4191. }
  4192.  
  4193. //Reload Page to get back to lobby
  4194. function goToLobby() {
  4195. location.reload(1);
  4196. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址