1

I am writing some mahjong-related functions in JavaScript.

Here is what I have below, with code for test cases.

Note that mahjong hands are represented by arrays, with:

  • element 0 being the total number of tiles in the hand
  • elements 1 through 34 being the number of tiles of each type in the hand
    • first craks, then dots, then bams, then winds, and finally dragons

The function to find waits runs really slow. How can I speed it up?

// tiles are indexed as follows:
// 1..9 = 1 crak .. 9 crak
// 10..18 = 1 dot .. 9 dot
// 19..27 = 1 bam .. 9 bam
// 28..34 = east, south, west, north, white, green, red

var wall = new Array();
set_up_wall();

function set_up_wall() {
  for (var i=1; i<=34; i++) wall[i] = 4;
  wall[0]=136;
}

// draw tile from wall
function draw() {
  var fudge = 1-(1e-14);
  var n = Math.floor(Math.random()*wall[0]*fudge);
  var i = 1;
  while (n>=wall[i]) n-=wall[i++];
  wall[i]--;
  wall[0]--;
  return i;
}

// get number of a tile (or 0 if honor)
// e.g. 8 bams = 8
function tilenum(i) {
  if (i>27) return 0;
  if (i%9==0) return 9;
  return i%9;
}

// get suit of a tile (or 0 if honor)
function tilesuit(i) {
  var eps = 1e-10;
  return Math.ceil(i/9-eps)%4;
}

// is this a well-formed hand?
function well_formed(h) {
  // this function is recursive
  if (h[0]==2) return only_pairs(h);
  if (h[0]==14) {
    if (only_pairs(h)) return true;
    if (thirteen_orphans(h)) return true;
  }
  if (h[0]%3 != 2) return false; // wrong no. of tiles in hand
  // now we start splitting up the hand
  // look for three of a kind
  for (var i=1; i<=34; i++) {
    if (h[i]>=3) {
      // create new hand minus the three of a kind
      hh = new Array();
      for (var j=0; j<=34; j++) hh[j]=h[j];
      hh[0]-=3;
      hh[i]-=3;
      if (well_formed(hh)) return true;
    }
  }
  // look for a run of three
  for (var i=1; i<=25; i++) {
    if (tilenum(i)<=7) {
      if (h[i]>=1 && h[i+1]>=1 && h[i+2]>=1) {
      // create new hand minus the run
      hh = new Array();
      for (var j=0; j<=34; j++) hh[j]=h[j];
      hh[0]-=3;
      hh[i]--; hh[i+1]--; hh[i+2]--;
      if (well_formed(hh)) return true;
      }
    }
  }
  // if we reach here, we have exhausted all possibilities
  return false;
}

// is this hand all pairs?
function only_pairs(h) {
  for (var i=1; i<=34; i++) if (h[i]==1 || h[i]>=3) return false;
  return true;
}

// thirteen orphans?
function thirteen_orphans(h) {
  var d=0;
  var c=new Array(14, 1,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,1);
  for (var i=0; i<=34; i++) {
    if (c[i]==0 && h[i]>0) return false;
    if (h[i]!=c[i]) d++;
  }
  return d==1;
}

// this is inefficient
function waits(h) {
  var w=new Array();
  for (var j=0; j<=34; j++) w[j]=false;  
  if (h[0]%3!=1) return w; // wrong no. of tiles in hand
  // so we don't destroy h
  var hh = new Array();
  for (var j=0; j<=34; j++) hh[j]=h[j];
  for (var i=1; i<=34; i++) {
    // add the tile we are trying to test
    hh[0]++; hh[i]++;
    if (hh[i]<5) { // exclude hands waiting for a nonexistent fifth tile
      if (well_formed(hh)) {
        w[0] = true;
        w[i] = true;
      }
    }
    hh[0]--; hh[i]--;
  }
  return w;
}

function tiles_to_string(t) { // strictly for testing purposes
  var n;
  var ss="";
  var s = "x 1c 2c 3c 4c 5c 6c 7c 8c 9c 1d 2d 3d 4d 5d 6d 7d 8d 9d ";
  s += "1b 2b 3b 4b 5b 6b 7b 8b 9b Ew Sw Ww Nw Wd Gd Rd";
  s=s.split(" ");
  for (var i=1; i<=34; i++) {
    n=t[i]*1; // kludge
    while (n--) ss+=(" "+s[i]);
  }
  return ss;
}

// tests

var x;
x = new Array(13, 0,0,0,0,0,1,2,2,2, 2,2,2,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0);
document.write("Hand: "+tiles_to_string(x)+"<br />");
document.write("Waits: "+tiles_to_string(waits(x))+"<br /><br />");
x = new Array(13, 1,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,1, 1,0,0,0,0,0,0,0,1, 1,1,1,1,1,1,1);
document.write("Hand: "+tiles_to_string(x)+"<br />");
document.write("Waits: "+tiles_to_string(waits(x))+"<br /><br />");
x = new Array(13, 0,0,0,0,0,0,0,0,0, 3,1,1,1,1,1,1,1,3, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0);
document.write("Hand: "+tiles_to_string(x)+"<br />");
document.write("Waits: "+tiles_to_string(waits(x))+"<br /><br />");
x = new Array(13, 4,0,0,3,3,3,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0);
document.write("Hand: "+tiles_to_string(x)+"<br />");
document.write("Waits: "+tiles_to_string(waits(x))+"<br /><br />");
x = new Array(13, 0,0,4,3,3,3,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0);
document.write("Hand: "+tiles_to_string(x)+"<br />");
document.write("Waits: "+tiles_to_string(waits(x))+"<br /><br />");
David M
  • 71,481
  • 13
  • 158
  • 186
Robert L
  • 1,963
  • 2
  • 13
  • 11
  • you would probably get more & better responses if you didn't assum that we know what mah-jong is, or what terms like wall, wait, etc. are. And maybe explained you functional requirements at all. – RBarryYoung Dec 18 '09 at 16:02
  • 1
    Two other comments - your well_formed function will miss some hands by grabbing the sets of three (pungs) first. For example, 345, 456, 567 in the same suit would immediately lose all the 5s. Also, you are not considering kongs (four of a kind) here. But perhaps this last one is deliberate? – David M Dec 18 '09 at 16:12
  • As for kongs: I have decided not to deal with them yet, and in any case, a hand with a *declared* kong would for our purposes have only eleven tiles (the ones not part of the kong). And second: As for 345, 456, 567: if I had a hand with 344555667, yes, I would first remove the 555 and then check 344667. Then I would realize that that doesn't work, so then I would try removing 345, be left with 455667, and see that that does work. Kind of like the standard way to solve the "Eight Queens" problem. – Robert L Dec 18 '09 at 16:50

3 Answers3

1

You have an array to hold the contents of the hand, and then you are creating a new array to hold the contents minus a particular set of tiles each time - in a recursive function. Instead of all this array creation, create two arrays - one to hold the hand under consideration, the other to hold the tiles from the hand that you have already considered - and just pass them both around. So this:

hh = new Array();
for (var j=0; j<=34; j++) hh[j]=h[j];
hh[0]-=3;
hh[i]-=3;
if (well_formed(hh)) return true;

becomes this:

h[0]-=3;
h[i]-=3;
hc[0]+=3;
hc[i]+=3;
if (well_formed(h,hc)) return true;

You pass both h and hc around, and bear in mind that to reconstruct the whole hand, you need to add the two arrays. But this can come right at the end of considering whether or not the hnd is complete.

EDIT: Here is what I mean in more detail: EDIT: Now working I hope... typo in first attempt.

// is this a well-formed hand?
function well_formed(h) {
  hc = new Array();
  for (var i=0; i<=34; i++) hc[i]=0;
  result = well_formed_recursive(h, hc);
  for (var i=0; i<=34; i++) h[i]+=hc[i];
  return result;
}

function well_formed_recursive(h, hc) {
  // this function is recursive
  if (h[0]==2) return only_pairs(h);
  if (h[0]==14) {
    if (only_pairs(h)) return true;
    if (thirteen_orphans(h)) return true;
  }
  if (h[0]%3 != 2) return false; // wrong no. of tiles in hand
  // now we start splitting up the hand
  // look for three of a kind
  for (var i=1; i<=34; i++) {
    if (h[i]>=3) {
      h[0]-=3;
      h[i]-=3;
      hc[0]+=3;
      hc[i]+=3;
      if (well_formed_recursive(h,hc)) return true;
    }
  }
  // look for a run of three
  for (var i=1; i<=25; i++) {
    if (tilenum(i)<=7) {
      if (h[i]>=1 && h[i+1]>=1 && h[i+2]>=1) {
        h[0]-=3;
        h[i]--; h[i+1]--; h[i+2]--;
        hc[0]+=3;
        hc[i]++; hc[i+1]++; hc[i+2]++;
        if (well_formed_recursive(h,hc)) return true;
      }
    }
  }
  // if we reach here, we have exhausted all possibilities
  return false;
}
David M
  • 71,481
  • 13
  • 158
  • 186
  • Nice, except in JavaScript, arrays are passed by reference, not by value, aren't they? – Robert L Dec 18 '09 at 15:57
  • Hence my comment that you will need to add them back up at the end. Effectively, hc tracks the changes you are temporarily making to h to make them reversible. – David M Dec 18 '09 at 15:59
  • So, in fact suggest you make well_formed(h) internall call a recursive function well_formed_recursive(h, hc), and then reconstruct h when the call finally returns. – David M Dec 18 '09 at 15:59
  • There is a typo in the fourth line inside the function. But I'll fix it and then try it... – Robert L Dec 18 '09 at 16:24
  • Yes, there was - couple of others as well. Suggest you try the current version. – David M Dec 18 '09 at 16:28
  • It didn't work. For example, on that Nine Gates hand, it only got one of the nine waits. – Robert L Dec 18 '09 at 16:38
0

To duplicate an array use the concat function.

var a=[1,2,3,4];
var b=a.concat();
Patrick
  • 15,702
  • 1
  • 39
  • 39
0

Two things wrong performance-wise, that I can see.

First is what David M has already noted: stop copying the whole array every time you recurse in well_formed(), just add in the changes before you recurse and back out the additions when you return, just as you did in your waits() function.

Second is that in well_formed() you keep rescanning the whole array every time you make a single incremental change to the hand. That's inherently inefficient, instead you should look for opportunities to keep "state counters" instead.

For instance you could easily check for only_pairs() if you always know how many pairs you had. Instead of scanning the hand() array for any non-pairs, you keep a pairs_counter as part of the array (or its associated context), whenever you add a card to hand[i] then you check if hand[i]=2 you increment the pairs counter if it's 3, you decrement it. Likewise when you remove a card, if hand[j] = 2 you increment the pairs counter, but if it equals 1 your decrement it.

There are a number of places where you can employ this strategy and it will have a big impact on your performance.

RBarryYoung
  • 55,398
  • 14
  • 96
  • 137
  • Hmm, I'm not sure what *A∀A∀A∀A∀A∀A* means? (Heck, I'm not even sure how you typed it in... ;-) ). And yes, sometimes the state-counter trick is not too easy, but in this case, I think that there are several opportunities that are. I already pointed out one of them. – RBarryYoung Dec 20 '09 at 15:04