AlphaJong

A Mahjong Soul Bot.

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

QingJ © 2025

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