<?xml version="1.0" encoding="UTF-8"?>
<Module>
<ModulePrefs 
  title="Tents Puzzle" 
  height="100"
  width="256"
  scrolling="false"
  description="A logic puzzle where you have to infer the locations of tents in a camp site."
  author="Ralph Becket"
  author_email="ralphbecket.gadgets+tents@gmail.com"
  author_location="Melbourne, Australia"
  screenshot="http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_screenshot.jpg"
  thumbnail="http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_tent.png"
>
  <Require feature="tabs"/>
  <Require feature="dynamic-height"/>
</ModulePrefs>
<Content type="html"><![CDATA[

  <style>
    table.tents_board td {
      width: 8%;
      height: 8%;
      text-align: center;
      vertical-align: middle;
      background-color: #dddddd;
    }
    
    table.tents_board img {
      width: 100%;
      height: 100%;
      align: middle;
    }

    table.tents_board td.inconsistent {
      background-color: #ffcccc;
    }
  </style>
  
  <div id="tents_help" style="display:none">
    <h3>Tents</h3>
    <p>(C) Ralph Becket, 2009
    <p>Find the locations of all the tents.
    <ul>
      <li> Clicking on a square changes it from empty to grass to tent to
        empty (you can't change trees).
      <li> Every tree has a tent immediately North, South, East, or West.
      <li> The one-square border around a tent cannot contain another tent.
      <li> The number of tents in each row and column are shown along the edges.
    </ul>
  </div>
  
  <div id="tents_buttons"></div>
  
  <br>
    
  <div id="tents_goal">
    <small>
    Place a tent right above, below, or to the side of each tree 
    such that it is not adjacent to any other tent.
    </small>
  </div>

  <br>
        
  <div id="tents_puzzle"></div>
  
  <script type="text/javascript"
    src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js">
  </script>      
  
  <script type="text/javascript">

    // The tents puzzle involves finding the locations of some tents on
// an NxN grid, given only the locations of trees (each tent is
// immediately north, south, east, or west of a tree and there is a
// one-to-one correspondence between tents and trees) and the row
// and column sums showing how many tents there are in each row and
// column respectively.  Tents cannot be placed adjacent to each
// other, even diagonally.

// -------- Utilities --------

function ELT(x)             { return $(document.createElement(x)); }
function IMG(src)           { return ELT("img").attr("src", src); }
function TD()               { return ELT("td"); }
function TR()               { return ELT("tr"); }
function TABLE()            { return ELT("table"); }
function TEXT(txt)          { return ELT("div").html(txt); }
function BUTTON(id, lbl)    { return ELT("button").attr("id", id).html(lbl); }

// -------- The puzzle --------

// An enumeration of the various things that can occupy a grid square.
//
var nthings = 0;
var empty = nthings++;  // empty must be 0
var grass = nthings++;
var tree  = nthings++;
var tent  = nthings++;

// An enumeration of the cardinal directions.
//
var ndirs = 0;
var north = ndirs++;
var south = ndirs++;
var east  = ndirs++;
var west  = ndirs++;

// rnd(a, b) returns a random integer in the interval [a, b].
//
function rnd(a, b) {
    return a + Math.floor((1 + b - a) * Math.random());
}

// Convert the puzzle to an HTML PRE element.
//
function boardToPre(a) {

    var N = a.size;
    var z = "<pre>\n";

    // Show the trees, tents, grass, etc.
    //
    for (var r = 1; r <= N; r++) {
        for (var c = 1; c <= N; c++) {
            switch (a[r][c]) {
                case grass: z += "' "; break;
                case tree:  z += "^ "; break;
                case tent:  z += "# "; break;
                default:    z += ". "; break;
            }
        }
        // Append the row sum to each row.
        z += " " + a.row_target[r] + "\n";
    }
    // Append the column sums.
    z += "\n";
    for (var c = 1; c <= N; c++) {
        z += a.col_target[c] + " ";
    }
    z += "\n</pre>";

    // Create an HTML element to hold the result.
    //
    var elt = document.createElement("div");
    elt.innerHTML = z;

    return elt;
}

// Convert a board square to an image item.
//
function squareToImgSrc(a, r, c) {
    switch (a[r][c]) {
        case grass: return "http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_grass.png";
        case tree:  return "http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_tree.png";
        case tent:  return "http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_tent.png";
        default:    return "http://hosting.gmodules.com/ig/gadgets/file/100039392818962997713/tents_empty.png";
    }
}

// Convert the puzzle to an HTML TABLE.
//
function boardToTable(a) {

    var N, table, tr, td;

    N = a.size;
    table = TABLE().addClass("tents_board");

    // Show the trees, tents, grass, etc.
    //
    for (var r = 1; r <= N; r++) {
        tr = TR();
        for (var c = 1; c <= N; c++) {
            td = TD()
                .attr("id", "square_" + r + "_" + c)
                .append(ELT("img").attr("src", squareToImgSrc(a, r, c)));
            tr.append(td);
        }
        // Append the row sum to each row.
        //
        td = TD()
            .attr("id", "row_" + r)
            .html(a.row_target[r]);
        tr.append(td);
        table.append(tr);
    }
    // Append the column sum to each column.
    //
    tr = TR();
    for (var c = 1; c <= N; c++) {
        td = TD()
            .attr("id", "col_" + c)
            .html(a.col_target[c]);
        tr.append(td);
    }
    table.append(tr);

    return table;
}

// A sparse array giving the locations of tents and trees.
//
// [r][c]           - either empty, grass, tent, or tree.
//                    r and c range over [0, N+1] to simplify edge testing.
// .size            - the length of side of the board.
// .n_row_empty[r]  - the number of empty squares in each row.
// .n_col_empty[c]  - the number of empty squares in each column.
// .n_row_tents[r]  - the number of tents in each row.
// .n_col_tents[c]  - the number of tents in each column.
// .row_target[r]   - the number of tents required in each row.
// .col_target[c]   - the number of tents required in each column.
//
function newBoard(N) {

    var a = [];

    a.size = N;

    for (var r = 0; r <= N+1; r++) {
        a[r] = [];
        for (var c = 0; c <= N+1; c++) {
            a[r][c] = empty;
        }
    }

    a.n_row_empty = [];
    a.n_col_empty = [];
    a.n_row_tents = [];
    a.n_col_tents = [];
    a.row_target = [];
    a.col_target = [];
    for (var i = 1; i <= N; i++) {
        a.n_row_empty[i] = 0;
        a.n_col_empty[i] = 0;
        a.n_row_tents[i] = 0;
        a.n_col_tents[i] = 0;
        a.row_target[i] = 0;
        a.col_target[i] = 0;
    }

    return a;
}

// Construct the puzzle by placing tents at random.  
//
function newPuzzle(N, ntents) {

    var a, nspaces;

    a = newBoard(N);

    nspaces = N * N + 1;

    for (var r = 1; r <= N; r++) {

        for (var c = 1; c <= N; c++) {

            nspaces--;

            if (0 < nspaces && Math.random() < (2 * ntents / nspaces)) {

                // Check this space hasn't got a tree in it.

                if (a[r][c]     != empty) continue;

                // Check there are no nearby tents.

                if (a[r-1][c-1] == tent)  continue;
                if (a[r-1][c  ] == tent)  continue;
                if (a[r-1][c+1] == tent)  continue;
                if (a[r  ][c-1] == tent)  continue;

                // Find a place to put a tree.

                var spaces = [];
                if (1 < r && a[r-1][c  ] == empty) spaces.push(north);
                if (1 < c && a[r  ][c-1] == empty) spaces.push(west);
                if (c < N && a[r  ][c+1] == empty) spaces.push(east);
                if (r < N && a[r+1][c  ] == empty) spaces.push(south);
                if (spaces.length == 0)   continue;

                // Place the tree and tent.

                a[r][c] = tent;
                switch(spaces[rnd(0, spaces.length - 1)]) {
                    case north: a[r-1][c  ] = tree; break;
                    case west:  a[r  ][c-1] = tree; break;
                    case east:  a[r  ][c+1] = tree; break;
                    case south: a[r+1][c  ] = tree; break;
                }
                ntents--;
            }
        }
    }

    // Calculate the row and column sums.
    //
    for (var r = 1; r <= N; r++) {
        for (var c = 1; c <= N; c++) {
            if (a[r][c] == tent) {
                a.row_target[r]++;
                a.col_target[c]++;
            }
        }
    }

    // Any empty square not adjacent to a tree has to be grass.  It's just
    // annoying for the player to have to fill these in.
    //
    for (var r = 1; r <= N; r++) {
        for (var c = 1; c <= N; c++) {
            if ( (a[r][c]   == empty ) &&
                 (a[r-1][c] != tree  ) &&
                 (a[r][c-1] != tree  ) &&
                 (a[r+1][c] != tree  ) &&
                 (a[r][c+1] != tree  )
            ) {
                a[r][c] = grass;
            }
        }
    }

    // Now remove the tents: they're for the player to find.
    //
    for (var r = 1; r <= N; r++) {
        for (var c = 1; c <= N; c++) {
            if (a[r][c] == tent) a[r][c] = empty;
        }
    }

    // Automatically fill in any empty squares in zero sum rows or columns with
    // grass.
    //
    for (var i = 1; i <= N; i++) {
        if (a.row_target[i] == 0) {
            for (var j = 1; j <= N; j++) {
                if (a[i][j] == empty) a[i][j] = grass;
            }
        }
        if (a.col_target[i] == 0) {
            for (var j = 1; j <= N; j++) {
                if (a[j][i] == empty) a[j][i] = grass;
            }
        }
    }

    // Calculate the row and column empty sums.
    //
    for (var r = 1; r <= N; r++) {
        for (var c = 1; c <= N; c++) {
            if (a[r][c] == empty) {
                a.n_row_empty[r]++;
                a.n_col_empty[c]++;
            }
        }
    }

    return a;
}

// Returns true iff a[r][c] is consistent.
// - Grass and empty squares are always in a consistent state.
// - A tree is inconsistent if none of its four cardinal neighbours is empty
// or a tent.
// - A tent is inconsistent if either none of its four cardinal neighbours is
// a tree or if one of its eight neighbours is another tent.
//
function squareIsConsistent(a, r, c) {

    var N = a.size;

    switch (a[r][c]) {

        case empty:
            return true;

        case grass:
            return true;

        case tree:
            return  (a[r-1][c] == tent) || (1 < r && a[r-1][c] == empty) ||
                    (a[r][c+1] == tent) || (c < N && a[r][c+1] == empty) ||
                    (a[r+1][c] == tent) || (r < N && a[r+1][c] == empty) ||
                    (a[r][c-1] == tent) || (1 < c && a[r][c-1] == empty);

        case tent:
            return  (a[r-1][c-1] != tent) &&
                    (a[r-1][c  ] != tent) &&
                    (a[r-1][c+1] != tent) &&
                    (a[r  ][c+1] != tent) &&
                    (a[r+1][c+1] != tent) &&
                    (a[r+1][c  ] != tent) &&
                    (a[r+1][c-1] != tent) &&
                    (a[r  ][c-1] != tent) &&
                    (   (a[r-1][c] == tree) ||
                        (a[r][c+1] == tree) ||
                        (a[r+1][c] == tree) ||
                        (a[r][c-1] == tree)
                    );

    }
}

// A row is consistent if
// - it does not have too many tents
// - the number of tents plus the number of empty spaces is not less than
// the row sum.
//
// Likewise for columns.
//
function rowIsConsistent(a, r) {

    return  ( a.n_row_tents[r] <= a.row_target[r] ) &&
            ( a.n_row_tents[r] + a.n_row_empty[r] >= a.row_target[r] );

}

function colIsConsistent(a, c) {

    return  ( a.n_col_tents[c] <= a.col_target[c] ) &&
            ( a.n_col_tents[c] + a.n_col_empty[c] >= a.col_target[c] );

}

function rowIsSolved(a, r) {

    return a.n_row_tents[r] == a.row_target[r];

}

function colIsSolved(a, c) {

    return a.n_col_tents[c] == a.col_target[c];

}

// Change the contents of a square.
//
function changeSquare(a, r, c) {
    switch (a[r][c]) {

        case empty:
            a[r][c] = grass;
            a.n_row_empty[r]--;
            a.n_col_empty[c]--;
            break;

        case grass:
            a[r][c] = tent;
            a.n_row_tents[r]++;
            a.n_col_tents[c]++;
            break;

        case tent:
            a[r][c] = empty;
            a.n_row_tents[r]--;
            a.n_col_tents[c]--;
            a.n_row_empty[r]++;
            a.n_col_empty[c]++;
            break;

    }
}

// -------- UI --------

function UIBoardClick(event, a) {

    var table, td, id, id_parts, r, c, N;

    td = $(event.target).closest("td");
    table = $(td).closest("table");
    id = td.attr("id");
    id_parts = id.split("_");
    N = a.size;

    if (id_parts.length == 3 && id_parts[0] == "square") {

        // We've clicked on a square.  Change it and update the consistency
        // status of its neighbours.
        //
        r = parseInt(id_parts[1]);
        c = parseInt(id_parts[2]);
        changeSquare(a, r, c);

        // Update the square image.
        //
        $("img", td).attr("src", squareToImgSrc(a, r, c));

        // Mark any inconsistent squares.
        //
        for (var rr = r-1; rr <= r+1; rr++) {
            for (var cc = c-1; cc <= c+1; cc++) {
                if (0 < rr && rr <= N && 0 < cc && cc <= N) {
                    td = $("td#square_" + rr + "_" + cc, table);
                    if (squareIsConsistent(a, rr, cc)) {
                        td.removeClass("inconsistent");
                    } else {
                        td.addClass("inconsistent");
                    }
                }
            }
        }
        // Mark any inconsistent row or column sums.
        //
        td = $("td#row_" + r, table);
        if (rowIsConsistent(a, r)) {
            td.removeClass("inconsistent");
        } else {
            td.addClass("inconsistent");
        }
        td = $("td#col_" + c, table);
        if (colIsConsistent(a, c)) {
            td.removeClass("inconsistent");
        } else {
            td.addClass("inconsistent");
        }
        // See if we've reached a solution.
        //
        if ($(".inconsistent", table).length == 0) {
            var solved = true;
            for (var i = 1; i <= N; i++) {
                solved = solved && rowIsSolved(a, i);
                solved = solved && colIsSolved(a, i);
            }
            if (solved) {
                alert("Congratulations, you've solved the puzzle.");
            }
        }
    }

}

function UINewPuzzle(N, ntents) {
    var a = newPuzzle(N, ntents);
    $("#tents_puzzle")
        .html(
            boardToTable(a)
                .click(function (event) { UIBoardClick(event, a) })
        );
    gadgets.window.adjustHeight();
}

function UIInit() {
    $("#tents_buttons")
        .append(
            BUTTON("tents_new_8x8", "New 8x8")
                .click(function () {UINewPuzzle(8, 16)})
        )
        .append(
            BUTTON("tents_new_10x10", "New 10x10")
                .click(function () {UINewPuzzle(10, 20)})
        )
        .append(
            BUTTON("tents_new_12x12", "New 12x12")
                .click(function () {UINewPuzzle(12, 24)})
        );
}

// -------- Entry point --------
//
$(UIInit);
  </script>

]]></Content>
</Module>
