diff --git a/sudoku/class.Sudoku.php b/sudoku/class.Sudoku.php index b586c57..f6da5ca 100755 --- a/sudoku/class.Sudoku.php +++ b/sudoku/class.Sudoku.php @@ -1,2181 +1,1972 @@ - - * @copyright copyright @ 2005 by Dick Munroe, Cottage Software Works, Inc. - * @license http://www.csworks.com/publications/ModifiedNetBSD.html - * @version 2.2.0 - * @package Sudoku - */ - -/** - * Basic functionality needed for ObjectSs in the Sudoku solver. - * - * Technically speaking these aren't restricted to the Sudoku classes - * and are of use generally. - * - * @package Sudoku - */ - -class ObjectS -{ - /** - * @desc Are two array's equal (have the same contents). - * @param array - * @param array - * @return boolean - */ - - function array_equal($theArray1, $theArray2) - { - if (!(is_array($theArray1) && is_array($theArray2))) - { - return false ; - } - - if (count($theArray1) != count($theArray2)) - { - return false ; - } - - $xxx = array_diff($theArray1, $theArray2) ; - - return (count($xxx) == 0) ; - } - - /** - * Deep copy anything. - * - * @access public - * @param array $theArray [optional] Something to be deep copied. [Default is the current - * ObjectS. - * @return mixed The deep copy of the input. All references embedded within - * the array have been resolved into copies allowing things like the - * board array to be copied. - */ - - function deepCopy($theArray = NULL) - { - if ($theArray === NULL) - { - return unserialize(serialize($this)) ; - } - else - { - return unserialize(serialize($theArray)) ; - } - } - - /** - * @desc Debugging output interface. - * @access public - * @param mixed $theValue The "thing" to be pretty printed. - * @param boolean $theHTMLFlag True if the output will be seen in a browser, false otherwise. - */ - - function print_d(&$theValue, $theHTMLFlag = true) - { - print SDD::dump($theValue, $theHTMLFlag) ; - } -} - -/** - * The individual cell on the Sudoku board. - * - * These cells aren't restricted to 9x9 Sudoku (although pretty much everything else - * at the moment). This class provides the state manipulation and searching capabilities - * needed by the inference engine (class RCS). - * - * @package Sudoku - */ - -class Cell extends ObjectS -{ - var $r ; - var $c ; - - var $state = array() ; - var $applied = false ; - - - /** - * @desc Constructor - * @param integer $r row address of this cell (not used, primarily for debugging purposes). - * @param integer $c column address of this cell (ditto). - * @param integer $nStates The number of states each cell can have. Looking forward to - * implementing Super-doku. - */ - - function Cell($r, $c, $nStates = 9) - { - - $this->r = $r ; - $this->c = $c ; - - for ($i = 1; $i <= $nStates; $i++) - { - $this->state[$i] = $i ; - } - } - - /** - * @desc This cell has been "applied", i.e., solved, to the board. - */ - - function applied() - { - $this->applied = true ; - } - - /** - * Only those cells which are not subsets of the tuple have the - * contents of the tuple removed. - * - * @desc apply a 23Tuple to a cell. - * @access public - * @param array $aTuple the tuple to be eliminated. - */ - - function apply23Tuple($aTuple) - { - if (is_array($this->state)) - { - $xxx = array_intersect($this->state, $aTuple) ; - if ((count($xxx) > 0) && (count($xxx) != count($this->state))) - { - return $this->un_set($aTuple) ; - } - else - { - return false ; - } - } - else - { - return false ; - } - } - - /** - * For more details on the pair tuple algorithm, see RCS::_pairSolution. - * - * @desc Remove all values in the tuple, but only if the cell is a superset. - * @access public - * @param array A tuple to be eliminated from the cell's state. - */ - - function applyTuple($aTuple) - { - if (is_array($this->state)) - { - if (!$this->array_equal($aTuple, $this->state)) - { - return $this->un_set($aTuple) ; - } - } - - return false ; - } - - /** - * @desc Return the string representation of the cell. - * @access public - * @param boolean $theFlag true if the intermediate states of the cell are to be visible. - * @return string - */ - - function asString($theFlag = false) - { - if (is_array($this->state)) - { - if (($theFlag) || (count($this->state) == 1)) - { - return implode(", ", $this->state) ; - } - else - { - return " " ; - } - } - else - { - return $this->state ; - } - } - - /** - * Used to make sure that solved positions show up at print time. - * The value is used as a candidate for "slicing and dicing" by elimination in - * Sudoku::_newSolvedPosition. - * - * @desc Assert pending solution. - * @access public - * @param integer $value The value for the solved position. - */ - - function flagSolvedPosition($value) - { - $this->state = array($value => $value) ; - } - - /** - * @desc return the state of a cell. - * @access protected - * @return mixed Either solved state or array of state pending solution. - */ - - function &getState() - { - return $this->state ; - } - - /** - * @desc Has the state of this cell been applied to the board. - * @access public - * @return boolean True if it has, false otherwise. Implies that IsSolved is true as well. - */ - - function IsApplied() - { - return $this->applied ; - } - - /** - * @desc Has this cell been solved? - * @access public - * @return boolean True if this cell has hit a single state. - */ - - function IsSolved() - { - return !is_array($this->state) ; - } - - /** - * This is used primarily by the pretty printer, but has other applications - * in the code. - * - * @desc Return information about the state of a cell. - * @access public - * @return integer 0 => the cell has been solved. - * 1 => the cell has been solved but not seen a solved. - * 2 => the cell has not been solved. - */ - - function solvedState() - { - if (is_array($this->state)) - { - if (count($this->state) == 1) - { - return 1 ; - } - else - { - return 2 ; - } - } - else - { - return 0 ; - } - } - - /** - * This is the negative inference of Sudoku. By eliminating values the - * cells approach solutions. Once a cell has been completely eliminated, - * the value causing the complete elimination must be the solution and the - * cell is promoted into the solved state. - * - * @desc Eliminate one or more values from the state information of the cell. - * @access public - * @param mixed The value or values to be removed from the cell state. - * @return boolean True if the cell state was modified, false otherwise. - */ - - function un_set($theValues) - { - if (is_array($theValues)) - { - $theReturn = FALSE ; - - foreach ($theValues as $theValue) - { - $theReturn |= $this->un_set($theValue) ; - } - - return $theReturn ; - } - - if (is_array($this->state)) - { - $theReturn = isset($this->state[$theValues]) ; - unset($this->state[$theValues]) ; - if (count($this->state) == 0) - { - $this->state = $theValues ; - } - return $theReturn ; - } - else - { - return false ; - } - } -} - -/** - * The individual row column or square on the Sudoku board. - * - * @package Sudoku - */ - -class RCS extends ObjectS -{ - var $theIndex ; - - var $theRow = array() ; - - var $theHeader = "" ; - - var $theTag = "" ; - - /** - * This - * @desc Constructor - * @access public - * @param string $theTag "Row", "Column", "Square", used primarily in debugging. - * @param integer $theIndex 1..9, where is this on the board. Square are numbered top - * left, ending bottom right - * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what - * limts things to 9x9 Sudoku currently. - */ - - function RCS($theTag, $theIndex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) - { - $this->theTag = $theTag ; - $this->theIndex = $theIndex ; - $this->theRow[1] = &$a1 ; - $this->theRow[2] = &$a2 ; - $this->theRow[3] = &$a3 ; - $this->theRow[4] = &$a4 ; - $this->theRow[5] = &$a5 ; - $this->theRow[6] = &$a6 ; - $this->theRow[7] = &$a7 ; - $this->theRow[8] = &$a8 ; - $this->theRow[9] = &$a9 ; - } - - /** - * There is a special case that comes up a lot in Sudoku. If there - * are values i, j, k and cells of the form (i, j), (j, k), (i, j, k) - * the the values i, j, and k cannot appear in any other cells. The - * proof is a simple "by contradiction" proof. Assume that the values - * do occur elsewhere and you always get a contradiction for these - * three cells. I'm pretty sure that this is a general rule, but for - * 9x9 Sudoku, they probably aren't of interested. - * - * @desc - * @access private - * @return boolean True if a 23 solution exists and has been applied. - */ - - function _23Solution() - { - $theCounts = array() ; - $theTuples = array() ; - $theUnsolved = 0 ; - - for ($i = 1; $i <= 9; $i++) - { - $j = count($this->theRow[$i]->getState()); - $theCounts[ $j][] = $i ; - $theUnsolved++ ; - } - - if( array_key_exists( 2, $theCounts) and array_key_exists( 3, $theCounts)) - { - if ((count($theCounts[2]) < 2) || (count($theCounts[3]) < 1)) - return false ; - } - - /* - * Look at each pair of 2 tuples and see if their union exists in the 3 tuples. - * If so, eliminate everything from the set and bail. - */ - - $the2Tuples = &$theCounts[2] ; - $the3Tuples = &$theCounts[3] ; - $theCount2 = count($the2Tuples) ; - $theCount3 = count($the3Tuples) ; - - for ($i = 0; $i < $theCount2 - 1; $i++) - { - for ($j = $i + 1; $j < $theCount2; $j++) - { - $xxx = array_unique(array_merge($this->theRow[$the2Tuples[$i]]->getState(), - $this->theRow[$the2Tuples[$j]]->getState())) ; - for ($k = 0; $k < $theCount3; $k++) - { - if ($this->array_equal($xxx, $this->theRow[$the3Tuples[$k]]->getState())) - { - $theTuples[] = $xxx ; - break ; - } - } - } - } - - /* - * Since it takes 3 cells to construct the 23 tuple, unless there are more than 3 - * unsolved cells, further work doesn't make any sense. - */ - - $theReturn = false ; - - if ((count($theTuples) != 0) && ($theUnsolved > 3)) - { - foreach ($theTuples as $aTuple) - { - foreach($this->theRow as $theCell) - { - $theReturn |= $theCell->apply23Tuple($aTuple) ; - } - } - } - - if ($theReturn) - { - $this->theHeader[] = sprintf("
Apply %s[%d] 23 Tuple Inference:", $this->theTag, $this->theIndex) ; - } - - return $theReturn ; - } - - /** - * @desc apply a tuple to exclude items from within the row/column/square. - * @param array $aTuple the tuple to be excluded. - * @access private - * @return boolean true if anything changes. - */ - - function _applyTuple(&$aTuple) - { - $theReturn = FALSE ; - - for ($i = 1; $i <=9; $i++) - { - $theReturn |= $this->theRow[$i]->applyTuple($aTuple) ; - } - - return $theReturn ; - } - - /** - * This is a placeholder to be overridden to calculate the "coupling" for - * a cell. Coupling is defined to be the sum of the sizes of the intersection - * between this cell and all others in the row/column/square. This provides - * a metric for deciding placement of clues within puzzles. In effect, this - * forces the puzzle generator to select places for new clues depending upon - * how little information is changed by altering the state of a cell. The larger - * the number returned by the coupling, function, the less information is currently - * available for the state of the cell. By selecting areas with the least information - * the clue sets are substantially smaller than simple random placement. - * - * @desc Calculate the coupling for a cell within the row/column/square. - * @access abstract - * @param integer $theRow the row coordinate on the board of the cell. - * @param integer $theColumn the column coordinate on the board of the cell. - * @return integer the degree of coupling between the cell and the rest of the cells - * within the row/column/square. - */ - - function coupling($theRow, $theColumn) - { - return 0 ; - } - - /** - * I think that the goal of the inference engine is to eliminate - * as much "junk" state as possible on each pass. Therefore the - * order of the inferences should be 23 tuple, pair, unique because - * the 23 tuple allows you to eliminate 3 values (if it works), and the - * pair (generally) only 2. The unique solution adds no new information. - * - * @desc Run the inference engine for a row/column/square. - * @access public - * @param array theRow A row/column/square data structure. - * @param string theType A string merged with the standard headers during - * intermediate solution printing. - * @return boolean True when at least one inference has succeeded. - */ - - function doAnInference() - { - $this->theHeader = NULL ; - - $theReturn = $this->_23Solution() ; - $theReturn |= $this->_pairSolution() ; - $theReturn |= $this->_uniqueSolution() ; - - return $theReturn ; - } - - /** - * @desc Find all tuples with the same contents. - * @param array Array of n size tuples. - * @returns array of tuples that appear the same number of times as the size of the contents - */ - - function _findTuples(&$theArray) - { - $theReturn = array() ; - for ($i = 0; $i < count($theArray); $i++) - { - $theCount = 1 ; - - for ($j = $i + 1; $j < count($theArray); $j++) - { - $s1 = &$this->theRow[$theArray[$i]] ; - $s1 =& $s1->getState() ; - - $s2 = &$this->theRow[$theArray[$j]] ; - $s2 =& $s2->getState() ; - - $aCount = count($s1) ; - - if ($this->array_equal($s1, $s2)) - { - $theCount++ ; - - if ($theCount == $aCount) - { - $theReturn[] = $s1 ; - break ; - } - } - } - } - - return $theReturn ; - } - - /** - * @desc Get a reference to the specified cell. - * @access public - * @return reference to ObjectS of class Cell. - */ - - function &getCell($i) - { - return $this->theRow[$i] ; - } - - /** - * @desc Get the header set by the last call to doAnInference. - * - */ - - function getHeader() - { - return $this->theHeader ; - } - - /** - * Turns out if you every find a position of n squares which can only contain - * the same values, then those values cannot appear elsewhere in the structure. - * This is a second positive inference that provides additional negative information. - * Thanks to Ghica van Emde Boas (also an author of a Sudoku class) for convincing - * me that these situations really occurred. - * - * @desc Eliminate tuple-locked alternatives. - * @access private - * @return boolean True if something changed. - */ - - function _pairSolution() - { - $theCounts = array() ; - $theTuples = array() ; - - for ($i = 1; $i <= 9; $i++) - { - $c = &$this->theRow[$i] ; - $theCounts[count($c->getState())][] = $i ; - } - - unset($theCounts[1]) ; - - /* - ** Get rid of any set of counts which cannot possibly meet the - ** requirements. - */ - - $thePossibilities = $theCounts ; - - foreach ($theCounts as $theKey => $theValue) - { - if (count($theValue) < $theKey) - { - unset($thePossibilities[$theKey]) ; - } - } - - if (count($thePossibilities) == 0) - { - return false ; - } - - /* - * At this point there are 1 or more tuples which MAY satisfy the conditions. - */ - - $theReturn = false ; - - foreach ($thePossibilities as $theValue) - { - $theTuples = $this->_findTuples($theValue) ; - - if (count($theTuples) != 0) - { - foreach ($theTuples as $aTuple) - { - $theReturn |= $this->_applyTuple($aTuple) ; - } - } - } - - if ($theReturn) - { - $this->theHeader[] = sprintf("
Apply %s[%d] Pair Inference:", $this->theTag, $this->theIndex) ; - } - - return $theReturn ; - } - - function un_set($theValues) - { - $theReturn = false ; - - for ($i = 1; $i <= 9; $i++) - { - $c = &$this->theRow[$i] ; - $theReturn |= $c->un_set($theValues) ; - } - - return $theReturn ; - } - - /** - * Find a solution to a row/column/square. - * - * Find any unique numbers within the row/column/square under consideration. - * Look through a row structure for a value that appears in only one cell. - * When you find one, that's a solution for that cell. - * - * There is a second inference that can be taken. Given "n" cells in a row/column/square - * and whose values can only consist of a set of size "n", then those values may obtain - * there and ONLY there and may be eliminated from consideration in the rest of the set. - * For example, if two cells must contain the values 5 or 6, then no other cell in that - * row/column/square may contain those values, similarly for 3 cells, etc. - * - * @access private - * @return boolean True if one or more values in the RCS has changed state. - */ - - function _uniqueSolution() - { - $theSet = array() ; - - for ($i = 1; $i <= 9; $i++) - { - $c = &$this->theRow[$i] ; - if (!$c->IsSolved()) - { - foreach ($c->getState() as $theValue) - { - $theSet[$theValue][] = $i ; - } - } - } - - /* - * If there were no unsolved positions, then we're done and nothing has - * changed. - */ - - if (count($theSet) == 0) - { - return false ; - } - - /* - * Pull out all those keys having only one occurrance in the RCS. - */ - - foreach ($theSet as $theKey => $theValues) - { - if (count($theValues) != 1) - { - unset($theSet[$theKey]) ; - } - } - - /* - * If there aren't any unique values, we're done. - */ - - if (count($theSet) == 0) - { - return false ; - } - - foreach ($theSet as $theValue => $theIndex) - { - $this->theRow[$theIndex[0]]->flagSolvedPosition($theValue) ; - } - - $this->theHeader[] = sprintf("
Apply %s[%d] Unique Inference:", $this->theTag, $this->theIndex) ; - - return true ; - } - - /** - * @desc Check to see if the RCS contains a valid state. - * @access public - * @return boolean True if the state of the RCS could be part of a valid - * solution, false otherwise. - */ - - function validateSolution() - { - $theNewSet = array() ; - - foreach ($this->theRow as $theCell) - { - if ($theCell->solvedState() == 0) - { - $theNewSet[] = $theCell->getState() ; - } - } - - $xxx = array_unique($theNewSet) ; - - return (count($xxx) == count($this->theRow)) ; - } - - /** - * Validate a part of a trial solution. - * - * Check a row/column/square to see if there are any invalidations on this solution. - * Only items that are actually solved are compared. This is used during puzzle - * generation. - * - * @access public - * @return True if the input parameter contains a valid solution, false otherwise. - */ - - function validateTrialSolution() - { - $theNewSet = array() ; - - foreach($this->theRow as $theCell) - { - if ($theCell->solvedState() == 0) - { - $theNewSet[] = $theCell->getState() ; - } - } - - $xxx = array_unique($theNewSet) ; - - return ((count($xxx) == count($theNewSet) ? TRUE : FALSE)) ; - } -} - -/** - * Row ObjectS. - * - * @package Sudoku - */ - -class R extends RCS -{ - /** - * @desc Constructor - * @access public - * @param string $theTag "Row", "Column", "Square", used primarily in debugging. - * @param integer $theIndex 1..9, where is this on the board. Square are numbered top - * left, ending bottom right - * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what - * limts things to 9x9 Sudoku currently. - */ - - function R($theTag, $theIndex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) - { - $this->RCS($theTag, $theIndex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9) ; - } - - /** - * @see RCS::coupling - */ - - function coupling($theRow, $theColumn) - { - return $theState = $this->_coupling($theColumn) ; - } - - /** - * @see RCS::coupling - * @desc Heavy lifting for row/column coupling calculations. - * @access private - * @param integer $theIndex the index of the cell within the row or column. - * @return integer the "coupling coefficient" for the cell. The sum of the - * sizes of the intersection between this and all other - * cells in the row or column. - */ - - function _coupling($theIndex) - { - $theCommonState =& $this->getCell($theIndex) ; - $theCommonState =& $theCommonState->getState() ; - - $theCoupling = 0 ; - - for ($i = 1; $i <= count($this->theRow); $i++) - { - if ($i != $theIndex) - { - $theCell =& $this->getCell($i) ; - if ($theCell->solvedState() != 0) - { - $theCoupling += count(array_intersect($theCommonState, $theCell->getState())) ; - } - } - } - - return $theCoupling ; - } -} - -/** - * The column ObjectS. - * - * @package Sudoku - */ - -class C extends R -{ - /** - * @desc Constructor - * @access public - * @param string $theTag "Row", "Column", "Square", used primarily in debugging. - * @param integer $theIndex 1..9, where is this on the board. Square are numbered top - * left, ending bottom right - * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what - * limts things to 9x9 Sudoku currently. - */ - - function C($theTag, $theIndex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) - { - $this->R($theTag, $theIndex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9) ; - } - - /** - * @see R::coupling - */ - - function coupling($theRow, $theColumn) - { - return $theState = $this->_coupling($theRow) ; - } - -} - -/** - * The Square ObjectS. - * - * @package Sudoku - */ - -class S extends RCS -{ - /** - * The cells within the 3x3 sudoku which participate in the coupling calculation for a square. - * Remember that the missing cells have already participated in the row or column coupling - * calculation. - * - * @access private - * @var array - */ - - var $theCouplingOrder = - array( 1 => array(5, 6, 8, 9), - 2 => array(4, 6, 7, 9), - 3 => array(4, 5, 7, 8), - 4 => array(2, 3, 8, 9), - 5 => array(1, 3, 7, 9), - 6 => array(1, 2, 7, 8), - 7 => array(2, 3, 5, 6), - 8 => array(1, 3, 4, 6), - 9 => array(1, 2, 4, 5)) ; - - /** - * @desc Constructor - * @access public - * @param string $theTag "Row", "Column", "Square", used primarily in debugging. - * @param integer $theIndex 1..9, where is this on the board. Square are numbered top - * left, ending bottom right - * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what - * limts things to 9x9 Sudoku currently. - */ - - function S($theTag, $theIndex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) - { - $this->RCS($theTag, $theIndex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9) ; - } - - /** - * @see RCS::coupling - */ - - function coupling($theRow, $theColumn) - { - $theIndex = ((($theRow - 1) % 3) * 3) + (($theColumn - 1) % 3) + 1 ; - $theCommonState =& $this->getCell($theIndex) ; - $theCommonState =& $theCommonState->getState() ; - - $theCoupling = 0 ; - - foreach ($this->theCouplingOrder[$theIndex] as $i) - { - $theCell =& $this->getCell($i) ; - if ($theCell->solvedState() != 0) - { - $theCoupling += count(array_intersect($theCommonState, $theCell->getState())) ; - } - } - - return $theCoupling ; - } -} - -/** - * Solve and generate Sudoku puzzles. - * - * Solve and generate Sudoku. A simple output interface is provided for - * web pages. The primary use of this class is as infra-structure for - * Sudoku game sites. - * - * The solver side of this class (solve) relies on the usual characteristic - * of logic puzzles, i.e., at any point in time there is one (or more) - * UNIQUE solution to some part of the puzzle. This solution can be - * applied, then iterated upon to find the next part of the puzzle. A - * properly constructed Sudoku can have only one solution which guarangees - * that this is the case. (Sudoku with multiple solutions will always - * require guessing at some point which is specifically disallowed by - * the rules of Sudoku). - * - * While the solver side is algorithmic, the generator side is much more - * difficult and, in fact, the generation of Sudoku appears to be NP - * complete. That being the case, I observed that most successful - * generated initial conditions happened quickly, typically with < 40 - * iterations. So the puzzle generator runs "for a while" until it - * either succeeds or doesn't generated a solveable puzzle. If we get - * to that position, I just retry and so far I've always succeeded in - * generating an initial state. Not guarateed, but in engineering terms - * "close enough". - * - * @package Sudoku - * @example ./example.php - * @example ./example1.php - * @example ./example2.php - * @example ./example3.php -*/ - -class Sudoku extends ObjectS -{ - /** - * An array of Cell ObjectSs, organized into rows and columns. - * - * @access private - * @var array of ObjectSs of type Cell. - */ - - var $theBoard = array() ; - - /** - * True if debugging output is to be provided during a run. - * - * @access private - * @var boolean - */ - - var $theDebug = FALSE ; - - /** - * An array of RCS ObjectSs, one ObjectS for each row. - * - * @access private - * @var ObjectS of type R - */ - - var $theRows = array() ; - - /** - * An array of RCS ObjectSs, one ObjectS for each Column. - * - * @access private - * @var ObjectS of type C - */ - - var $theColumns = array() ; - - /** - * An array of RCS ObjectSs, one ObjectS for each square. - * - * @access private - * @var ObjectS of type S - */ - - var $theSquares = array() ; - - /** - * Used during puzzle generation for debugging output. There may - * eventually be some use of theLevel to figure out where to stop - * the backtrace when puzzle generation fails. - * - * @access private - * @var integer. - */ - - var $theLevel = 0 ; - - /** - * Used during puzzle generation to determine when the generation - * will fail. Failure, in this case, means to take a LONG time. The - * backtracing algorithm used in the puzzle generator will always find - * a solution, it just might take a very long time. This is a way to - * limit the damage before taking another guess. - * - * @access private - * @var integer. - */ - - var $theMaxIterations = 50 ; - - /** - * Used during puzzle generation to limit the number of trys at - * generation a puzzle in the event puzzle generation fails - * (@see Suduko::$theMaxIterations). I've never seen more than - * a couple of failures in a row, so this should be sufficient - * to get a puzzle generated. - * - * @access private - * @var integer. - */ - - var $theTrys = 10 ; - - /** - * Used during puzzle generation to count the number of iterations - * during puzzle generation. It the number gets above $theMaxIterations, - * puzzle generation has failed and another try is made. - * - * @access private - * @var integer. - */ - - var $theGenerationIterations = 0 ; - - function Sudoku($theDebug = FALSE) - { - $this->theDebug = $theDebug ; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - $this->theBoard[$i][$j] = new Cell($i, $j) ; - } - } - - $this->_buildRCS() ; - } - - /** - * Apply a pending solved position to the row/square/column. - * - * At this point, the board has been populated with any pending solutions. - * This applies the "negative" inference that no row, column, or square - * containing the value within the cell. - * - * @access private - * @param integer $row The row of the board's element whose value is now fixed. - * @param integer $col The column of the board's element whose value is now fixed. - */ - - function _applySolvedPosition($row, $col) - { - $theValue = $this->theBoard[$row][$col]->getState() ; - - /* - ** No other cell in the row, column, or square can take on the value "value" any longer. - */ - - $i = (((int)(($row - 1) / 3)) * 3) ; - $i = $i + ((int)(($col - 1) / 3)) + 1 ; - - $this->theRows[$row]->un_set($theValue) ; - - $this->theColumns[$col]->un_set($theValue) ; - - $this->theSquares[$i]->un_set($theValue) ; - } - - /** - * @desc Apply all pending solved positions to the board. - * @access private - * @return boolean True if at least one solved position was applied, false - * otherwise. - */ - - function _applySolvedPositions() - { - $theReturn = false ; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - if (!$this->theBoard[$i][$j]->IsApplied()) - { - if ($this->theBoard[$i][$j]->solvedState() == 0) - { - $this->_applySolvedPosition($i, $j) ; - - /* - ** Update the solved position matrix and make sure that the board actually - ** has a value in place. - */ - - $this->theBoard[$i][$j]->applied() ; - $theReturn = TRUE ; - } - } - } - } - - return $theReturn ; - } - - /** - * @desc build the row/column/square structures for the board. - * @access private - */ - - function _buildRCS() - { - for ($i = 1; $i <= 9; $i++) - { - $this->theRows[$i] = - new R("Row", - $i, - $this->theBoard[$i][1], - $this->theBoard[$i][2], - $this->theBoard[$i][3], - $this->theBoard[$i][4], - $this->theBoard[$i][5], - $this->theBoard[$i][6], - $this->theBoard[$i][7], - $this->theBoard[$i][8], - $this->theBoard[$i][9]) ; - $this->theColumns[$i] = - new C("Column", - $i, - $this->theBoard[1][$i], - $this->theBoard[2][$i], - $this->theBoard[3][$i], - $this->theBoard[4][$i], - $this->theBoard[5][$i], - $this->theBoard[6][$i], - $this->theBoard[7][$i], - $this->theBoard[8][$i], - $this->theBoard[9][$i]) ; - - $r = ((int)(($i - 1) / 3)) * 3 ; - $c = (($i - 1) % 3) * 3 ; - - $this->theSquares[$i] = - new S("Square", - $i, - $this->theBoard[$r + 1][$c + 1], - $this->theBoard[$r + 1][$c + 2], - $this->theBoard[$r + 1][$c + 3], - $this->theBoard[$r + 2][$c + 1], - $this->theBoard[$r + 2][$c + 2], - $this->theBoard[$r + 2][$c + 3], - $this->theBoard[$r + 3][$c + 1], - $this->theBoard[$r + 3][$c + 2], - $this->theBoard[$r + 3][$c + 3]) ; - } - } - - /** - * Seek alternate solutions in a solution set. - * - * Given a solution, see if there are any alternates within the solution. - * In theory this should return the "minimum" solution given any solution. - * - * @access public - * @param array $theInitialState (@see Sudoku::initializePuzzleFromArray) - * @return array A set of triples containing the minimum solution. - */ - - function findAlternateSolution($theInitialState) - { - $j = count($theInitialState) ; - - for ($i = 0; $i < $j; $i++) - { - $xxx = $theInitialState ; - - $xxx = array_splice($xxx, $i, 1) ; - - $this->Sudoku() ; - - $this->initializePuzzleFromArray($xxx) ; - - if ($this->solve()) - { - return $this->findAlternateSolution($xxx) ; - } - } - - return $theInitialState ; - } - - /** - * Initialize Sudoku puzzle generation and generate a puzzle. - * - * Turns out that while the solution of Sudoku is mechanical, the creation of - * Sudoku is an NP-Complete problem. Which means that I can use the inference - * engine to help generate puzzles, but I need to test the solution to see if - * I've gone wrong and back up and change my strategy. So something in the - * recursive descent arena will be necessary. Since the generation can take - * a long time to force a solution, it's easier to probe for a solution - * if you go "too long". - * - * @access public - * @param integer $theDifficultyLevel [optional] Since virtually everybody who - * plays sudoku wants a variety of difficulties this controls that. - * 1 is the easiest, 10 the most difficult. The easier Sudoku have - * extra information. - * @param integer $theMaxInterations [optional] Controls the number of iterations - * before the puzzle generator gives up and trys a different set - * of initial parameters. - * @param integer $theTrys [optional] The number of attempts at resetting the - * initial parameters before giving up. - * @return array A set of triples suitable for initializing a new Sudoku class - * (@see Sudoku::initializePuzzleFromArray). - */ - - function generatePuzzle($theDifficultyLevel = 10, $theMaxIterations = 50, $theTrys = 10) - { - $theDifficultyLevel = min($theDifficultyLevel, 10) ; - $theDifficultyLevel = max($theDifficultyLevel, 1) ; - - $this->theLevel = 0 ; - $this->theTrys = $theTrys ; - $this->theMaxIterations = $theMaxIterations ; - $this->theGenerationIterations = 0 ; - - for ($theTrys = 0; $theTrys < $this->theTrys ; $theTrys++) - { - $theAvailablePositions = array() ; - $theCluesPositions = array() ; - $theClues = array() ; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - $theAvailablePositions[] = array($i, $j) ; - } - - $theInitialState = $this->_generatePuzzle($theAvailablePositions, $theCluesPositions, $theClues) ; - - if ($theInitialState) - { - if ($theDifficultyLevel != 10) - { - $xxx = array() ; - - foreach ($theInitialState as $yyy) - $xxx[] = (($yyy[0] - 1) * 9) + ($yyy[1] - 1) ; - - /* - ** Get rid of the available positions already used in the initial state. - */ - - sort($xxx) ; - $xxx = array_reverse($xxx) ; - - foreach ($xxx as $i) - array_splice($theAvailablePositions, $i, 1) ; - - /* - ** Easy is defined as the number of derivable clues added to the minimum - ** required information to solve the puzzle as returned by _generatePuzzle. - */ - - for ($i = 0; $i < (10 - $theDifficultyLevel); $i++) - { - $xxx = mt_rand(0, count($theAvailablePositions)-1) ; - $row = $theAvailablePositions[$xxx][0] ; - $col = $theAvailablePositions[$xxx][1] ; - $theInitialState[] = array($row, $col, $this->theBoard[$row][$col]) ; - array_splice($theAvailablePositions, $xxx, 1) ; - } - } - - //echo "found $theTrys
"; - return $theInitialState ; - } - - if ($this->theDebug) - printf("
Too many iterations (%d), %d\n", $this->theMaxIterations, $theTrys); - - $this->Sudoku($this->theDebug) ; - } - - /* - ** No solution possible, we guess wrong too many times. - */ - - //echo "try=$theTrys
"; - return array() ; - } - - /** - * Sudoku puzzle generator. - * - * This is the routine that does the heavy lifting - * for the puzzle generation. It works by taking a guess for a value of a cell, applying - * the solver, testing the solution, and if it's a valid solution, calling itself - * recursively. If during this process, a solution cannot be found, the generator backs - * up (backtrace in Computer Science parlance) and trys another value. Since the generation - * appears to be an NP complete problem (according to the literature) limits on the number - * of iterations are asserted. Once these limits are passed, the generator gives up and - * makes another try. If enough tries are made, the generator gives up entirely. - * - * @access private - * @param array $theAvailablePositions A set of pairs for all positions which have not been - * filled by the solver or the set of guesses. When we run out of available - * positions, the solution is in hand. - * @param array $theCluesPositions A set of pairs for which values have been set by the - * puzzle generator. - * @param array $theClues A set of values for each pair in $theCluesPositions. - * @return array NULL array if no solution is possible, otherwise a set of triples - * suitable for feeding to {@link Sudoku::initializePuzzleFromArray} - */ - - function _generatePuzzle($theAvailablePositions, $theCluesPositions, $theClues) - { - $this->theLevel++ ; - - $this->theGenerationIterations++ ; - - /* - ** Since the last solution sequence may have eliminated one or more positions by - ** generating forced solutions for them, go through the list of available positions - ** and get rid of any that have already been solved. - */ - - $j = count($theAvailablePositions) ; - - for ($i = 0; $i < $j; $i++) - { - if ($this->theBoard[$theAvailablePositions[$i][0]][$theAvailablePositions[$i][1]]->IsApplied()) - { - array_splice($theAvailablePositions, $i, 1) ; - $i = $i - 1; - $j = $j - 1; - } - } - - if (count($theAvailablePositions) == 0) - { - /* - ** We're done, so we can return the clues and their positions to the caller. - ** This test is being done here to accommodate the eventual implementation of - ** generation from templates in which partial boards will be fed to the solver - ** and then the remaining board fed in. - */ - - for ($i = 0; $i < count($theCluesPositions); $i++) - array_push($theCluesPositions[$i], $theClues[$i]) ; - - return $theCluesPositions ; - } - - /* - ** Calculate the coupling for each available position. - ** - ** "coupling" is a measure of the amount of state affected by any change - ** to a given cell. In effect, the larger the coupling, the less constrained - ** the state of the cell is and the greater the effect of any change made to - ** the cell. There is some literature to this effect associated with Roku puzzles - ** (4x4 grid). I'm trying this attempting to find a way to generate consistently - ** more difficult Sudoku and it seems to have worked; the clue count drops to 25 or - ** fewer, more in line with the numbers predicted by the literature. The remainder - ** of the work is likely to be associated with finding better algorithms to solve - ** Sudoku (which would have the effect of generating harder ones). - */ - - $theCouplings = array() ; - - foreach ($theAvailablePositions as $xxx) - { - $theRowCoupling = $this->theRows[$xxx[0]]->coupling($xxx[0], $xxx[1]) ; - $theColumnCoupling = $this->theColumns[$xxx[1]]->coupling($xxx[0], $xxx[1]) ; - $theSquareCoupling = $this->theSquares[$this->_squareIndex($xxx[0], $xxx[1])]->coupling($xxx[0], $xxx[1]) ; - $theCouplings[$theRowCoupling + $theColumnCoupling + $theSquareCoupling][] = $xxx ; - } - - $theMaximumCoupling = max(array_keys($theCouplings)) ; - - /* - ** Pick a spot on the board and get the clues set up. - */ - - $theChoice = mt_rand(0, count($theCouplings[$theMaximumCoupling])-1) ; - $theCluesPositions[] = $theCouplings[$theMaximumCoupling][$theChoice] ; - $theRow = $theCouplings[$theMaximumCoupling][$theChoice][0] ; - $theColumn = $theCouplings[$theMaximumCoupling][$theChoice][1] ; - - /* - ** Capture the necessary global state of the board - */ - - $theCurrentBoard = $this->deepCopy($this->theBoard) ; - - /* - ** This is all possible states for the chosen cell. All values will be - ** randomly tried to see if a solution results. If all solutions fail, - ** the we'll back up in time and try again. - */ - - $thePossibleClues = array_keys($this->theBoard[$theRow][$theColumn]->getState()) ; - - while (count($thePossibleClues) != 0) - { - if ($this->theGenerationIterations > $this->theMaxIterations) - { - $this->theLevel = $this->theLevel - 1 ; - return array() ; - } - - $theClueChoice = mt_rand(0, count($thePossibleClues)-1) ; - $theValue = $thePossibleClues[$theClueChoice] ; - array_splice($thePossibleClues, $theClueChoice, 1) ; - - $theClues[] = $theValue ; - - $this->theBoard[$theRow][$theColumn]->flagSolvedPosition($theValue) ; - - if ($this->theDebug ) { printf("
(%03d, %03d) Trying (%d, %d) = %d\n", $this->theLevel, $this->theGenerationIterations, $theRow, $theColumn, $theValue) ; } ; - - $theFlag = $this->solve(false) ; - - if ($this->_validateTrialSolution()) - { - if ($theFlag) - { - /* - ** We're done, so we can return the clues and their positions to the caller. - */ - - for ($i = 0; $i < count($theCluesPositions); $i++) - { - array_push($theCluesPositions[$i], $theClues[$i]) ; - } - - return $theCluesPositions ; - } - else - { - $xxx = $this->_generatePuzzle($theAvailablePositions, $theCluesPositions, $theClues) ; - - if ($xxx) - { - return $xxx ; - } - } - } - - /* - ** We failed of a solution, back out the state and try the next possible value - ** for this position. - */ - - $this->theBoard = $theCurrentBoard ; - $this->_buildRCS() ; - array_pop($theClues) ; - } - - $this->theLevel = $this->theLevel - 1 ; - - /* - ** If we get here, we've tried all possible values remaining for the chosen - ** position and couldn't get a solution. Back out and try something else. - */ - - return array() ; - } - - /** - * Return the contents of the board as a string of digits and blanks. Blanks - * are used where the corresponding board item is an array, indicating the cell - * has not yet been solved. - * - * @desc Get the current state of the board as a string. - * @access public - */ - - function getBoardAsString() - { - $theString = "" ; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - $theString .= $this->theBoard[$i][$j]->asString() ; - } - } - - return $theString ; - } - - function &getCell($r, $c) - { - return $this->theBoard[$r][$c] ; - } - - /** - * Each element of the input array is a triple consisting of (row, column, value). - * Each of these values is in the range 1..9. - * - * @access public - * @param array $theArray - */ - - function initializePuzzleFromArray($theArray) - { - foreach ($theArray as $xxx) - { - $c =& $this->getCell($xxx[0], $xxx[1]) ; - $c->flagSolvedPosition($xxx[2]) ; - } - } - - /** - * Initialize puzzle from an input file. - * - * The input file is a text file, blank or tab delimited, with each line being a - * triple consisting of "row column value". Each of these values is in the range - * 1..9. Input lines that are blank (all whitespace) or which begin with whitespace - * followed by a "#" character are ignored. - * - * @access public - * @param mixed $theHandle [optional] defaults to STDIN. If a string is passed - * instead of a file handle, the file is opened. - */ - - function initializePuzzleFromFile($theHandle = STDIN) - { - $theOpenedFileFlag = FALSE ; - - /* - ** If a file name is passed instead of a resource, open the - ** file and process it. - */ - - if (is_string($theHandle)) - { - $theHandle = fopen($theHandle, "r") ; - if ($theHandle === FALSE) - { - exit() ; - } - } - - $yyy = array() ; - - if ($theHandle) - { - while (!feof($theHandle)) - { - $theString = trim(fgets($theHandle)) ; - if (($theString != "") && - (!preg_match('/^\s*#/', $theString))) - { - $xxx = preg_split('/\s+/', $theString) ; - if (!feof($theHandle)) - { - $yyy[] = array((int)$xxx[0], (int)$xxx[1], (int)$xxx[2]) ; - } - } - } - } - - $this->initializePuzzleFromArray($yyy) ; - - if ($theOpenedFileFlag) - { - fclose($theHandle) ; - } - } - - /** - * The input parameter consists of a string of 81 digits and blanks. If fewer characters - * are provide, the string is padded on the right. - * - * @desc Initialize puzzle from a string. - * @access public - * @param string $theString The initial state of each cell in the puzzle. - */ - - function initializePuzzleFromString($theString) - { - $theString = str_pad($theString, 81, " ") ; - - for ($i = 0; $i < 81; $i++) - { - if ($theString{$i} != " ") - { - $theArray[] = array((int)($i/9) + 1, ($i % 9) + 1, (int)$theString{$i}) ; - } - } - - $this->initializePuzzleFromArray($theArray) ; - } - - /** - * @desc predicate to determine if the current puzzle has been solved. - * @access public - * @return boolean true if the puzzle has been solved. - */ - - function isSolved() - { - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <=9; $j++) - { - if (!$this->theBoard[$i][$j]->IsSolved()) - { - return false ; - } - } - } - - return true ; - } - - /** - * Convert pending to actual solutions. - * - * This step is actually unnecessary unless you want a pretty output of the - * intermediate. - * - * @access private - * @return boolean True if at least on pending solution existed, false otherwise. - */ - - function _newSolvedPosition() - { - $theReturn = false ; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - if ($this->theBoard[$i][$j]->solvedState() == 1) - { - $this->theBoard[$i][$j]->un_set($this->theBoard[$i][$j]->getState()) ; - $theReturn = true ; - } - } - } - - return $theReturn ; - } - - /** - * Print the contents of the board in HTML format. - * - * A "hook" so that extension classes can show all the steps taken by - * the solve function. - * - * @see SudokuIntermediateSolution. - * - * @access private - * @param string $theHeader [optional] The header line to be output along - * with the intermediate solution. - */ - - function _printIntermediateSolution($theHeader = NULL) - { - if ($this->theDebug) - $this->printSolution($theHeader) ; - } - - /** - * Print the contents of the board in HTML format. - * - * Simple output, is tailored by hand so that an initial state and - * a solution will find nicely upon a single 8.5 x 11 page of paper. - * - * @access public - * @param mixed $theHeader [optional] The header line[s] to be output along - * with the solution. - */ - - function printSolution($theHeader = NULL) - { - if (($this->theDebug) && ($theHeader != NULL)) - { - if (is_array($theHeader)) - { - foreach ($theHeader as $aHeader) - print $aHeader ; - } - else - print $theHeader ; - } - - $theColors = array("green", "blue", "red") ; - $theFontSize = array("1em", "1em", ".8em") ; - $theFontWeight = array("bold", "bold", "lighter") ; - - printf("
\n") ; - - $theLast = 2 ; - - for ($i = 1; $i <= 9; $i++) - { - if ($theLast == 2) - printf("\n") ; - - printf("\n") ; - - $theLast = ($i - 1) % 3 ; - if ($theLast == 2) - printf("\n") ; - } - - printf("
\n") ; - - $theLast1 = 2 ; - - for ($j = 1; $j <=9; $j++) - { - if ($theLast1 == 2) - printf("\n") ; - - $c = &$this->theSquares[$i] ; - $c =& $c->getCell($j) ; ; - $theSolvedState = $c->solvedState() ; - - printf("\n") ; - - $theLast1 = ($j - 1) % 3 ; - if ($theLast1 == 2) - printf("\n") ; - } - - printf("
", - $theColors[$theSolvedState], - $theFontWeight[$theSolvedState], - $theFontSize[$theSolvedState]) ; - $xxx = $c->asString($this->theDebug) ; - print ($xxx == " " ? " " : $xxx) ; - printf("
\n") ; - } - - /** - * Solve a Sudoku. - * - * As explained earlier, this works by iterating upon three different - * types of inference: - * - * 1. A negative one, in which a value used within a row/column/square - * may not appear elsewhere within the enclosing row/column/square. - * 2. A positive one, in which any value with is unique in a row - * or column or square must be the solution to that position. - * 3. A tuple based positive one which comes in a number of flavors: - * 3a. The "Pair" rule as stated by the author of the "other" Sudoku - * class on phpclasses.org and generalized by me, e.g., in any RCS - * two cells containing a pair of values eliminate those values from - * consideration in the rest of the RC or S. - * 3b. The n/n+1 set rule as discovered by me, e.g., in any RCS, three cells - * containing the following pattern, (i, j)/(j, k)/(i, j, k) eliminate - * the values i, j, k from consideration in the rest of the RC or S. - * - * During processing I explain which structures (row, column, square) - * are being used to infer solutions. - * - * @access public - * @param boolean $theInitialStateFlag [optional] True if the initial - * state of the board is to be printed upon entry, false - * otherwise. [Default = true] - * @return boolean true if a solution was possible, false otherwise. - */ - - function solve($theInitialStateFlag = true) - { - $theHeader = "
Initial Position:" ; - - do - { - do - { - $this->_applySolvedPositions() ; - if ($theInitialStateFlag) - { - $this->_printIntermediateSolution($theHeader) ; - $theHeader = NULL ; - } - else - { - $theInitialStateFlag = true ; - $theHeader = "
Apply Slice and Dice:" ; - } - } while ($this->_newSolvedPosition()) ; - - $theRowIteration = FALSE ; - - for ($i = 1; $i <= 9; $i++) - { - if ($this->theRows[$i]->doAnInference()) - { - $theHeader = $this->theRows[$i]->getHeader() ; - $theRowIteration = TRUE ; - break ; - } - } - - $theColumnIteration = FALSE ; - - if (!$theRowIteration) - { - for ($i = 1; $i <= 9; $i++) - { - if ($this->theColumns[$i]->doAnInference()) - { - $theHeader = $this->theColumns[$i]->getHeader() ; - $theColumnIteration = TRUE ; - break ; - } - } - } - - $theSquareIteration = FALSE ; - - if (!($theRowIteration || $theColumnIteration)) - { - for ($i = 1; $i <= 9; $i++) - { - if ($this->theSquares[$i]->doAnInference()) - { - $theHeader = $this->theSquares[$i]->getHeader() ; - $theSquareIteration = TRUE ; - break ; - } - } - } - } while ($theRowIteration || $theColumnIteration || $theSquareIteration) ; - - return $this->IsSolved() ; - } - - /** - * Here there be dragons. In conversations with other Sudoku folks, I find that there ARE Sudoku with - * unique solutions for which a clue set may be incomplete, i.e., does not lead to a solution. The - * solution may only be found by guessing the next move. I'm of the opinion that this violates the - * definition of Sudoku (in which it's frequently said "never guess") but if it's possible to find - * a solution, this will do it. - * - * The problem is that it can take a LONG time if there ISN'T a solution since this is basically a - * backtracing solution trier. - * - * The basic algorithm is pretty simple: - * - * 1. Find the first unsolved cell. - * 2. For every possible value, substutite value for the cell, apply inferences. - * 3. If a solution was found, we're done. - * 4. Recurse looking for the next cell to try a value for. - * - * There's a bit of bookkeeping to keep the state right when backing up, but that's pretty - * straightforward and looks a lot like that of generatePuzzle. - * - * @desc Brute force additional solutions. - * @access public - * @returns array The clues added sufficient to solve the puzzle. - */ - - function solveBruteForce($i = 1, $j = 1) - { - for (; $i <= 9; $i++) - { - for (; $j <= 9; $j++) - { - if ($this->theBoard[$i][$j]->solvedState() != 0) - { - if ($this->theDebug) - { - printf("
Applying Brute Force to %d, %d\n", $i, $j) ; - } - - $theCurrentBoard = $this->deepCopy($this->theBoard) ; - $theValues = $this->theBoard[$i][$j]->getState() ; - - foreach ($theValues as $theValue) - { - $this->theBoard[$i][$j]->flagSolvedPosition($theValue) ; - - $theSolutionFlag = $this->solve() ; - $theTrialSolutionFlag = $this->_validateTrialSolution() ; - - if ($theTrialSolutionFlag && $theSolutionFlag) - { - return array(array($i, $j, $theValue)) ; - } - - if ($theTrialSolutionFlag) - { - $theNewGuesses = $this->solveBruteForce($i, $j+1) ; - - if ($theNewGuesses) - { - $theNewGuesses[] = array($i, $j, $theValue) ; - - return $theNewGuesses ; - } - } - - if ($this->theDebug) - { - printf("
Backing out\n") ; - } - - $this->theBoard = $theCurrentBoard ; - $this->_buildRCS() ; - } - - return array() ; - } - } - } - } - - /** - * @desc Calculate the index of the square containing a specific cell. - * @param integer $theRow the row coordinate. - * @param integer $theColumn the column coordinate. - * @return integer the square index in the range 1..9 - */ - - function _squareIndex($theRow, $theColumn) - { - $theIndex = ((int)(($theRow - 1) / 3) * 3) + (int)(($theColumn - 1) / 3) + 1 ; - return $theIndex ; - } - - /** - * Validate a complete solution. - * - * After a complete solution has been generated check the board and - * report any inconsistencies. This is primarily intended for debugging - * purposes. - * - * @access public - * @return mixed true if the solution is valid, an array containing the - * error details. - */ - - function validateSolution() - { - $theReturn = array() ; - - for ($i = 1; $i <= 9; $i++) - { - if (!$this->theRows[$i]->validateSolution()) - { - $theReturn[0][] = $i ; - } - if (!$this->theColumns[$i]->validateSolution()) - { - $theReturn[1][] = $i ; - } - if (!$this->theSquares[$i]->validateSolution()) - { - $theReturn[2][] = $i ; - } - } - - return (count($theReturn) == 0 ? TRUE : $theReturn) ; - } - - /** - * Validate an entire trial solution. - * - * Used during puzzle generation to determine when to backtrace. - * - * @access private - * @return True when the intermediate soltuion is valid, false otherwise. - */ - - function _validateTrialSolution() - { - - for ($i = 1; $i <= 9; $i++) - { - if (!(($this->theRows[$i]->validateTrialSolution()) && - ($this->theColumns[$i]->validateTrialSolution()) && - ($this->theSquares[$i]->validateTrialSolution()))) - { - return FALSE ; - } - } - - return TRUE ; - } -} - -/** - * Extend Sudoku to generate puzzles based on templates. - * - * Templates are either input files or arrays containing doubles. - * - * @package Sudoku - */ - -class SudokuTemplates extends Sudoku -{ - function SudokuTemplates($theDebug = FALSE) - { - $this->Sudoku($theDebug) ; - } - - function generatePuzzleFromFile($theHandle = STDIN, $theDifficultyLevel = 10) - { - $yyy = array() ; - - if ($theHandle) - { - while (!feof($theHandle)) - { - $theString = trim(fgets($theHandle)) ; - $xxx = preg_split("/\s+/", $theString) ; - if (!feof($theHandle)) - { - $yyy[] = array((int)$xxx[0], (int)$xxx[1]) ; - } - } - } - - return $this->generatePuzzleFromArray($yyy, $theDifficultyLevel) ; - } - - function generatePuzzleFromArray($theArray, $theDifficultyLevel = 10) - { - $this->_generatePuzzle($theArray, array(), array()) ; - - /* - ** Because the generation process may infer values for some of the - ** template cells, we construct the clues from the board and the - ** input array before continuing to generate the puzzle. - */ - - foreach ($theArray as $theKey => $thePosition) - { - $theTemplateClues[] = array($thePosition[0], $thePosition[1], $this->theBoard[$thePosition[0]][$thePosition[1]]) ; - } - - $theOtherClues = $this->generatePuzzle($theDifficultyLevel) ; - - return array_merge($theTemplateClues, $theOtherClues) ; - } -} - -/** - * Extend Sudoku to print all intermediate results. - * - * @package Sudoku - */ - -class SudokuIntermediateSolution extends Sudoku -{ - function SudokuIntermediateResults($theDebug = FALSE) - { - $this->Sudoku($theDebug) ; - } - - function _printIntermediateSolution($theHeader = NULL) - { - $this->printSolution($theHeader) ; - } -} - -function make_seed() -{ - list($usec, $sec) = explode(' ', microtime()); - return (float) $sec + ((float) $usec * 100000); -} - -?> \ No newline at end of file +. + +/* + * Edit History: + * + * Dick Munroe (munroe@csworks.com) 02-Nov-2005 + * Initial version created. + * + * Dick Munroe (munroe@csworks.com) 12-Nov-2005 + * Allow initialzePuzzle to accept a file name in addition + * to a resource. Windows doesn't do file redirection properly + * so the examples have to be able to handle a file NAME as + * input as well as a redirected file. + * Allow initializePuzzle to accept a string of 81 characters. + * + * Dick Munroe (munroe@csworks.com) 13-Nov-2005 + * It appears that getBoardAsString screws up somehow. Rewrite it. + * + * Dick Munroe (munroe@csworks.com) 16-Nov-2005 + * Add a "pair" inference. + * Dick Munroe (munroe@csworks.com) 17-Nov-2005 + * Add comments to input files. + * There was a bug in _applyTuple that caused premature exiting of the inference + * engine. + * If SDD isn't present, don't display error. + * + * Dick Munroe (munroe@csworks.com) 19-Nov-2005 + * Add a new tuple inference. + * Do a ground up ObjectS oriented redesign to make the addition of arbitrary + * inferences MUCH easier. + * Get the printing during solving right. + * Somehow array_equal developed a "bug". + * + * Dick Munroe (munroe@csworks.com) 22-Nov-2005 + * Add n,n+1 tuple recognition for n=2. + * Restructure inference engine to get maximum benefit out of each pass. + * + * Dick Munroe (munroe@csworks.com) 28-Nov-2005 + * Attempt to build harder Sudoku by implementing a coupling coefficient + * attempting to distribute clues more optimally. + */ + +@require_once("SDD/class.SDD.php"); + +/** + * @author Dick Munroe + * @copyright copyright @ 2005 by Dick Munroe, Cottage Software Works, Inc. + * @license http://www.csworks.com/publications/ModifiedNetBSD.html + * @version 2.2.0 + * @package Sudoku + */ + +/** + * Basic functionality needed for ObjectSs in the Sudoku solver. + * + * Technically speaking these aren't restricted to the Sudoku classes + * and are of use generally. + * + * @package Sudoku + */ + +class ObjectS { + /** + * @desc Are two array's equal (have the same contents). + * @param array + * @param array + * @return boolean + */ + + public function array_equal($thearray1, $thearray2) { + if (!(is_array($thearray1) && is_array($thearray2))) { + return false; + } + + if (count($thearray1) != count($thearray2)) { + return false; + } + + $xxx = array_diff($thearray1, $thearray2); + + return (count($xxx) == 0); + } + + /** + * Deep copy anything. + * + * @access public + * @param array $theArray [optional] Something to be deep copied. [Default is the current + * ObjectS. + * @return mixed The deep copy of the input. All references embedded within + * the array have been resolved into copies allowing things like the + * board array to be copied. + */ + + public function deepcopy($thearray = null) { + if ($thearray === null) { + return unserialize(serialize($this)); + } else { + return unserialize(serialize($thearray)); + } + } + + /** + * @desc Debugging output interface. + * @access public + * @param mixed $theValue The "thing" to be pretty printed. + * @param boolean $theHTMLFlag True if the output will be seen in a browser, false otherwise. + */ + + public function print_d(&$thevalue, $thehtmlflag = true) { + print SDD::dump($thevalue, $thehtmlflag); + } +} + +/** + * The individual cell on the Sudoku board. + * + * These cells aren't restricted to 9x9 Sudoku (although pretty much everything else + * at the moment). This class provides the state manipulation and searching capabilities + * needed by the inference engine (class RCS). + * + * @package Sudoku + */ +class Cell extends ObjectS { + protected $r; + protected $c; + + protected $state = array(); + protected $applied = false; + + /** + * @desc Constructor + * @param integer $r row address of this cell (not used, primarily for debugging purposes). + * @param integer $c column address of this cell (ditto). + * @param integer $nStates The number of states each cell can have. Looking forward to + * implementing Super-doku. + */ + + public function cell($r, $c, $nstates = 9) { + $this->r = $r; + $this->c = $c; + + for ($i = 1; $i <= $nstates; $i++) { + $this->state[$i] = $i; + } + } + + /** + * @desc This cell has been "applied", i.e., solved, to the board. + */ + + public function applied() { + $this->applied = true; + } + + /** + * Only those cells which are not subsets of the tuple have the + * contents of the tuple removed. + * + * @desc apply a 23Tuple to a cell. + * @access public + * @param array $aTuple the tuple to be eliminated. + */ + + public function apply23tuple($atuple) { + if (is_array($this->state)) { + $xxx = array_intersect($this->state, $atuple); + if ((count($xxx) > 0) && (count($xxx) != count($this->state))) { + return $this->un_set($atuple); + } else { + return false; + } + } else { + return false; + } + } + + /** + * For more details on the pair tuple algorithm, see RCS::_pairSolution. + * + * @desc Remove all values in the tuple, but only if the cell is a superset. + * @access public + * @param array A tuple to be eliminated from the cell's state. + */ + + public function applytuple($atuple) { + if (is_array($this->state)) { + if (!$this->array_equal($atuple, $this->state)) { + return $this->un_set($atuple); + } + } + + return false; + } + + /** + * @desc Return the string representation of the cell. + * @access public + * @param boolean $theFlag true if the intermediate states of the cell are to be visible. + * @return string + */ + + public function asstring($theflag = false) { + if (is_array($this->state)) { + if (($theflag) || (count($this->state) == 1)) { + return implode(", ", $this->state); + } else { + return " "; + } + } else { + return $this->state; + } + } + + /** + * Used to make sure that solved positions show up at print time. + * The value is used as a candidate for "slicing and dicing" by elimination in + * Sudoku::_newSolvedPosition. + * + * @desc Assert pending solution. + * @access public + * @param integer $value The value for the solved position. + */ + + public function flagsolvedposition($value) { + $this->state = array($value => $value); + } + + /** + * @desc return the state of a cell. + * @access protected + * @return mixed Either solved state or array of state pending solution. + */ + + public function &getstate() { + return $this->state; + } + + /** + * @desc Has the state of this cell been applied to the board. + * @access public + * @return boolean True if it has, false otherwise. Implies that IsSolved is true as well. + */ + + public function isapplied() { + return $this->applied; + } + + /** + * @desc Has this cell been solved? + * @access public + * @return boolean True if this cell has hit a single state. + */ + + public function issolved() { + return !is_array($this->state); + } + + /** + * This is used primarily by the pretty printer, but has other applications + * in the code. + * + * @desc Return information about the state of a cell. + * @access public + * @return integer 0 => the cell has been solved. + * 1 => the cell has been solved but not seen a solved. + * 2 => the cell has not been solved. + */ + + public function solvedstate() { + if (is_array($this->state)) { + if (count($this->state) == 1) { + return 1; + } else { + return 2; + } + } else { + return 0; + } + } + + /** + * This is the negative inference of Sudoku. By eliminating values the + * cells approach solutions. Once a cell has been completely eliminated, + * the value causing the complete elimination must be the solution and the + * cell is promoted into the solved state. + * + * @desc Eliminate one or more values from the state information of the cell. + * @access public + * @param mixed The value or values to be removed from the cell state. + * @return boolean True if the cell state was modified, false otherwise. + */ + + public function un_set($thevalues) { + if (is_array($thevalues)) { + $thereturn = false; + + foreach ($thevalues as $thevalue) { + $thereturn |= $this->un_set($thevalue); + } + + return $thereturn; + } + + if (is_array($this->state)) { + $thereturn = isset($this->state[$thevalues]); + unset($this->state[$thevalues]); + if (count($this->state) == 0) { + $this->state = $thevalues; + } + return $thereturn; + } else { + return false; + } + } +} + +/** + * The individual row column or square on the Sudoku board. + * + * @package Sudoku + */ + +class rcs extends ObjectS{ + protected $theindex; + + protected $therow = array(); + + protected $theheader = ""; + + protected $thetag = ""; + + /** + * This + * @desc Constructor + * @access public + * @param string $theTag "Row", "Column", "Square", used primarily in debugging. + * @param integer $theIndex 1..9, where is this on the board. Square are numbered top + * left, ending bottom right + * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what + * limts things to 9x9 Sudoku currently. + */ + + public function rcs($thetag, $theindex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) { + $this->thetag = $thetag; + $this->theindex = $theindex; + $this->therow[1] = &$a1; + $this->therow[2] = &$a2; + $this->therow[3] = &$a3; + $this->therow[4] = &$a4; + $this->therow[5] = &$a5; + $this->therow[6] = &$a6; + $this->therow[7] = &$a7; + $this->therow[8] = &$a8; + $this->therow[9] = &$a9; + } + + /** + * There is a special case that comes up a lot in Sudoku. If there + * are values i, j, k and cells of the form (i, j), (j, k), (i, j, k) + * the the values i, j, and k cannot appear in any other cells. The + * proof is a simple "by contradiction" proof. Assume that the values + * do occur elsewhere and you always get a contradiction for these + * three cells. I'm pretty sure that this is a general rule, but for + * 9x9 Sudoku, they probably aren't of interested. + * + * @desc + * @access private + * @return boolean True if a 23 solution exists and has been applied. + */ + + public function _23solution() { + $thecounts = array(); + $thetuples = array(); + $theunsolved = 0; + + for ($i = 1; $i <= 9; $i++) { + $j = count($this->theRow[$i]->getState()); + $thecounts[ $j][] = $i; + $theunsolved++; + } + + if (array_key_exists( 2, $thecounts) and array_key_exists( 3, $thecounts)) { + if ((count($thecounts[2]) < 2) || (count($thecounts[3]) < 1)) { + return false; + } + } + + /* + * Look at each pair of 2 tuples and see if their union exists in the 3 tuples. + * If so, eliminate everything from the set and bail. + */ + + $the2tuples = &$thecounts[2]; + $the3tuples = &$thecounts[3]; + $thecount2 = count($the2tuples); + $thecount3 = count($the3tuples); + + for ($i = 0; $i < $thecount2 - 1; $i++) { + for ($j = $i + 1; $j < $thecount2; $j++) { + $xxx = array_unique(array_merge($this->therow[$the2tuples[$i]]->getstate(), + $this->therow[$the2tuples[$j]]->getstate())); + for ($k = 0; $k < $thecount3; $k++) { + if ($this->array_equal($xxx, $this->therow[$the3tuples[$k]]->getstate())) { + $thetuples[] = $xxx; + break; + } + } + } + } + + /* + * Since it takes 3 cells to construct the 23 tuple, unless there are more than 3 + * unsolved cells, further work doesn't make any sense. + */ + + $thereturn = false; + + if ((count($thetuples) != 0) && ($theunsolved > 3)) { + foreach ($thetuples as $atuple) { + foreach ($this->therow as $thecell) { + $thereturn |= $thecell->apply23tuple($atuple); + } + } + } + + if ($thereturn) { + $this->theheader[] = sprintf("
Apply %s[%d] 23 Tuple Inference:", $this->thetag, $this->theindex); + } + + return $thereturn; + } + + /** + * @desc apply a tuple to exclude items from within the row/column/square. + * @param array $aTuple the tuple to be excluded. + * @access private + * @return boolean true if anything changes. + */ + + protected function _applytuple(&$atuple) { + $thereturn = false; + + for ($i = 1; $i <= 9; $i++) { + $thereturn |= $this->therow[$i]->applytuple( $atuple); + } + + return $thereturn; + } + + /** + * This is a placeholder to be overridden to calculate the "coupling" for + * a cell. Coupling is defined to be the sum of the sizes of the intersection + * between this cell and all others in the row/column/square. This provides + * a metric for deciding placement of clues within puzzles. In effect, this + * forces the puzzle generator to select places for new clues depending upon + * how little information is changed by altering the state of a cell. The larger + * the number returned by the coupling, function, the less information is currently + * available for the state of the cell. By selecting areas with the least information + * the clue sets are substantially smaller than simple random placement. + * + * @desc Calculate the coupling for a cell within the row/column/square. + * @access abstract + * @param integer $theRow the row coordinate on the board of the cell. + * @param integer $theColumn the column coordinate on the board of the cell. + * @return integer the degree of coupling between the cell and the rest of the cells + * within the row/column/square. + */ + + public function coupling($therow, $thecolumn) { + return 0; + } + + /** + * I think that the goal of the inference engine is to eliminate + * as much "junk" state as possible on each pass. Therefore the + * order of the inferences should be 23 tuple, pair, unique because + * the 23 tuple allows you to eliminate 3 values (if it works), and the + * pair (generally) only 2. The unique solution adds no new information. + * + * @desc Run the inference engine for a row/column/square. + * @access public + * @param array theRow A row/column/square data structure. + * @param string theType A string merged with the standard headers during + * intermediate solution printing. + * @return boolean True when at least one inference has succeeded. + */ + + public function doaninference() { + $this->theheader = null; + + $thereturn = $this->_23solution(); + $thereturn |= $this->_pairsolution(); + $thereturn |= $this->_uniquesolution(); + + return $thereturn; + } + + /** + * @desc Find all tuples with the same contents. + * @param array Array of n size tuples. + * @returns array of tuples that appear the same number of times as the size of the contents + */ + + public function _findtuples(&$thearray) { + $thereturn = array(); + for ($i = 0; $i < count($thearray); $i++) { + $thecount = 1; + + for ($j = $i + 1; $j < count($thearray); $j++) { + $s1 = &$this->theRow[$thearray[$i]]; + $s1 =& $s1->getstate(); + + $s2 = &$this->therow[$thearray[$j]]; + $s2 =& $s2->getstate(); + + $acount = count($s1); + + if ($this->array_equal($s1, $s2)) { + $thecount++; + + if ($thecount == $acount) { + $thereturn[] = $s1; + break; + } + } + } + } + + return $thereturn; + } + + /** + * @desc Get a reference to the specified cell. + * @access public + * @return reference to ObjectS of class Cell. + */ + + public function &getcell($i) { + return $this->therow[$i]; + } + + /** + * @desc Get the header set by the last call to doAnInference. + * + */ + + public function getheader() { + return $this->theheader; + } + + /** + * Turns out if you every find a position of n squares which can only contain + * the same values, then those values cannot appear elsewhere in the structure. + * This is a second positive inference that provides additional negative information. + * Thanks to Ghica van Emde Boas (also an author of a Sudoku class) for convincing + * me that these situations really occurred. + * + * @desc Eliminate tuple-locked alternatives. + * @access private + * @return boolean True if something changed. + */ + + protected function _pairsolution() { + $thecounts = array(); + $thetuples = array(); + + for ($i = 1; $i <= 9; $i++) { + $c = &$this->therow[$i]; + $thecounts[count($c->getstate())][] = $i; + } + + unset($thecounts[1]); + + /* + ** Get rid of any set of counts which cannot possibly meet the + ** requirements. + */ + + $thepossibilities = $thecounts; + + foreach ($thecounts as $thekey => $thevalue) { + if (count($thevalue) < $thekey) { + unset($thepossibilities[$thekey]); + } + } + + if (count($thepossibilities) == 0) { + return false; + } + + /* + * At this point there are 1 or more tuples which MAY satisfy the conditions. + */ + + $thereturn = false; + + foreach ($thepossibilities as $thevalue) { + $thetuples = $this->_findtuples($thevalue); + + if (count($thetuples) != 0) { + foreach ($thetuples as $atuple) { + $thereturn |= $this->_applyruple($atuple); + } + } + } + + if ($thereturn) { + $this->theheader[] = sprintf("
Apply %s[%d] Pair Inference:", $this->thetag, $this->theindex); + } + + return $thereturn; + } + + public function un_set($thevalues) { + $thereturn = false; + + for ($i = 1; $i <= 9; $i++) { + $c = &$this->therow[$i]; + $thereturn |= $c->un_set($thevalues); + } + + return $thereturn; + } + + /** + * Find a solution to a row/column/square. + * + * Find any unique numbers within the row/column/square under consideration. + * Look through a row structure for a value that appears in only one cell. + * When you find one, that's a solution for that cell. + * + * There is a second inference that can be taken. Given "n" cells in a row/column/square + * and whose values can only consist of a set of size "n", then those values may obtain + * there and ONLY there and may be eliminated from consideration in the rest of the set. + * For example, if two cells must contain the values 5 or 6, then no other cell in that + * row/column/square may contain those values, similarly for 3 cells, etc. + * + * @access private + * @return boolean True if one or more values in the RCS has changed state. + */ + + protected function _uniquesolution() { + $theset = array(); + + for ($i = 1; $i <= 9; $i++) { + $c = &$this->therow[$i]; + if (!$c->issolved()) { + foreach ($c->getstate() as $thevalue) { + $theset[$thevalue][] = $i; + } + } + } + + /* + * If there were no unsolved positions, then we're done and nothing has + * changed. + */ + + if (count($theset) == 0) { + return false; + } + + /* + * Pull out all those keys having only one occurrance in the RCS. + */ + + foreach ($theset as $thekey => $thevalues) { + if (count($thevalues) != 1) { + unset($theset[$thekey]); + } + } + + /* + * If there aren't any unique values, we're done. + */ + + if (count($theset) == 0) { + return false; + } + + foreach ($theset as $thevalue => $theindex) { + $this->therow[$theindex[0]]->flagsolvedposition($thevalue); + } + + $this->theheader[] = sprintf("
Apply %s[%d] Unique Inference:", $this->thetag, $this->theindex); + + return true; + } + + /* + * @desc Check to see if the RCS contains a valid state. + * @access public + * @return boolean True if the state of the RCS could be part of a valid + * solution, false otherwise. + */ + + public function validatesolution() { + $thenewset = array(); + + foreach ($this->therow as $thecell) { + if ($thecell->solvedstate() == 0) { + $thenewset[] = $thecell->getstate(); + } + } + + $xxx = array_unique($thenewset); + + return (count($xxx) == count($this->therow)); + } + + /** + * Validate a part of a trial solution. + * + * Check a row/column/square to see if there are any invalidations on this solution. + * Only items that are actually solved are compared. This is used during puzzle + * generation. + * + * @access public + * @return True if the input parameter contains a valid solution, false otherwise. + */ + + public function validatetrialsolution() { + $thenewset = array(); + + foreach ($this->therow as $thecell) { + if ($thecell->solvedstate() == 0) { + $thenewset[] = $thecell->getstate(); + } + } + + $xxx = array_unique($thenewset); + + return ((count($xxx) == count($thenewset) ? true : false)); + } +} + +/** + * Row ObjectS. + * + * @package Sudoku + */ + +class r extends rcs { + /** + * @desc Constructor + * @access public + * @param string $theTag "Row", "Column", "Square", used primarily in debugging. + * @param integer $theIndex 1..9, where is this on the board. Square are numbered top + * left, ending bottom right + * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what + * limts things to 9x9 Sudoku currently. + */ + + public function r($thetag, $theindex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) { + $this->rcs($thetag, $theindex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9); + } + + /** + * @see RCS::coupling + */ + + public function coupling($therow, $thecolumn) { + return $thestate = $this->_coupling($thecolumn); + } + + /** + * @see RCS::coupling + * @desc Heavy lifting for row/column coupling calculations. + * @access private + * @param integer $theIndex the index of the cell within the row or column. + * @return integer the "coupling coefficient" for the cell. The sum of the + * sizes of the intersection between this and all other + * cells in the row or column. + */ + + protected function _coupling($theindex) { + $thecommonstate =& $this->getCell($theindex); + $thecommonstate =& $thecommonstate->getstate(); + + $thecoupling = 0; + + for ($i = 1; $i <= count($this->therow); $i++) { + if ($i != $theindex) { + $thecell =& $this->getcell($i); + if ($thecell->solvedstate() != 0) { + $thecoupling += count(array_intersect($thecommonstate, $thecell->getstate())); + } + } + } + + return $thecoupling; + } +} + +/** + * The column ObjectS. + * + * @package Sudoku + */ + +class c extends r { + /** + * @desc Constructor + * @access public + * @param string $theTag "Row", "Column", "Square", used primarily in debugging. + * @param integer $theIndex 1..9, where is this on the board. Square are numbered top + * left, ending bottom right + * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what + * limts things to 9x9 Sudoku currently. + */ + + public function c($thetag, $theindex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) { + $this->r($thetag, $theindex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9); + } + + /** + * @see R::coupling + */ + + public function coupling($therow, $thecolumn) { + return $thestate = $this->_coupling($therow); + } +} + +/** + * The Square ObjectS. + * + * @package Sudoku + */ + +class s extends rcs { + /* + * The cells within the 3x3 sudoku which participate in the coupling calculation for a square. + * Remember that the missing cells have already participated in the row or column coupling + * calculation. + * + * @access private + * @var array + */ + + protected $thecouplingorder = array( 1 => array(5, 6, 8, 9), + 2 => array(4, 6, 7, 9), + 3 => array(4, 5, 7, 8), + 4 => array(2, 3, 8, 9), + 5 => array(1, 3, 7, 9), + 6 => array(1, 2, 7, 8), + 7 => array(2, 3, 5, 6), + 8 => array(1, 3, 4, 6), + 9 => array(1, 2, 4, 5)); + + /** + * @desc Constructor + * @access public + * @param string $theTag "Row", "Column", "Square", used primarily in debugging. + * @param integer $theIndex 1..9, where is this on the board. Square are numbered top + * left, ending bottom right + * @param ObjectS $a1..9 of class Cell. The cells comprising this entity. This interface is what + * limts things to 9x9 Sudoku currently. + */ + + public function s($thetag, $theindex, &$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7, &$a8, &$a9) { + $this->rcs($thetag, $theindex, $a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9); + } + + /** + * @see RCS::coupling + */ + + public function coupling($therow, $thecolumn) { + $theindex = ((($therow - 1) % 3) * 3) + (($thecolumn - 1) % 3) + 1; + $thecommonstate =& $this->getcell($theindex); + $thecommonstate =& $thecommonstate->getstate(); + + $thecoupling = 0; + + foreach ($this->thecouplingorder[$theindex] as $i) { + $thecell =& $this->getcell($i); + if ($thecell->solvedstate() != 0) { + $thecoupling += count(array_intersect($thecommonstate, $thecell->getstate())); + } + } + + return $thecoupling; + } +} + +/** + * Solve and generate Sudoku puzzles. + * + * Solve and generate Sudoku. A simple output interface is provided for + * web pages. The primary use of this class is as infra-structure for + * Sudoku game sites. + * + * The solver side of this class (solve) relies on the usual characteristic + * of logic puzzles, i.e., at any point in time there is one (or more) + * UNIQUE solution to some part of the puzzle. This solution can be + * applied, then iterated upon to find the next part of the puzzle. A + * properly constructed Sudoku can have only one solution which guarangees + * that this is the case. (Sudoku with multiple solutions will always + * require guessing at some point which is specifically disallowed by + * the rules of Sudoku). + * + * While the solver side is algorithmic, the generator side is much more + * difficult and, in fact, the generation of Sudoku appears to be NP + * complete. That being the case, I observed that most successful + * generated initial conditions happened quickly, typically with < 40 + * iterations. So the puzzle generator runs "for a while" until it + * either succeeds or doesn't generated a solveable puzzle. If we get + * to that position, I just retry and so far I've always succeeded in + * generating an initial state. Not guarateed, but in engineering terms + * "close enough". + * + * @package Sudoku + * @example ./example.php + * @example ./example1.php + * @example ./example2.php + * @example ./example3.php + */ + +class sudoku extends ObjectS { + /* + * An array of Cell ObjectSs, organized into rows and columns. + * + * @access private + * @var array of ObjectSs of type Cell. + */ + + protected $theboard = array(); + + /* + * True if debugging output is to be provided during a run. + * + * @access private + * @var boolean + */ + + protected $thedebug = false; + + /* + * An array of RCS ObjectSs, one ObjectS for each row. + * + * @access private + * @var ObjectS of type R + */ + + protected $therows = array(); + + /* + * An array of RCS ObjectSs, one ObjectS for each Column. + * + * @access private + * @var ObjectS of type C + */ + + private $thecolumns = array(); + + /** + * An array of RCS ObjectSs, one ObjectS for each square. + * + * @access private + * @var ObjectS of type S + */ + + protected $thesquares = array(); + + /* + * Used during puzzle generation for debugging output. There may + * eventually be some use of theLevel to figure out where to stop + * the backtrace when puzzle generation fails. + * + * @access private + * @var integer. + */ + + protected $thelevel = 0; + + /* + * Used during puzzle generation to determine when the generation + * will fail. Failure, in this case, means to take a LONG time. The + * backtracing algorithm used in the puzzle generator will always find + * a solution, it just might take a very long time. This is a way to + * limit the damage before taking another guess. + * + * @access private + * @var integer. + */ + + protected $themaxiterations = 50; + + /* + * Used during puzzle generation to limit the number of trys at + * generation a puzzle in the event puzzle generation fails + * (@see Suduko::$theMaxIterations). I've never seen more than + * a couple of failures in a row, so this should be sufficient + * to get a puzzle generated. + * + * @access private + * @var integer. + */ + + protected $thetrys = 10; + + /* + * Used during puzzle generation to count the number of iterations + * during puzzle generation. It the number gets above $theMaxIterations, + * puzzle generation has failed and another try is made. + * + * @access private + * @var integer. + */ + + protected $thegenerationiterations = 0; + + public function sudoku($thedebug = false) { + $this->thedebug = $thedebug; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + $this->theboard[$i][$j] = new cell($i, $j); + } + } + + $this->_buildrcs(); + } + + /* + * Apply a pending solved position to the row/square/column. + * + * At this point, the board has been populated with any pending solutions. + * This applies the "negative" inference that no row, column, or square + * containing the value within the cell. + * + * @access private + * @param integer $row The row of the board's element whose value is now fixed. + * @param integer $col The column of the board's element whose value is now fixed. + */ + + protected function _applysolvedposition($row, $col) { + $thevalue = $this->theboard[$row][$col]->getstate(); + + /* + * No other cell in the row, column, or square can take on the value "value" any longer. + */ + + $i = (((int)(($row - 1) / 3)) * 3); + $i = $i + ((int)(($col - 1) / 3)) + 1; + + $this->therows[$row]->un_set($thevalue); + + $this->thecolumns[$col]->un_set($thevalue); + + $this->thesquares[$i]->un_set($thevalue); + } + + /* + * @desc Apply all pending solved positions to the board. + * @access private + * @return boolean True if at least one solved position was applied, false + * otherwise. + */ + + protected function _applysolvedpositions() { + $thereturn = false; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + if (!$this->theboard[$i][$j]->isapplied()) { + if ($this->theboard[$i][$j]->solvedstate() == 0) { + $this->_applysolvedposition($i, $j); + + /* + * Update the solved position matrix and make sure that the board actually + * has a value in place. + */ + + $this->theboard[$i][$j]->applied(); + $thereturn = true; + } + } + } + } + + return $thereturn; + } + + /* + * @desc build the row/column/square structures for the board. + * @access private + */ + + protected function _buildrcs() { + for ($i = 1; $i <= 9; $i++) { + $this->therows[$i] = new r("Row", + $i, + $this->theboard[$i][1], + $this->theboard[$i][2], + $this->theboard[$i][3], + $this->theboard[$i][4], + $this->theboard[$i][5], + $this->theboard[$i][6], + $this->theboard[$i][7], + $this->theboard[$i][8], + $this->theboard[$i][9]); + $this->thecolumns[$i] = new C("Column", + $i, + $this->theboard[1][$i], + $this->theboard[2][$i], + $this->theboard[3][$i], + $this->theboard[4][$i], + $this->theboard[5][$i], + $this->theboard[6][$i], + $this->theboard[7][$i], + $this->theboard[8][$i], + $this->theboard[9][$i]); + + $r = ((int)(($i - 1) / 3)) * 3; + $c = (($i - 1) % 3) * 3; + + $this->thesquares[$i] = new S("Square", + $i, + $this->theboard[$r + 1][$c + 1], + $this->theboard[$r + 1][$c + 2], + $this->theboard[$r + 1][$c + 3], + $this->theboard[$r + 2][$c + 1], + $this->theboard[$r + 2][$c + 2], + $this->theboard[$r + 2][$c + 3], + $this->theboard[$r + 3][$c + 1], + $this->theboard[$r + 3][$c + 2], + $this->theboard[$r + 3][$c + 3]); + } + } + + /* + * Seek alternate solutions in a solution set. + * + * Given a solution, see if there are any alternates within the solution. + * In theory this should return the "minimum" solution given any solution. + * + * @access public + * @param array $theInitialState (@see Sudoku::initializePuzzleFromArray) + * @return array A set of triples containing the minimum solution. + */ + + public function findalternatesolution($theinitialstate) { + $j = count($theinitialstate); + + for ($i = 0; $i < $j; $i++) { + $xxx = $theinitialstate; + + $xxx = array_splice($xxx, $i, 1); + + $this->sudoku(); + + $this->initializepuzzlefromarray($xxx); + + if ($this->solve()) { + return $this->findalternatesolution($xxx); + } + } + + return $theinitialstate; + } + + /* + * Initialize Sudoku puzzle generation and generate a puzzle. + * + * Turns out that while the solution of Sudoku is mechanical, the creation of + * Sudoku is an NP-Complete problem. Which means that I can use the inference + * engine to help generate puzzles, but I need to test the solution to see if + * I've gone wrong and back up and change my strategy. So something in the + * recursive descent arena will be necessary. Since the generation can take + * a long time to force a solution, it's easier to probe for a solution + * if you go "too long". + * + * @access public + * @param integer $theDifficultyLevel [optional] Since virtually everybody who + * plays sudoku wants a variety of difficulties this controls that. + * 1 is the easiest, 10 the most difficult. The easier Sudoku have + * extra information. + * @param integer $theMaxInterations [optional] Controls the number of iterations + * before the puzzle generator gives up and trys a different set + * of initial parameters. + * @param integer $theTrys [optional] The number of attempts at resetting the + * initial parameters before giving up. + * @return array A set of triples suitable for initializing a new Sudoku class + * (@see Sudoku::initializePuzzleFromArray). + */ + + public function generatepuzzle($thedifficultylevel = 10, $themaxiterations = 50, $thetrys = 10) { + $thedifficultylevel = min($thedifficultylevel, 10); + $thedifficultylevel = max($thedifficultylevel, 1); + + $this->thelevel = 0; + $this->thetrys = $thetrys; + $this->themaxiterations = $themaxiterations; + $this->thegenerationiterations = 0; + + for ($thetrys = 0; $thetrys < $this->thetrys; $thetrys++) { + $theavailablepositions = array(); + $thecluespositions = array(); + $theclues = array(); + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + $theavailablepositions[] = array($i, $j); + } + } + + $theinitialstate = $this->_generatepuzzle($theavailablepositions, $thecluespositions, $theclues); + + if ($theinitialstate) { + if ($thedifficultylevel != 10) { + $xxx = array(); + + foreach ($theinitialstate as $yyy) { + $xxx[] = (($yyy[0] - 1) * 9) + ($yyy[1] - 1); + } + /* + * Get rid of the available positions already used in the initial state. + */ + + sort($xxx); + $xxx = array_reverse($xxx); + + foreach ($xxx as $i) { + array_splice($theavailablepositions, $i, 1); + } + + /* + * Easy is defined as the number of derivable clues added to the minimum + * required information to solve the puzzle as returned by _generatePuzzle. + */ + + for ($i = 0; $i < (10 - $thedifficultylevel); $i++) { + $xxx = mt_rand(0, count($theavailablepositions) - 1); + $row = $theavailablepositions[$xxx][0]; + $col = $theavailablepositions[$xxx][1]; + $theinitialstate[] = array($row, $col, $this->theboard[$row][$col]); + array_splice($theavailablepositions, $xxx, 1); + } + } + + return $theinitialstate; + } + + if ($this->thedebug) { + printf("
Too many iterations (%d), %d\n", $this->themaxiterations, $thetrys); + } + $this->sudoku($this->thedebug); + } + + /* + * No solution possible, we guess wrong too many times. + */ + + return array(); + } + + /* + * Sudoku puzzle generator. + * + * This is the routine that does the heavy lifting + * for the puzzle generation. It works by taking a guess for a value of a cell, applying + * the solver, testing the solution, and if it's a valid solution, calling itself + * recursively. If during this process, a solution cannot be found, the generator backs + * up (backtrace in Computer Science parlance) and trys another value. Since the generation + * appears to be an NP complete problem (according to the literature) limits on the number + * of iterations are asserted. Once these limits are passed, the generator gives up and + * makes another try. If enough tries are made, the generator gives up entirely. + * + * @access private + * @param array $theAvailablePositions A set of pairs for all positions which have not been + * filled by the solver or the set of guesses. When we run out of available + * positions, the solution is in hand. + * @param array $theCluesPositions A set of pairs for which values have been set by the + * puzzle generator. + * @param array $theClues A set of values for each pair in $theCluesPositions. + * @return array NULL array if no solution is possible, otherwise a set of triples + * suitable for feeding to {@link Sudoku::initializePuzzleFromArray} + */ + + protected function _generatepuzzle($theavailablepositions, $thecluespositions, $theclues) { + $this->thelevel++; + + $this->thegenerationiterations++; + + /* + * Since the last solution sequence may have eliminated one or more positions by + * generating forced solutions for them, go through the list of available positions + * and get rid of any that have already been solved. + */ + + $j = count($theavailablepositions); + + for ($i = 0; $i < $j; $i++) { + if ($this->theboard[$theavailablepositions[$i][0]] + [$theavailablepositions[$i][1]]->isapplied()) { + array_splice($theavailablepositions, $i, 1); + $i = $i - 1; + $j = $j - 1; + } + } + + if (count($theavailablepositions) == 0) { + /* + * We're done, so we can return the clues and their positions to the caller. + * This test is being done here to accommodate the eventual implementation of + * generation from templates in which partial boards will be fed to the solver + * and then the remaining board fed in. + */ + + for ($i = 0; $i < count($thecluespositions); $i++) { + array_push($thecluespositions[$i], $theclues[$i]); + } + + return $thecluespositions; + } + + /* + * Calculate the coupling for each available position. + * + * "coupling" is a measure of the amount of state affected by any change + * to a given cell. In effect, the larger the coupling, the less constrained + * the state of the cell is and the greater the effect of any change made to + * the cell. There is some literature to this effect associated with Roku puzzles + * (4x4 grid). I'm trying this attempting to find a way to generate consistently + * more difficult Sudoku and it seems to have worked; the clue count drops to 25 or + * fewer, more in line with the numbers predicted by the literature. The remainder + * of the work is likely to be associated with finding better algorithms to solve + * Sudoku (which would have the effect of generating harder ones). + */ + + $thecouplings = array(); + + foreach ($theavailablepositions as $xxx) { + $therowcoupling = $this->therows[$xxx[0]]->coupling($xxx[0], $xxx[1]); + $thecolumncoupling = $this->thecolumns[$xxx[1]]->coupling($xxx[0], $xxx[1]); + $thesquarecoupling = $this->thesquares[$this->_squareindex($xxx[0], $xxx[1])]->coupling($xxx[0], $xxx[1]); + $thecouplings[$therowcoupling + $thecolumncoupling + $thesquarecoupling][] = $xxx; + } + + $themaximumcoupling = max(array_keys($thecouplings)); + + /* + * Pick a spot on the board and get the clues set up. + */ + + $thechoice = mt_rand(0, count($thecouplings[$themaximumcoupling]) - 1; + $thecluespositions[] = $thecouplings[$themaximumcoupling][$thechoice]; + $therow = $thecouplings[$themaximumcoupling][$thechoice][0]; + $thecolumn = $thecouplings[$themaximumcoupling][$thechoice][1]; + + /* + * Capture the necessary global state of the board + */ + + $thecurrentboard = $this->deepcopy($this->theboard); + + /* + * This is all possible states for the chosen cell. All values will be + * randomly tried to see if a solution results. If all solutions fail, + * the we'll back up in time and try again. + */ + + $thepossibleclues = array_keys($this->theboard[$therow][$thecolumn]->getstate()); + + while (count($thepossibleclues) != 0) { + if ($this->thegenerationiterations > $this->themaxiterations) { + $this->thelevel = $this->thelevel - 1; + return array(); + } + + $thecluechoice = mt_rand(0, count($thepossibleclues) - 1); + $thevalue = $thepossibleclues[$thecluechoice]; + array_splice($thepossibleclues, $thecluechoice, 1); + + $theclues[] = $thevalue; + + $this->theboard[$therow][$thecolumn]->flagsolvedposition($thevalue); + + if ($this->thedebug) { + printf("
(%03d, %03d) Trying (%d, %d) = %d\n", $this->theLevel, + $this->thegenerationiterations, $therow, $thecolumn, $thevalue); + } + + $theflag = $this->solve(false); + + if ($this->_validatetrialsolution()) { + if ($theflag) { + /* + * We're done, so we can return the clues and their positions to the caller. + */ + + for ($i = 0; $i < count($thecluespositions); $i++) { + array_push($thecluespositions[$i], $theclues[$i]); + } + + return $thecluespositions; + } else { + $xxx = $this->_generatepuzzle($theavailablepositions, $thecluespositions, $theclues); + if ($xxx) { + return $xxx; + } + } + } + + /* + * We failed of a solution, back out the state and try the next possible value + * for this position. + */ + + $this->theboard = $thecurrentboard; + $this->_buildrcs(); + array_pop($theclues); + } + + $this->thelevel = $this->thelevel - 1; + + /* + * If we get here, we've tried all possible values remaining for the chosen + * position and couldn't get a solution. Back out and try something else. + */ + + return array(); + } + + /* + * Return the contents of the board as a string of digits and blanks. Blanks + * are used where the corresponding board item is an array, indicating the cell + * has not yet been solved. + * + * @desc Get the current state of the board as a string. + * @access public + */ + + public function getboardasstring() { + $thestring = ""; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + $thestring .= $this->theboard[$i][$j]->asstring(); + } + } + + return $thestring; + } + + public function &getcell($r, $c) { + return $this->theboard[$r][$c]; + } + + /* + * Each element of the input array is a triple consisting of (row, column, value). + * Each of these values is in the range 1..9. + * + * @access public + * @param array $theArray + */ + + public function initializepuzzlefromarray($thearray) { + foreach ($thearray as $xxx) { + $c =& $this->getcell($xxx[0], $xxx[1]); + $c->flagsolvedposition($xxx[2]); + } + } + + /* + * Initialize puzzle from an input file. + * + * The input file is a text file, blank or tab delimited, with each line being a + * triple consisting of "row column value". Each of these values is in the range + * 1..9. Input lines that are blank (all whitespace) or which begin with whitespace + * followed by a "#" character are ignored. + * + * @access public + * @param mixed $theHandle [optional] defaults to STDIN. If a string is passed + * instead of a file handle, the file is opened. + */ + + public function initializepuzzlefromfile($thehandle = STDIN) { + $theopenedfileflag = false; + + /* + * If a file name is passed instead of a resource, open the + * file and process it. + */ + + if (is_string($thehandle)) { + $thehandle = fopen($thehandle, "r"); + if ($thehandle === false) { + exit(); + } + } + + $yyy = array(); + + if ($thehandle) { + while (!feof($thehandle)) { + $thestring = trim(fgets($thehandle)); + if (($thestring != "") && + (!preg_match('/^\s*#/', $thestring))) { + $xxx = preg_split('/\s+/', $thestring); + if (!feof($thehandle)) { + $yyy[] = array((int)$xxx[0], (int)$xxx[1], (int)$xxx[2]); + } + } + } + } + + $this->initializepuzzlefromarray($yyy); + + if ($theopenedfileflag) { + fclose( $thehandle); + } + } + + /** + * The input parameter consists of a string of 81 digits and blanks. If fewer characters + * are provide, the string is padded on the right. + * + * @desc Initialize puzzle from a string. + * @access public + * @param string $theString The initial state of each cell in the puzzle. + */ + + public function initializepuzzlefromstring($thestring) { + $thestring = str_pad($thestring, 81, " "); + + for ($i = 0; $i < 81; $i++) { + if ($thestring{$i} != " ") { + $thearray[] = array((int)($i / 9) + 1, ($i % 9) + 1, (int)$thestring{$i}); + } + } + + $this->initializepuzzlefromrray($therray); + } + + /* + * @desc predicate to determine if the current puzzle has been solved. + * @access public + * @return boolean true if the puzzle has been solved. + */ + + public function issolved() { + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + if (!$this->theboard[$i][$j]->issolved()) { + return false; + } + } + } + + return true; + } + + /** + * Convert pending to actual solutions. + * + * This step is actually unnecessary unless you want a pretty output of the + * intermediate. + * + * @access private + * @return boolean True if at least on pending solution existed, false otherwise. + */ + + protected function _newsolvedposition() { + $thereturn = false; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + if ($this->theboard[$i][$j]->solvedstate() == 1) { + $this->theboard[$i][$j]->un_set($this->theboard[$i][$j]->getstate()); + $thereturn = true; + } + } + } + + return $thereturn; + } + + /** + * Print the contents of the board in HTML format. + * + * A "hook" so that extension classes can show all the steps taken by + * the solve function. + * + * @see SudokuIntermediateSolution. + * + * @access private + * @param string $theHeader [optional] The header line to be output along + * with the intermediate solution. + */ + + protected function _printintermediatesolution($theheader = null) { + if ($this->thedebug) { + $this->printsolution( $theheader); + } + } + + /** + * Print the contents of the board in HTML format. + * + * Simple output, is tailored by hand so that an initial state and + * a solution will find nicely upon a single 8.5 x 11 page of paper. + * + * @access public + * @param mixed $theHeader [optional] The header line[s] to be output along + * with the solution. + */ + + public function printsolution($theheader = null) { + if (($this->thedebug) && ($theheader != null)) { + if (is_array($theheader)) { + foreach ($theheader as $aheader) { + print $aheader; + } + } else { + print $theheader; + } + } + + $thecolors = array("green", "blue", "red"); + $thefontsize = array("1em", "1em", ".8em"); + $thefontweight = array("bold", "bold", "lighter"); + + printf("
\n"); + + $thelast = 2; + + for ($i = 1; $i <= 9; $i++) { + if ($thelast == 2) { + printf("\n"); + } + + printf("\n"); + + $thelast = ($i - 1) % 3; + if ($thelast == 2) { + printf("\n"); + } + } + + printf("
\n"); + + $thelast1 = 2; + + for ($j = 1; $j <= 9; $j++) { + if ($thelast1 == 2) { + printf("\n"); + } + + $c = &$this->thesquares[$i]; + $c =& $c->getcell($j); + $thesolvedstate = $c->solvedstate(); + + printf("\n"); + + $thelast1 = ($j - 1) % 3; + if ($thelast1 == 2) { + printf("\n"); + } + } + + printf("
", + $thecolors[$thesolvedstate], + $thefontweight[$thesolvedstate], + $thefontsize[$thesolvedstate]); + $xxx = $c->asstring($this->thedebug); + print ($xxx == " " ? " " : $xxx); + printf("
\n"); + } + + /** + * Solve a Sudoku. + * + * As explained earlier, this works by iterating upon three different + * types of inference: + * + * 1. A negative one, in which a value used within a row/column/square + * may not appear elsewhere within the enclosing row/column/square. + * 2. A positive one, in which any value with is unique in a row + * or column or square must be the solution to that position. + * 3. A tuple based positive one which comes in a number of flavors: + * 3a. The "Pair" rule as stated by the author of the "other" Sudoku + * class on phpclasses.org and generalized by me, e.g., in any RCS + * two cells containing a pair of values eliminate those values from + * consideration in the rest of the RC or S. + * 3b. The n/n+1 set rule as discovered by me, e.g., in any RCS, three cells + * containing the following pattern, (i, j)/(j, k)/(i, j, k) eliminate + * the values i, j, k from consideration in the rest of the RC or S. + * + * During processing I explain which structures (row, column, square) + * are being used to infer solutions. + * + * @access public + * @param boolean $theInitialStateFlag [optional] True if the initial + * state of the board is to be printed upon entry, false + * otherwise. [Default = true] + * @return boolean true if a solution was possible, false otherwise. + */ + + public function solve($theinitialstateflag = true) { + $theheader = "
Initial Position:"; + + do { + do { + $this->_applysolvedpositions(); + if ($theinitialstateflag) { + $this->_printintermediatesolution($theheader); + $theheader = null; + } else { + $theinitialstateflag = true; + $theheader = "
Apply Slice and Dice:"; + } + } while ($this->_newsolvedposition()); + + $therowiteration = false; + + for ($i = 1; $i <= 9; $i++) { + if ($this->theRows[$i]->doninference()) { + $theheader = $this->therows[$i]->getheader(); + $therowiteration = true; + break; + } + } + + $thecolumniteration = false; + + if (!$therowiteration) { + for ($i = 1; $i <= 9; $i++) { + if ($this->thecolumns[$i]->doaninference()) { + $theheader = $this->thecolumns[$i]->getheader(); + $thecolumniteration = true; + break; + } + } + } + + $thesquareiteration = false; + + if (!($therowiteration || $thecolumniteration)) { + for ($i = 1; $i <= 9; $i++) { + if ($this->thesquares[$i]->doaninference()) { + $theheader = $this->thesquares[$i]->getheader(); + $thesquareiteration = true; + break; + } + } + } + } while ($therowiteration || $thecolumniteration || $thesquareiteration); + + return $this->issolved(); + } + + /** + * Here there be dragons. In conversations with other Sudoku folks, I find that there ARE Sudoku with + * unique solutions for which a clue set may be incomplete, i.e., does not lead to a solution. The + * solution may only be found by guessing the next move. I'm of the opinion that this violates the + * definition of Sudoku (in which it's frequently said "never guess") but if it's possible to find + * a solution, this will do it. + * + * The problem is that it can take a LONG time if there ISN'T a solution since this is basically a + * backtracing solution trier. + * + * The basic algorithm is pretty simple: + * + * 1. Find the first unsolved cell. + * 2. For every possible value, substutite value for the cell, apply inferences. + * 3. If a solution was found, we're done. + * 4. Recurse looking for the next cell to try a value for. + * + * There's a bit of bookkeeping to keep the state right when backing up, but that's pretty + * straightforward and looks a lot like that of generatePuzzle. + * + * @desc Brute force additional solutions. + * @access public + * @returns array The clues added sufficient to solve the puzzle. + */ + + public function solvebruteforce($i = 1, $j = 1) { + for (; $i <= 9; $i++) { + for (; $j <= 9; $j++) { + if ($this->theboard[$i][$j]->solvedstate() != 0) { + if ($this->thedebug) { + printf("
Applying Brute Force to %d, %d\n", $i, $j); + } + + $thecurrentboard = $this->deepcopy($this->theboard); + $thevalues = $this->theboard[$i][$j]->getstate(); + + foreach ($thevalues as $thevalue) { + $this->theboard[$i][$j]->flagsolvedposition($thevalue); + + $thesolutionflag = $this->solve(); + $thetrialsolutionflag = $this->_validatetrialsolution(); + + if ($thetrialsolutionflag && $thesolutionflag) { + return array(array($i, $j, $thevalue)); + } + + if ($thetrialsolutionflag) { + $thenewguesses = $this->solvebruteForce($i, $j + 1); + + if ($thenewguesses) { + $thenewguesses[] = array($i, $j, $thevalue); + + return $thenewguesses; + } + } + + if ($this->thedebug) { + printf("
Backing out\n"); + } + + $this->theboard = $thecurrentboard; + $this->_buildrcs(); + } + + return array(); + } + } + } + } + + /** + * @desc Calculate the index of the square containing a specific cell. + * @param integer $theRow the row coordinate. + * @param integer $theColumn the column coordinate. + * @return integer the square index in the range 1..9 + */ + + protected function _squareindex($therow, $thecolumn) { + $theindex = ((int)(($therow - 1) / 3) * 3) + (int)(($thecolumn - 1) / 3) + 1; + return $theindex; + } + + /** + * Validate a complete solution. + * + * After a complete solution has been generated check the board and + * report any inconsistencies. This is primarily intended for debugging + * purposes. + * + * @access public + * @return mixed true if the solution is valid, an array containing the + * error details. + */ + + public function validatesolution() { + $thereturn = array(); + + for ($i = 1; $i <= 9; $i++) { + if (!$this->therows[$i]->validatesolution()) { + $thereturn[0][] = $i; + } + if (!$this->thecolumns[$i]->validatesolution()) { + $thereturn[1][] = $i; + } + if (!$this->thesquares[$i]->validatesolution()) { + $thereturn[2][] = $i; + } + } + + return (count($thereturn) == 0 ? true : $thereturn); + } + + /** + * Validate an entire trial solution. + * + * Used during puzzle generation to determine when to backtrace. + * + * @access private + * @return True when the intermediate soltuion is valid, false otherwise. + */ + + protected function _validatetrialsolution() { + for ($i = 1; $i <= 9; $i++) { + if (!(($this->therows[$i]->validatetrialsolution()) && + ($this->thecolumns[$i]->validatetrialsolution()) && + ($this->thesquares[$i]->validatetrialsolution()))) { + return false; + } + } + + return true; + } +} + +/** + * Extend Sudoku to generate puzzles based on templates. + * + * Templates are either input files or arrays containing doubles. + * + * @package Sudoku + */ + +class SudokuTemplates extends Sudoku +{ + public function sudokutemplates($theDebug = false) { + $this->sudoku($thedebug); + } + + public function generatepuzzlefromfile($thehandle = STDIN, $thedifficultylevel = 10) { + $yyy = array(); + + if ($thehandle) { + while (!feof($thehandle)) { + $thestring = trim(fgets($thehandle)); + $xxx = preg_split("/\s+/", $thestring); + if (!feof($thehandle)) { + $yyy[] = array((int)$xxx[0], (int)$xxx[1]); + } + } + } + + return $this->generatepuzzlefromarray($yyy, $thedifficultylevel); + } + + public function generatepuzzlefromarray($thearray, $thedifficultylevel = 10) { + $this->_generatepuzzle($thearray, array(), array()); + + /* + ** Because the generation process may infer values for some of the + ** template cells, we construct the clues from the board and the + ** input array before continuing to generate the puzzle. + */ + + foreach ($thearray as $thekey => $theposition) { + $thetemplateclues[] = array($theposition[0], $theposition[1], $this->theboard[$theposition[0]][$theposition[1]]); + } + + $theotherclues = $this->generatepuzzle($thedifficultylevel); + + return array_merge($thetemplateclues, $theotherclues); + } +} + +/** + * Extend Sudoku to print all intermediate results. + * + * @package Sudoku + */ + +class sudokuintermediatesolution extends sudoku { + public function sudokuintermediateresults($thedebug = false) { + $this->sudoku($thedebug); + } + + protected function _printintermediatesolution($theheader = null) { + $this->printsolution($theheader); + } +} + +function make_seed() { + list($usec, $sec) = explode(' ', microtime()); + return (float) $sec + ((float) $usec * 100000); +} diff --git a/sudoku/create.php b/sudoku/create.php index 7092736..d797aa1 100755 --- a/sudoku/create.php +++ b/sudoku/create.php @@ -1,144 +1,144 @@ - -
-
- - - - - - -
: -
-

- - - - - - - data = PackSudoku( $si, $sp); - if( strlen( $newrec->data) != 81){ - return 0; - } - $newrec->level = $level; - $newrec->opened = GetOpened( $si); - - $DB->insert_record( 'game_sudoku_database', $newrec, true); - - $level++; - if( $level > $level2){ - $level = $level1; - } - - echo get_string( 'sudoku_creating', 'game', $i)."
\r\n"; - } -} - -function PackSudoku( $si, $sp) -{ - $data = ""; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - $c = &$sp->theSquares[$i]; - $c = &$c->getCell($j) ; - $solution = $c->asString( false); - - $c = &$si->theSquares[$i] ; - $c = &$c->getCell($j) ; - $theSolvedState = $c->solvedState() ; - - if( $theSolvedState == 1) { //hint - $solution = substr( 'ABCDEFGHI', $c->asString( false) - 1, 1); - } - - $data .= $solution; - } - } - - return $data; -} - - -function create( &$si, &$sp, $level=1) -{ - for( $i=1; $i <= 40; $i++) - { - //set_time_limit( 30); - $sp = new Sudoku() ; - $theInitialPosition = $sp->generatePuzzle( 10, 50, $level) ; - if( count( $theInitialPosition)){ - break; - } - } - if( $i > 40){ - return false; - } - - $si = new Sudoku() ; - - $si->initializePuzzleFromArray($theInitialPosition); - - return true; -} - -function GetOpened( $si) -{ - $count = 0; - - for ($i = 1; $i <= 9; $i++) - { - for ($j = 1; $j <= 9; $j++) - { - $c = &$si->theSquares[$i] ; - $c = &$c->getCell($j) ; - $theSolvedState = $c->solvedState() ; - - if( $theSolvedState == 1) //hint - $count++; - } - } - - return $count; -} - +. + +require( "../../../config.php"); +require_once("class.Sudoku.php"); +require( '../header.php'); + +$action = optional_param('action', PARAM_ALPHA); // The action. + +if ($action == 'create') { + AppendSudokuB(); +} else { + showform(); +} + +function showform() { + $id = required_param('id', PARAM_NUMBER); // The action. + +?> +
+
+ + + + + + +
: +
+

+ + + + + + +data = packsudoku( $si, $sp); + if (strlen( $newrec->data) != 81) { + return 0; + } + $newrec->level = $level; + $newrec->opened = GetOpened( $si); + + $DB->insert_record( 'game_sudoku_database', $newrec, true); + + $level++; + if ($level > $level2) { + $level = $level1; + } + + echo get_string( 'sudoku_creating', 'game', $i)."
\r\n"; + } +} + +function packsudoku( $si, $sp) { + $data = ''; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + $c = &$sp->thesquares[$i]; + $c = &$c->getcell($j); + $solution = $c->asstring( false); + + $c = &$si->thesquares[$i]; + $c = &$c->getCell($j); + $thesolvedstate = $c->solvedstate(); + + if ($thesolvedstate == 1) { + // Hint. + $solution = substr( 'ABCDEFGHI', $c->asString( false) - 1, 1); + } + + $data .= $solution; + } + } + + return $data; +} + +function create( &$si, &$sp, $level=1) { + for ($i = 1; $i <= 40; $i++) { + $sp = new sudoku(); + $theinitialposition = $sp->generatepuzzle( 10, 50, $level); + if (count( $theinitialposition)) { + break; + } + } + if ($i > 40) { + return false; + } + + $si = new sudoku(); + + $si->initializepuzzlefromarray( $theinitialposition); + + return true; +} + +function getopened( $si) { + $count = 0; + + for ($i = 1; $i <= 9; $i++) { + for ($j = 1; $j <= 9; $j++) { + $c = &$si->thesquares[$i]; + $c = &$c->getcell($j); + $thesolvedstate = $c->solvedstate(); + + if ($thesolvedstate == 1) { + // Hint. + $count++; + } + } + } + + return $count; +} diff --git a/sudoku/export.php b/sudoku/export.php index a4e1d88..9096b6a 100755 --- a/sudoku/export.php +++ b/sudoku/export.php @@ -1,33 +1,45 @@ -. require( "../../../config.php"); export(); -function export() -{ - global $CFG; - - - $file = "import.php"; - $h = fopen($file, 'w') or die("can't open file"); - - fwrite( $h, "level, $rec->opened, '$rec->data')\", false);\r\n"); - if( ++$i % 10 == 0) - fwrite( $h, "\r\n"); - } - fwrite( $h, "\r\necho'Finished importing';"); - - fclose($h); +function export() { + global $CFG; + $file = "import.php"; + $h = fopen($file, 'w') or die("can't open file"); + + fwrite( $h, "level, $rec->opened, '$rec->data')\", false);\r\n"); + if (++$i % 10 == 0) { + fwrite( $h, "\r\n"); + } + } + fwrite( $h, "\r\necho'Finished importing';"); + + fclose($h); } diff --git a/sudoku/play.php b/sudoku/play.php index 172782c..a2f1f66 100755 --- a/sudoku/play.php +++ b/sudoku/play.php @@ -1,151 +1,167 @@ -. require_once( "../../lib/questionlib.php"); -function game_sudoku_continue( $id, $game, $attempt, $sudoku, $endofgame, $context) -{ - global $CFG, $DB, $USER; - - if( $endofgame){ - game_updateattempts( $game, $attempt, -1, true); - $endofgame = false; - } - - if( $attempt != false and $sudoku != false){ - return game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); - } - - if( $attempt == false){ - $attempt = game_addattempt( $game); - } - - //new game - srand( (double)microtime()*1000000); - - $recsudoku = getrandomsudoku(); - if( $recsudoku == false){ - print_error( 'Empty sudoku database'); - } +function game_sudoku_continue( $id, $game, $attempt, $sudoku, $endofgame, $context) { + global $CFG, $DB, $USER; + + if ($endofgame) { + game_updateattempts( $game, $attempt, -1, true); + $endofgame = false; + } + + if ($attempt != false and $sudoku != false) { + return game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); + } + + if ($attempt == false) { + $attempt = game_addattempt( $game); + } + + // New game. + srand( (double)microtime() * 1000000); + + $recsudoku = getrandomsudoku(); + if ($recsudoku == false) { + print_error( 'Empty sudoku database'); + } $newrec = new stdClass(); - $newrec->id = $attempt->id; - $newrec->guess = ''; - $newrec->data = $recsudoku->data; - $newrec->opened = $recsudoku->opened; - - $need = 81 - $recsudoku->opened; - $closed = game_sudoku_getclosed( $newrec->data); - $n = min( count($closed), $need); - //if the teacher set the maximum number of questions - if( $game->param2 > 0){ - if( $game->param2 < $n){ - $n = $game->param2; - } - } - $recs = game_questions_selectrandom( $game, CONST_GAME_TRIES_REPETITION*$n); - - if( $recs === false){ - mysql_execute( "DELETE FROM {game_sudoku} WHERE id={$game->id}"); - print_error( get_string( 'no_questions', 'game')); - } - - $closed = array_rand($closed, $n); - - $selected_recs = game_select_from_repetitions( $game, $recs, $n); - - if(!game_insert_record('game_sudoku', $newrec)){ - print_error('error inserting in game_sudoku'); - } - - $i = 0; + $newrec->id = $attempt->id; + $newrec->guess = ''; + $newrec->data = $recsudoku->data; + $newrec->opened = $recsudoku->opened; + + $need = 81 - $recsudoku->opened; + $closed = game_sudoku_getclosed( $newrec->data); + $n = min( count($closed), $need); + // If the teacher set the maximum number of questions. + if ($game->param2 > 0) { + if ($game->param2 < $n) { + $n = $game->param2; + } + } + $recs = game_questions_selectrandom( $game, CONST_GAME_TRIES_REPETITION * $n); + + if ($recs === false) { + $sql = "DELETE FROM {game_sudoku} WHERE id={$game->id}"; + $DB->execute( $sql); + print_error( get_string( 'no_questions', 'game')); + } + + $closed = array_rand($closed, $n); + + $selectedrecs = game_select_from_repetitions( $game, $recs, $n); + + if (!game_insert_record('game_sudoku', $newrec)) { + print_error('error inserting in game_sudoku'); + } + + $i = 0; $field = ($game->sourcemodule == 'glossary' ? 'glossaryentryid' : 'questionid'); - foreach( $recs as $rec) - { - if( $game->sourcemodule == 'glossary') - $key = $rec->glossaryentryid; - else - $key = $rec->questionid; - - if( !array_key_exists( $key, $selected_recs)) + foreach ($recs as $rec) { + if ($game->sourcemodule == 'glossary') { + $key = $rec->glossaryentryid; + } else { + $key = $rec->questionid; + } + + if (!array_key_exists( $key, $selectedrecs)) { continue; + } - $query = new stdClass(); - $query->attemptid = $newrec->id; - $query->gamekind = $game->gamekind; - $query->gameid = $game->id; - $query->userid = $USER->id; - $query->col = $closed[ $i++]; - $query->sourcemodule = $game->sourcemodule; - $query->questionid = $rec->questionid; - $query->glossaryentryid = $rec->glossaryentryid; - $query->score = 0; - if( ($query->id = $DB->insert_record( 'game_queries', $query)) == 0){ - print_error( 'error inserting in game_queries'); - } + $query = new stdClass(); + $query->attemptid = $newrec->id; + $query->gamekind = $game->gamekind; + $query->gameid = $game->id; + $query->userid = $USER->id; + $query->col = $closed[ $i++]; + $query->sourcemodule = $game->sourcemodule; + $query->questionid = $rec->questionid; + $query->glossaryentryid = $rec->glossaryentryid; + $query->score = 0; + if (($query->id = $DB->insert_record( 'game_queries', $query)) == 0) { + print_error( 'error inserting in game_queries'); + } game_update_repetitions($game->id, $USER->id, $query->questionid, $query->glossaryentryid); - } + } - game_updateattempts( $game, $attempt, 0, 0); + game_updateattempts( $game, $attempt, 0, 0); - game_sudoku_play( $id, $game, $attempt, $newrec, false, false, $context); + game_sudoku_play( $id, $game, $attempt, $newrec, false, false, $context); } -function game_sudoku_play( $id, $game, $attempt, $sudoku, $onlyshow, $showsolution, $context) -{ +function game_sudoku_play( $id, $game, $attempt, $sudoku, $onlyshow, $showsolution, $context) { $offsetquestions = game_sudoku_compute_offsetquestions( $game->sourcemodule, $attempt, $numbers, $correctquestions); - if( $game->toptext != ''){ - echo $game->toptext.'
'; - } - - game_sudoku_showsudoku( $sudoku->data, $sudoku->guess, true, $showsolution, $offsetquestions, $correctquestions, $id, $attempt, $game); - switch( $game->sourcemodule) - { - case 'quiz': - case 'question': - game_sudoku_showquestions_quiz( $id, $game, $attempt, $sudoku, $offsetquestions, $numbers, $correctquestions, $onlyshow, $showsolution, $context); - break; - case 'glossary': - game_sudoku_showquestions_glossary( $id, $game, $attempt, $sudoku, $offsetquestions, $numbers, $correctquestions, $onlyshow, $showsolution); - break; - } - - if( $game->bottomtext != ''){ - echo '
'.$game->bottomtext; - } + if ($game->toptext != '') { + echo $game->toptext.'
'; + } + + game_sudoku_showsudoku( $sudoku->data, $sudoku->guess, true, $showsolution, $offsetquestions, + $correctquestions, $id, $attempt, $game); + switch ($game->sourcemodule) { + case 'quiz': + case 'question': + game_sudoku_showquestions_quiz( $id, $game, $attempt, $sudoku, $offsetquestions, + $numbers, $correctquestions, $onlyshow, $showsolution, $context); + break; + case 'glossary': + game_sudoku_showquestions_glossary( $id, $game, $attempt, $sudoku, $offsetquestions, + $numbers, $correctquestions, $onlyshow, $showsolution); + break; + } + + if ($game->bottomtext != '') { + echo '
'.$game->bottomtext; + } } -//returns a map with an offset and id of each question -function game_sudoku_compute_offsetquestions( $sourcemodule, $attempt, &$numbers, &$correctquestions) -{ - global $CFG,$DB; +// Returns a map with an offset and id of each question. +function game_sudoku_compute_offsetquestions( $sourcemodule, $attempt, &$numbers, &$correctquestions) { + global $CFG, $DB; $select = "attemptid = $attempt->id"; - $fields = 'id, col, score'; - switch( $sourcemodule) - { - case 'quiz': - case 'question': - $fields .= ',questionid as id2'; - break; - case 'glossary': - $fields .= ',glossaryentryid as id2'; - break; - } - if( ($recs = $DB->get_records_select( 'game_queries', $select, null, '', $fields)) == false){ - $DB->execute( "DELETE FROM {$CFG->prefix}game_sudoku WHERE id={$attempt->id}"); + $fields = 'id, col, score'; + switch( $sourcemodule) + { + case 'quiz': + case 'question': + $fields .= ',questionid as id2'; + break; + case 'glossary': + $fields .= ',glossaryentryid as id2'; + break; + } + if (($recs = $DB->get_records_select( 'game_queries', $select, null, '', $fields)) == false) { + $DB->execute( "DELETE FROM {$CFG->prefix}game_sudoku WHERE id={$attempt->id}"); print_error( 'There are no questions '.$attempt->id); } $offsetquestions = array(); $numbers = array(); - $correctquestions = array(); - foreach( $recs as $rec){ + $correctquestions = array(); + foreach ($recs as $rec) { $offsetquestions[ $rec->col] = $rec->id2; $numbers[ $rec->id2] = $rec->col; - if($rec->score == 1) + if ( $rec->score == 1) { $correctquestions[ $rec->col] = 1; + } } ksort( $offsetquestions); @@ -153,479 +169,459 @@ function game_sudoku_compute_offsetquestions( $sourcemodule, $attempt, &$numbers return $offsetquestions; } -function getrandomsudoku() -{ - global $DB; +function getrandomsudoku() { + global $DB; $count = $DB->count_records( 'game_sudoku_database'); - if( $count == 0) - { + if ($count == 0) { require_once(dirname(__FILE__) . '/../db/importsudoku.php'); $count = $DB->count_records( 'game_sudoku_database'); - if( $count == 0) - return false; + if ($count == 0) { + return false; + } } - $i = mt_rand( 0, $count - 1); + $i = mt_rand( 0, $count - 1); - if( ($recs = $DB->get_records( 'game_sudoku_database', null, '', '*', $i, 1)) != false) - { - foreach( $recs as $rec){ - return $rec; - } - } + if (($recs = $DB->get_records( 'game_sudoku_database', null, '', '*', $i, 1)) != false) { + foreach ($recs as $rec) { + return $rec; + } + } - return false; + return false; } +function game_sudoku_getclosed( $data) { + $a = array(); -function game_sudoku_getclosed( $data) -{ - $a = array(); - - $n = game_strlen( $data); - for( $i=1; $i <= $n; $i++) - { - $c = game_substr( $data, $i-1, 1); - if( $c >= "1" and $c <= "9") - $a[ $i] = $i; - } + $n = game_strlen( $data); + for ($i = 1; $i <= $n; $i++) { + $c = game_substr( $data, $i - 1, 1); + if ($c >= "1" and $c <= "9") { + $a[ $i] = $i; + } + } - return $a; + return $a; } -function game_sudoku_showsudoku( $data, $guess, $bShowLegend, $bShowSolution, $offsetquestions, $correctquestions, $id, $attempt, $game) -{ - global $CFG, $DB; - - $correct = $count = 0; - - echo "
\r\n"; - echo ''; - $pos=0; - for( $i=0; $i <= 2; $i++) - { - echo ""; - for( $j=0; $j <= 2; $j++) - { - echo '\r\n"; - } - echo ""; +function game_sudoku_showsudoku( $data, $guess, $bshowlegend, $bshowsolution, $offsetquestions, + $correctquestions, $id, $attempt, $game) { + global $CFG, $DB; + + $correct = $count = 0; + + echo "
\r\n"; + echo '
'; - for( $k1=0; $k1 <= 2; $k1++) - { - echo ""; - for( $k2=0; $k2 <= 2; $k2++) - { - $s = substr( $data, $pos, 1); - $g = substr( $guess, $pos, 1); - $pos++; - if( $g != 0){ - $s = $g; - } - if( $s >= "1" and $s <= "9") - { - //closed number - if( $bShowLegend) - { - //show legend - if( $bShowSolution == false) - { - if( !array_key_exists( $pos, $correctquestions)){ - if( array_key_exists( $pos, $offsetquestions)) - { - if( $s != $g){ - $s = ''; - } - }else if( $g == 0) - { - $s = ''; - } - }else - { - //correct question - $count++; - } - } - echo ''; - }else - { - //not show legend - echo ''; - } - }else - { - $s = strpos( "-ABCDEFGHI", $s); - $count++; - echo ''; - } - } - echo ""; - } - echo "
'.$s.' '.$s.'
'; + $pos = 0; + for ($i = 0; $i <= 2; $i++) { + echo ""; + for ($j = 0; $j <= 2; $j++) { + echo '\r\n"; + } + echo ""; } - echo "
'; + for ($k1 = 0; $k1 <= 2; $k1++) { + echo ""; + for ($k2 = 0; $k2 <= 2; $k2++) { + $s = substr( $data, $pos, 1); + $g = substr( $guess, $pos, 1); + $pos++; + if ($g != 0) { + $s = $g; + } + if ($s >= "1" and $s <= "9") { + // Closed number. + if ($bshowlegend) { + // Show legend. + if ($bshowsolution == false) { + if (!array_key_exists( $pos, $correctquestions)) { + if (array_key_exists( $pos, $offsetquestions)) { + if ($s != $g) { + $s = ''; + } + } else if ($g == 0) { + $s = ''; + } + } else { + // Correct question. + $count++; + } + } + echo ''; + } else { + // Not show legend. + echo ''; + } + } else { + $s = strpos( "-ABCDEFGHI", $s); + $count++; + echo ''; + } + } + echo ""; + } + echo "
'.$s.' '.$s.'
\r\n"; - - ?> + echo "\r\n"; + $href = $CFG->wwwroot.'/mod/game/attempt.php?action=sudokucheckn&id='.$id; + +?> timefinish){ - return $count; - } - - if( count($offsetquestions) != count( $correctquestions)){ - return $count; - } - - if (! $cm = $DB->get_record( 'course_modules', array( 'id' => $id))) { - print_error( "Course Module ID was incorrect id=$id"); - } - - echo '
'.get_string( 'win', 'game').'

'; - echo '
'; - echo "wwwroot/mod/game/attempt.php?id=$id\">".get_string( 'nextgame', 'game').'         '; - echo "wwwroot/course/view.php?id=$cm->course\">".get_string( 'finish', 'game').' '; - - game_updateattempts( $game, $attempt, 1, 1); + // Here are the congratulations. + if ($attempt->timefinish) { + return $count; + } + + if (count($offsetquestions) != count( $correctquestions)) { + return $count; + } + + if (! $cm = $DB->get_record( 'course_modules', array( 'id' => $id))) { + print_error( "Course Module ID was incorrect id=$id"); + } + + echo '
'.get_string( 'win', 'game').'

'; + echo '
'; + echo "wwwroot/mod/game/attempt.php?id=$id\">". + get_string( 'nextgame', 'game').'         '; + echo "wwwroot/course/view.php?id=$cm->course\">".get_string( 'finish', 'game').' '; + + game_updateattempts( $game, $attempt, 1, 1); return $count; } - -function game_sudoku_getquestionlist( $offsetquestions) -{ +function game_sudoku_getquestionlist( $offsetquestions) { $questionlist = ''; - foreach( $offsetquestions as $q){ - if( $q != 0){ + foreach ($offsetquestions as $q) { + if ($q != 0) { $questionlist .= ','.$q; } } $questionlist = substr( $questionlist, 1); - + if ($questionlist == '') { print_error( get_string('no_questions', 'game')); } - return $questionlist; + return $questionlist; } -function game_sudoku_getglossaryentries( $game, $offsetentries, &$entrylist, $numbers) -{ - global $DB; +function game_sudoku_getglossaryentries( $game, $offsetentries, &$entrylist, $numbers) { + global $DB; $entrylist = implode( ',', $offsetentries); - + if ($entrylist == '') { print_error( get_string( 'sudoku_noentriesfound', 'game')); } - // Load the questions + // Load the questions. if (!$entries = $DB->get_records_select( 'glossary_entries', "id IN ($entrylist)")) { print_error( get_string('sudoku_noentriesfound', 'game')); } - + return $entries; } -function game_sudoku_showquestions_quiz( $id, $game, $attempt, $sudoku, $offsetquestions, $numbers, $correctquestions, $onlyshow, $showsolution, $context) -{ - global $CFG; +function game_sudoku_showquestions_quiz( $id, $game, $attempt, $sudoku, $offsetquestions, $numbers, + $correctquestions, $onlyshow, $showsolution, $context) { + global $CFG; - $questionlist = game_sudoku_getquestionlist( $offsetquestions); + $questionlist = game_sudoku_getquestionlist( $offsetquestions); $questions = game_sudoku_getquestions( $questionlist); - - //I will sort with the number of each question - $questions2 = array(); - foreach( $questions as $q){ - $ofs = $numbers[ $q->id]; - $questions2[ $ofs] = $q; - } - ksort( $questions2); - - if( count( $questions2) == 0){ - game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku); - return; - } - - $number=0; - $found = false; + + // I will sort with the number of each question. + $questions2 = array(); + foreach ($questions as $q) { + $ofs = $numbers[ $q->id]; + $questions2[ $ofs] = $q; + } + ksort( $questions2); + + if (count( $questions2) == 0) { + game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku); + return; + } + + $number = 0; + $found = false; foreach ($questions2 as $question) { $ofs = $numbers[ $question->id]; - if( array_key_exists( $ofs, $correctquestions)){ - continue; //I don't show the correct answers + if (array_key_exists( $ofs, $correctquestions)) { + continue; // I don't show the correct answers. } - - if( $found == false) - { + + if ( $found == false) { $found = true; - // Start the form - echo "
wwwroot}/mod/game/attempt.php\" onclick=\"this.autocomplete='off'\">\n"; - if( ($onlyshow === false) and ($showsolution === false)){ - echo "
"; - - echo "      "; - } - - // Add a hidden field with the quiz id + // Start the form. + echo "wwwroot}/mod/game/attempt.php\" onclick=\"this.autocomplete='off'\">\n"; + if (($onlyshow === false) and ($showsolution === false)) { + echo "
"; + + echo "      "; + } + + // Add a hidden field with the quiz id. echo '
'; echo '\n"; echo ''; - // Print all the questions + // Print all the questions. - // Add a hidden field with questionids - echo '\n"; + // Add a hidden field with questionids. + echo '\n"; } - - $number = "A$ofs"; - - game_print_question( $game, $question, $context); + + $number = "A$ofs"; + + game_print_question( $game, $question, $context); } - if( $found) - { + if ($found) { echo "
"; - // Finish the form + // Finish the form. echo ''; - if( ($onlyshow === false) and ($showsolution === false)){ - echo "
\n"; - } + if (($onlyshow === false) and ($showsolution === false)) { + echo "
\n"; + } echo "\n"; } } -//show the sudoku and glossaryentries -function game_sudoku_showquestions_glossary( $id, $game, $attempt, $sudoku, $offsetentries, $numbers, $correctentries, $onlyshow, $showsolution) -{ - global $CFG; - +// Show the sudoku and glossaryentries. +function game_sudoku_showquestions_glossary( $id, $game, $attempt, $sudoku, $offsetentries, $numbers, + $correctentries, $onlyshow, $showsolution) { + global $CFG; + $entries = game_sudoku_getglossaryentries( $game, $offsetentries, $questionlist, $numbers); - //I will sort with the number of each question - $entries2 = array(); - foreach( $entries as $q){ - $ofs = $numbers[ $q->id]; - $entries2[ $ofs] = $q; - } - ksort( $entries2); - - if( count( $entries2) == 0){ - game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku); - return; - } - - /// Start the form - echo "
wwwroot}/mod/game/attempt.php\" onclick=\"this.autocomplete='off'\">\n"; - - if( $onlyshow) - $hasquestions = false; - else - $hasquestions = ( count($correctentries) < count( $entries2)); - - if( $hasquestions){ - echo "
\n"; - } - - // Add a hidden field with the quiz id + // I will sort with the number of each question. + $entries2 = array(); + foreach ($entries as $q) { + $ofs = $numbers[ $q->id]; + $entries2[ $ofs] = $q; + } + ksort( $entries2); + + if (count( $entries2) == 0) { + game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku); + return; + } + + // Start the form. + echo "
wwwroot}/mod/game/attempt.php\" onclick=\"this.autocomplete='off'\">\n"; + + if ($onlyshow) { + $hasquestions = false; + } else { + $hasquestions = ( count($correctentries) < count( $entries2)); + } + + if ($hasquestions) { + echo "
\n"; + } + + // Add a hidden field with the quiz id. echo '
'; echo '\n"; echo ''; - /// Print all the questions + // Print all the questions. - // Add a hidden field with questionids + // Add a hidden field with questionids. echo '\n"; - $number=0; + $number = 0; foreach ($entries2 as $entry) { $ofs = $numbers[ $entry->id]; - if( array_key_exists( $ofs, $correctentries)){ - continue; //I don't show the correct answers + if (array_key_exists( $ofs, $correctentries)) { + continue; // I don't show the correct answers. } $query = new StdClass; $query->glossaryid = $game->glossaryid; $query->glossaryentryid = $entry->id; $s = 'A'.$ofs.'. '.game_show_query( $game, $query, $entry->definition, 0).'
'; - if( $showsolution){ - $s .= get_string( 'answer').': '; - $s .= "id}\" value=\"$entry->concept\"size=30 />
"; - }else if( $onlyshow === false){ - $s .= get_string( 'answer').': '; - $s .= "id}\" size=30 />
"; - } - echo $s."
\r\n"; + if ($showsolution) { + $s .= get_string( 'answer').': '; + $s .= "id}\" value=\"$entry->concept\"size=30 />
"; + } else if ($onlyshow === false) { + $s .= get_string( 'answer').': '; + $s .= "id}\" size=30 />
"; + } + echo $s."
\r\n"; } echo "
"; - // Finish the form - if( $hasquestions){ - echo "
\n"; - } + // Finish the form. + if ($hasquestions) { + echo "
\n"; + } echo "
\n"; } -function game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku) -{ - if( !set_field( 'game_attempts', 'finish', 1, 'id', $attempt->id)){ - print_error( "game_sudoku_showquestion_onfinish: Can't update game_attempts id=$attempt->id"); - } - - echo ''.get_string( 'win', 'game').'
'; - echo '
'; - echo "wwwroot}/mod/game/attempt.php?id=$id\">".get_string( 'nextgame', 'game').'         '; - echo "wwwroot}?id=$id\">".get_string( 'finish', 'game').' '; +function game_sudoku_showquestion_onfinish( $id, $game, $attempt, $sudoku) { + if (!set_field( 'game_attempts', 'finish', 1, 'id', $attempt->id)) { + print_error( "game_sudoku_showquestion_onfinish: Can't update game_attempts id=$attempt->id"); + } + + echo ''.get_string( 'win', 'game').'
'; + echo '
'; + echo "wwwroot}/mod/game/attempt.php?id=$id\">". + get_string( 'nextgame', 'game').'         '; + echo "wwwroot}?id=$id\">".get_string( 'finish', 'game').' '; } -function game_sudoku_checkanswers() -{ +function game_sudoku_checkanswers() { $responses = data_submitted(); $actions = question_extract_responses($questions, $responses, $event); } -function game_sudoku_check_questions( $id, $game, $attempt, $sudoku, $finishattempt, $course) -{ +function game_sudoku_check_questions( $id, $game, $attempt, $sudoku, $finishattempt, $course) { global $QTYPES, $DB; $responses = data_submitted(); $offsetquestions = game_sudoku_compute_offsetquestions( $game->sourcemodule, $attempt, $numbers, $correctquestions); - $questionlist = game_sudoku_getquestionlist( $offsetquestions); - + $questionlist = game_sudoku_getquestionlist( $offsetquestions); + $questions = game_sudoku_getquestions( $questionlist); - foreach($questions as $question) { - $query = new stdClass(); + foreach ($questions as $question) { + $query = new stdClass(); $select = "attemptid=$attempt->id"; $select .= " AND questionid=$question->id"; - - if( ($query->id = $DB->get_field_select( 'game_queries', 'id', $select)) == 0){ - die( "problem game_sudoku_check_questions (select=$select)"); + + if (($query->id = $DB->get_field_select( 'game_queries', 'id', $select)) == 0) { + die( "problem game_sudoku_check_questions (select=$select)"); continue; - } - + } + $grade = game_grade_responses( $question, $responses, 100, $answertext); - if( $grade < 99){ - //wrong answer - game_update_queries( $game, $attempt, $query, $grade/100, $answertext); + if ($grade < 99) { + // Wrong answer. + game_update_queries( $game, $attempt, $query, $grade / 100, $answertext); continue; } - //correct answer - game_update_queries( $game, $attempt, $query, 1, $answertext); + // Correct answer. + game_update_queries( $game, $attempt, $query, 1, $answertext); } game_sudoku_check_last( $id, $game, $attempt, $sudoku, $finishattempt, $course); } -function game_sudoku_check_glossaryentries( $id, $game, $attempt, $sudoku, $finishattempt, $course) -{ +function game_sudoku_check_glossaryentries( $id, $game, $attempt, $sudoku, $finishattempt, $course) { global $QTYPES, $DB; $responses = data_submitted(); - //this function returns offsetentries, numbers, correctquestions + // This function returns offsetentries, numbers, correctquestions. $offsetentries = game_sudoku_compute_offsetquestions( $game->sourcemodule, $attempt, $numbers, $correctquestions); - $entrieslist = game_sudoku_getquestionlist( $offsetentries ); + $entrieslist = game_sudoku_getquestionlist( $offsetentries ); - // Load the glossary entries + // Load the glossary entries. if (!($entries = $DB->get_records_select( 'glossary_entries', "id IN ($entrieslist)"))) { print_error( get_string('noglossaryentriesfound', 'game')); } - foreach($entries as $entry) { + foreach (entries as $entry) { $answerundefined = optional_param('resp'.$entry->id, 'undefined', PARAM_TEXT); - if( $answerundefined == 'undefined'){ + if ($answerundefined == 'undefined') { continue; } $answer = optional_param('resp'.$entry->id, '', PARAM_TEXT); - if( $answer == ''){ - continue; - } - if( game_upper( $entry->concept) != game_upper( $answer)){ - continue; - } - //correct answer + if ($answer == '') { + continue; + } + if (game_upper( $entry->concept) != game_upper( $answer)) { + continue; + } + // Correct answer. $select = "attemptid=$attempt->id"; $select .= " AND glossaryentryid=$entry->id AND col>0"; - $select .= " AND questiontext is null"; // check the student guesses not source glossary entry. - - $query = new stdClass(); - if( ($query->id = $DB->get_field_select( 'game_queries', 'id', $select)) == 0){ - echo "not found $select
"; + // Check the student guesses not source glossary entry. + $select .= " AND questiontext is null"; + + $query = new stdClass(); + if (($query->id = $DB->get_field_select( 'game_queries', 'id', $select)) == 0) { + echo "not found $select
"; continue; } game_update_queries( $game, $attempt, $query, 1, $answer); } - game_sudoku_check_last( $id, $game, $attempt, $sudoku, $finishattempt, $course); + game_sudoku_check_last( $id, $game, $attempt, $sudoku, $finishattempt, $course); return true; } +// This is the last function after submiting the answers. +function game_sudoku_check_last( $id, $game, $attempt, $sudoku, $finishattempt, $course) { + global $CFG, $DB; -//this is the last function after submiting the answers. -function game_sudoku_check_last( $id, $game, $attempt, $sudoku, $finishattempt, $course) -{ - global $CFG, $DB; - - $correct = $DB->get_field_select( 'game_queries', 'COUNT(*) AS c', "attemptid=$attempt->id AND score > 0.9"); - $all = $DB->get_field_select( 'game_queries', 'COUNT(*) AS c', "attemptid=$attempt->id"); - - if( $all){ - $grade = $correct / $all; - }else - { - $grade = 0; - } - game_updateattempts( $game, $attempt, $grade, $finishattempt); + $correct = $DB->get_field_select( 'game_queries', 'COUNT(*) AS c', "attemptid=$attempt->id AND score > 0.9"); + $all = $DB->get_field_select( 'game_queries', 'COUNT(*) AS c', "attemptid=$attempt->id"); + + if ($all) { + $grade = $correct / $all; + } else { + $grade = 0; + } + game_updateattempts( $game, $attempt, $grade, $finishattempt); } -function game_sudoku_check_number( $id, $game, $attempt, $sudoku, $pos, $num, $context) -{ +function game_sudoku_check_number( $id, $game, $attempt, $sudoku, $pos, $num, $context) { global $DB; - $correct = game_substr( $sudoku->data, $pos-1, 1); - - if( $correct != $num) - { - game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); - return; - } - - $leng = game_strlen( $sudoku->guess); - $lend = game_strlen( $sudoku->data); - if( $leng < $lend){ - $sudoku->guess .= str_repeat( ' ', $lend - $leng); - } - game_setchar( $sudoku->guess, $pos-1, $correct); - - if( !$DB->set_field_select('game_sudoku', 'guess', $sudoku->guess, "id=$sudoku->id")){ - print_error( 'game_sudoku_check_number: Cannot update table game_sudoku'); - } - - game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); + $correct = game_substr( $sudoku->data, $pos - 1, 1); + + if ($correct != $num) { + game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); + return; + } + + $leng = game_strlen( $sudoku->guess); + $lend = game_strlen( $sudoku->data); + if ($leng < $lend) { + $sudoku->guess .= str_repeat( ' ', $lend - $leng); + } + game_setchar( $sudoku->guess, $pos - 1, $correct); + + if (!$DB->set_field_select('game_sudoku', 'guess', $sudoku->guess, "id=$sudoku->id")) { + print_error( 'game_sudoku_check_number: Cannot update table game_sudoku'); + } + + game_sudoku_play( $id, $game, $attempt, $sudoku, false, false, $context); } diff --git a/sudoku/sdd/class.SDD.php b/sudoku/sdd/class.SDD.php index 849fa16..90f7b27 100755 --- a/sudoku/sdd/class.SDD.php +++ b/sudoku/sdd/class.SDD.php @@ -1,495 +1,420 @@ - - * @copyright copyright @ by Dick Munroe, 2004 - * @license http://opensource.org/licenses/gpl-license.php GNU Public License - * @package StructuredDataDumper - * @version 1.0.4 - */ - -// -// Edit History: -// -// Dick Munroe munroe@cworks.com 04-Dec-2004 -// Initial version created. -// -// Dick Munroe munroe@csworks.com 08-Dec-2004 -// Translate < to < for html output. -// -// Dick Munroe munroe@csworks.com 23-Dec-2004 -// Add interface for writing "stuff". Extend SDD -// to get things "written". -// -// Dick Munroe munroe@csworks.com 25-Dec-2004 -// If a class extends a base class, but doesn't add -// data members, a warning winds up appearing when -// printing. -// Added a memeber to fetch the state of the logging -// flag. -// -// Dick Munroe munroe@csworks.com 11-Mar-2006 -// The test for html flag should have assumed that -// $this can be defined for objects calling SDD::dump. -// -// Dick Munroe (munroe@csworks.com) 22-Mar-2006 -// Add a function to generate "newlines". -// - -class SDD -{ - /** - * HTML to be generated flag. - */ - - var $m_htmlFlag ; - - /** - * logging flag. - */ - - var $m_logging = false ; - - /** - * In memory log file. - */ - - var $m_log = array() ; - - /** - * Constructor. - * - * @access public - * @param boolean $theHTMLFlag [optional] True if HTML is to be generated. - * If omitted, $_SERVER is used to "guess" the state of - * the HTML flag. Be default, HTML is generated when - * accessed by a web server. - * @param boolean $theLoggingFlag [optional] the state of logging for - * this object. By default, logging is off. - */ - - function SDD($theHtmlFlag=null, $theLoggingFlag=false) - { - if ($theHtmlFlag === null) - { - $theHtmlFlag = (!empty($_SERVER['DOCUMENT_ROOT'])) ; - } - - $this->m_htmlFlag = $theHtmlFlag ; - $this->m_logging = $theLoggingFlag ; - } - - /** - * Close the log file. - * - * @access public - * @abstract - */ - - function close() - { - } - - /** - * Dump a structured variable. - * - * @static - * @param mixed $theVariable the variable to be dumped. - * @param boolean $theHtmlFlag [optional] true if HTML is to be generated, - * false if plain text is to be generated, null (default) if - * dump is to guess which to display. - * @return string The data to be displayed. - * @link http://www.php.net/manual/en/reserved.variables.php#reserved.variables.server Uses $_SERVER - */ - - function dump(&$theVariable, $theHtmlFlag=null) - { - if ($theHtmlFlag === null) - { - if (empty($this)) - { - $theHtmlFlag = (!empty($_SERVER['DOCUMENT_ROOT'])) ; - } - else - { - if (is_subclass_of($this, "sdd")) - { - $theHtmlFlag = $this->m_htmlFlag ; - } - else - { - $theHtmlFlag = (!empty($_SERVER['DOCUMENT_ROOT'])) ; - } - } - } - - switch (gettype($theVariable)) - { - case 'array': - { - return SDD::dArray($theVariable, $theHtmlFlag) ; - } - - case 'object': - { - return SDD::dObject($theVariable, $theHtmlFlag) ; - } - - default: - { - return SDD::scalar($theVariable, $theHtmlFlag) ; - } - } - } - - /** - * Dump the contents of an array. - * - * @param array $theArray the array whose contents are to be displayed. - * @param boolean $theHTMLFlag True if an HTML table is to be generated, - * false otherwise. - * @param string $theIndent [optional] Used by SDD::dArray during recursion - * to get indenting right. - * @return string The display form of the array. - */ - - function dArray(&$theArray, $theHTMLFlag, $theIndent = "") - { - $theOutput = array() ; - - foreach($theArray as $theIndex => $theValue) - { - if (is_array($theValue)) - { - $theString = SDD::dArray($theValue, $theHTMLFlag, $theIndent . " ") ; - $theOutput[$theIndex] = substr($theString, 0, strlen($theString) - 1) ; - } - else if (is_object($theValue)) - { - $theOutput[$theIndex] = SDD::dObject($theValue, $theHTMLFlag) ; - } - else - { - $theOutput[$theIndex] = ($theHTMLFlag ? - preg_replace('|<|s', '<', var_export($theValue, true)) : - var_export($theValue, true)) ; - } - } - - if ($theHTMLFlag) - { - $theString = "\n" ; - $theString .= "\n" ; - - foreach ($theOutput as $theIndex => $theVariableOutput) - { - $theString .= "\n\n\n" ; - } - - $theString .= "\n" ; - $theString .= "
Array (
$theIndex = >\n$theVariableOutput\n
)
\n" ; - } - else - { - $theString = "Array\n$theIndent(\n" ; - - foreach ($theOutput as $theIndex => $theVariableOutput) - { - $theString .= "$theIndent [$theIndex] => " . $theVariableOutput . "\n" ; - } - - $theString .= "$theIndent)\n" ; - } - - return $theString ; - } - - /** - * Dump the contents of an object. - * - * Provide a structured display of an object and all the - * classes from which it was derived. The contents of - * the object is displayed from most derived to the base - * class, in order. - * - * @param object $theObject the object to be dumped. - * @param boolean $theHTMLFlag true if HTML is to be generated. - * @return string the display form of the object. - */ - - function dObject(&$theObject, $theHTMLFlag) - { - $theObjectVars = get_object_vars($theObject) ; - - // - // Get the inheritance tree starting with the object and going - // through all the parent classes from there. - // - - $theClass = get_class($theObject) ; - - $theClasses[] = $theClass ; - - while ($theClass = get_parent_class($theClass)) - { - $theClasses[] = $theClass ; - } - - // - // Get all the class variables for each class in the inheritance - // tree. There will be some duplication, but we'll sort that out - // in the output process. - // - - foreach($theClasses as $theClass) - { - $theClassVars[$theClass] = get_class_vars($theClass) ; - } - - // - // Put the inheritance tree from base class to most derived order - // (this is how we get rid of duplication of the variable names) - // Go through the object variables starting with the base class, - // capture the output and delete the variable from the object - // variables. - // - - $theClasses = array_reverse($theClasses) ; - - $theOutput = array() ; - - foreach ($theClasses as $theClass) - { - $theOutput[$theClass] = array() ; - - foreach ($theClassVars[$theClass] as $theVariable => $value) - { - if (array_key_exists($theVariable, $theObjectVars)) - { - if (is_array($theObjectVars[$theVariable])) - { - $theOutput[$theClass][] = $theVariable . " = " . SDD::dArray($theObjectVars[$theVariable], $theHTMLFlag) ; - } - else if (is_object($theObjectVars[$theVariable])) - { - $theOutput[$theClass][] = $theVariable . " = " . SDD::dObject($theObjectVars[$theVariable], $theHTMLFlag) ; - } - else - { - $theOutput[$theClass][] = - $theVariable . " = " . - ($theHTMLFlag ? - preg_replace('|<|s', '<', var_export($theObjectVars[$theVariable], true)) : - var_export($theObjectVars[$theVariable], true)) ; - } - - unset($theObjectVars[$theVariable]) ; - } - } - } - - // - // Put the classes back in most derived order for generating printable - // output. - // - - $theClasses = array_reverse($theClasses) ; - - if ($theHTMLFlag) - { - $theString = "\n\n" ; - - foreach ($theClasses as $theClass) - { - $theString .= "\n" ; - } - - $theString .= "\n\n" ; - - foreach ($theClasses as $theClass) - { - $theString .= "\n" ; - } - - $theString .= "\n
\n$theClass\n
\n\n" ; - - foreach ($theOutput[$theClass] as $theVariableOutput) - { - $theString .= "\n\n\n" ; - } - - $theString .= "
\n$theVariableOutput\n
\n
\n" ; - } - else - { - - $classIndent = "" ; - - $classDataIndent = " " ; - - $theString = "" ; - - foreach ($theClasses as $theClass) - { - $theString .= "{$classIndent}class $theClass\n\n" ; - - foreach ($theOutput[$theClass] as $theVariableOutput) - { - $theString .= "$classDataIndent$theVariableOutput\n" ; - } - - $theString .= "\n" ; - - $classIndent .= " " ; - - $classDataIndent .= " " ; - } - } - - return $theString ; - } - - /** - * Write a debugging value to a log file. - * - * @access public - * @abstract - * @param mixed Data to be logged. - * @param string $theHeader [optional] string to be emitted prior to - * logging the data. By default it is a date/time - * stamp. - */ - - function log(&$theData, $theHeader=null) - { - $theHeader = date('[Y-m-d H:i:s]: ') . $theHeader ; - - if ($this->m_logging) - { - if ($this->m_htmlFlag) - { - $xxx = $this->dump($theData) ; - if (substr($xxx, 0, 5) == '
')
-                {
-                  $xxx = '
' . $theHeader . substr($xxx, 5) ;
-                }
-              else
-                {
-                  $xxx = $theHeader . $xxx ;
-                }
-
-              $this->writeLog($xxx) ;
-            }
-          else
-            {
-              $xxx = $theHeader . $this->dump($theData) ;
-              $this->writeLog($xxx) ;
-            }
-        }
-    }
-
-  /**
-   * @desc Generate context specific new line equivalents.
-   * @param integer [optional] the number of newlines.
-   * @param boolean [optional] true if generating html newlines.
-   * @return string newlines.
-   * @access public
-   */
-   
-  function newline($theCount=1, $theHtmlFlag=null)
-    {
-      if ($theHtmlFlag === null)
-        {
-          if (empty($this))
-            {
-              $theHtmlFlag = (!empty($_SERVER['DOCUMENT_ROOT'])) ;
-            }
-          else
-            {
-              if (is_subclass_of($this, "sdd"))
-              {
-                $theHtmlFlag = $this->m_htmlFlag ;
-              }
-              else
-              {
-                $theHtmlFlag = (!empty($_SERVER['DOCUMENT_ROOT'])) ;
-              }
-            }
-        }
-       
-      if ($theHtmlFlag)
-        {
-          return str_repeat("
", max($theCount, 0)) . "\n" ; - } - else - { - return str_repeat("\n", max($theCount, 0)) ; - } - } - - /** - * Dump any scalar value - * - * @param mixed $theVariable the variable to be dumped. - * @param boolean $theHtmlFlag true if html is to be generated. - */ - - function scalar(&$theVariable, $theHtmlFlag) - { - if ($theHtmlFlag) - { - return "
" . preg_replace('|<|s', '<', var_export($theVariable, true)) . "
" ; - } - else - { - return var_export($theVariable, true) ; - } - } - - /** - * Write data to the log file. - * - * @access public - * @abstract - * @parameter string $theData [by reference] the data to be written - * into the log file. - * @return integer the number of bytes written into the log file. - */ - - function writeLog(&$theData) - { - return strlen($this->m_log[] = $theData) ; - } - - /** - * Return the state of the logging flag. - * - * @access public - * @return boolean - */ - - function getLogging() - { - return $this->m_logging ; - } - - /** - * Set the state of the logging flag. - * - * @access public - * @return boolean - */ - - function setLogging($theLogging=false) - { - $this->m_logging = $theLogging ; - } -} -?> \ No newline at end of file +. + +/** + * Dump structured data, i.e., Objects and Arrays, in either plain text or + * html. This is a class wrapper for a couple of utility routines that I use + * all the time. It's handier to have them as a class. + * + * Its also the class interface for logging functions that I use in developing + * web enabled applications. + * + * @author Dick Munroe + * @copyright copyright @ by Dick Munroe, 2004 + * @license http://opensource.org/licenses/gpl-license.php GNU Public License + * @package StructuredDataDumper + * @version 1.0.4 + */ + +// +// Edit History: +// +// Dick Munroe munroe@cworks.com 04-Dec-2004 +// Initial version created. +// +// Dick Munroe munroe@csworks.com 08-Dec-2004 +// Translate < to < for html output. +// +// Dick Munroe munroe@csworks.com 23-Dec-2004 +// Add interface for writing "stuff". Extend SDD +// to get things "written". +// +// Dick Munroe munroe@csworks.com 25-Dec-2004 +// If a class extends a base class, but doesn't add +// data members, a warning winds up appearing when +// printing. +// Added a memeber to fetch the state of the logging +// flag. +// +// Dick Munroe munroe@csworks.com 11-Mar-2006 +// The test for html flag should have assumed that +// $this can be defined for objects calling SDD::dump. +// +// Dick Munroe (munroe@csworks.com) 22-Mar-2006 +// Add a function to generate "newlines". +// + +class sdd { + /* + * HTML to be generated flag. + */ + + protected $m_htmlflag; + + /* + * logging flag. + */ + + protected $m_logging = false; + + /* + * In memory log file. + */ + + protected $m_log = array(); + + /* + * Constructor. + * + * @access public + * @param boolean $theHTMLFlag [optional] True if HTML is to be generated. + * If omitted, $_SERVER is used to "guess" the state of + * the HTML flag. Be default, HTML is generated when + * accessed by a web server. + * @param boolean $theLoggingFlag [optional] the state of logging for + * this object. By default, logging is off. + */ + + public function sdd($thehtmlflag = null, $theloggingflag = false) { + if ($thehtmlflag === null) { + $thehtmlflag = (!empty($_SERVER['DOCUMENT_ROOT'])); + } + + $this->m_htmlflag = $thehtmlflag; + $this->m_logging = $theloggingflag; + } + + /* + * Close the log file. + * + * @access public + * @abstract + */ + + public function close() { + } + + /* + * Dump a structured variable. + * + * @static + * @param mixed $theVariable the variable to be dumped. + * @param boolean $theHtmlFlag [optional] true if HTML is to be generated, + * false if plain text is to be generated, null (default) if + * dump is to guess which to display. + * @return string The data to be displayed. + * @link http://www.php.net/manual/en/reserved.variables.php#reserved.variables.server Uses $_SERVER + */ + public function dump(&$thevariable, $thehtmlflag = null) { + if ($thehtmlflag === null) { + if (empty($this)) { + $thehtmlflag = (!empty($_SERVER['DOCUMENT_ROOT'])); + } else { + if (is_subclass_of($this, "sdd")) { + $thehtmlflag = $this->m_htmlflag; + } else { + $thehtmlflag = (!empty($_SERVER['DOCUMENT_ROOT'])); + } + } + } + + switch (gettype($thevariable)) { + case 'array': + return SDD::dArray($thevariable, $thehtmlflag); + case 'object': + return SDD::dObject($thevariable, $thehtmlflag); + default: + return SDD::scalar($thevariable, $thehtmlflag); + } + } + + /* + * Dump the contents of an array. + * + * @param array $theArray the array whose contents are to be displayed. + * @param boolean $theHTMLFlag True if an HTML table is to be generated, + * false otherwise. + * @param string $theIndent [optional] Used by SDD::dArray during recursion + * to get indenting right. + * @return string The display form of the array. + */ + + public function darray(&$thearray, $thehtmlflag, $theindent = "") { + $theoutput = array(); + + foreach ($thearray as $theindex => $thevalue) { + if (is_array($thevalue)) { + $thestring = ssd::dArray($thevalue, $thehtmlflag, $theindent . " "); + $theoutput[$theindex] = substr($thestring, 0, strlen($thestring) - 1); + } else if (is_object($thevalue)) { + $theoutput[$theindex] = sdd::dobject($thevalue, $thehtmlflag); + } else { + $theoutput[$theindex] = ($thehtmlflag ? preg_replace('|<|s', '<', + var_export($thevalue, true)) : var_export($thevalue, true)); + } + } + + if ($thehtmlflag) { + $thestring = "\n"; + $thestring .= "\n"; + + foreach ($theoutput as $theindex => $thevariableoutput) { + $thestring .= "\n\n\n"; + } + + $thestring .= "\n"; + $thestring .= "
Array (
$theindex = >\n$thevariableoutput\n
)
\n"; + } else { + $thestring = "Array\n$theindent(\n"; + + foreach ($theoutput as $theindex => $thevariableoutput) { + $thestring .= "$theindent [$theindex] => " . $thevariableoutput . "\n"; + } + + $thestring .= "$theindent)\n"; + } + + return $thestring; + } + + /* + * Dump the contents of an object. + * + * Provide a structured display of an object and all the + * classes from which it was derived. The contents of + * the object is displayed from most derived to the base + * class, in order. + * + * @param object $theObject the object to be dumped. + * @param boolean $theHTMLFlag true if HTML is to be generated. + * @return string the display form of the object. + */ + + public function dobject(&$theobject, $thehtmlflag) { + $theobjectvars = get_object_vars($theobject); + + /* Get the inheritance tree starting with the object and going + * through all the parent classes from there. + */ + + $theclass = get_class($theobject); + + $theclasses[] = $theclass; + + while ($theclass = get_parent_class($theclass)) { + $theclasses[] = $theclass; + } + + /* Get all the class variables for each class in the inheritance + * tree. There will be some duplication, but we'll sort that out + * in the output process. + */ + + foreach ($theclasses as $theclass) { + $theclassvars[$theclass] = get_class_vars($theclass); + } + + /* Put the inheritance tree from base class to most derived order + * (this is how we get rid of duplication of the variable names) + * Go through the object variables starting with the base class, + * capture the output and delete the variable from the object + * variables. + */ + + $theclasses = array_reverse($theclasses); + + $theoutput = array(); + + foreach ($theclasses as $theclass) { + $theoutput[$theclass] = array(); + + foreach ($theclassvars[$theclass] as $thevariable => $value) { + if (array_key_exists($thevariable, $theobjectvars)) { + if (is_array($theobjectvars[$thevariable])) { + $theoutput[$theclass][] = $thevariable . " = " . sdd::darray($theobjectvars[$thevariable], $thehtmlflag); + } else if (is_object($theobjectvars[$thevariable])) { + $theoutput[$theclass][] = $thevariable . " = " . sdd::dobject($theobjectvars[$thevariable], $thehtmlflag); + } else { + $theotput[$theclass][] = $thevariable . " = " . + ($thehtmlflag ? preg_replace('|<|s', '<', var_export( + $theobjectvars[$thevariable], true)) : var_export($theobjectvars[$thevariable], true)); + } + + unset($theobjectvars[$thevariable]); + } + } + } + + /* Put the classes back in most derived order for generating printable + * output. + */ + $theclasses = array_reverse($theclasses); + + if ($thehtmlflag) { + $thestring = "\n\n"; + + foreach ($theclasses as $theclass) { + $thestring .= "\n"; + } + + $thestring .= "\n\n"; + + foreach ($theclasses as $theclass) { + $thestring .= "\n"; + } + + $thestring .= "\n
\n$theclass\n
\n\n"; + + foreach ($theoutput[$theclass] as $thevariableoutput) { + $thestring .= "\n\n\n"; + } + + $thestring .= "
\n$thevariableoutput\n
\n
\n"; + } else { + $classindent = ""; + + $classdataindent = " "; + + $thestring = ""; + + foreach ($theclasses as $theclass) { + $thestring .= "{$classindent}class $theclass\n\n"; + + foreach ($theoutput[$theclass] as $thevariableoutput) { + $thestring .= "$classdataindent$thevariableoutput\n"; + } + + $thestring .= "\n"; + + $classindent .= " "; + + $classdataindent .= " "; + } + } + + return $thestring; + } + + /* + * Write a debugging value to a log file. + * + * @access public + * @abstract + * @param mixed Data to be logged. + * @param string $theHeader [optional] string to be emitted prior to + * logging the data. By default it is a date/time + * stamp. + */ + + public function log(&$thedata, $theheader = null) { + $theheader = date('[Y-m-d H:i:s]: ') . $theheader; + + if ($this->m_logging) { + if ($this->m_htmlflag) { + $xxx = $this->dump($thedata); + if (substr($xxx, 0, 5) == '
') {
+                    $xxx = '
' . $theheader . substr($xxx, 5);
+                } else {
+                    $xxx = $theheader . $xxx;
+                }
+
+                $this->writeLog($xxx);
+            } else {
+                $xxx = $theheader . $this->dump($thedata);
+                $this->writelog($xxx);
+            }
+        }
+    }
+
+    /*
+     * @desc Generate context specific new line equivalents.
+     * @param integer [optional] the number of newlines.
+     * @param boolean [optional] true if generating html newlines.
+     * @return string newlines.
+     * @access public
+     */
+
+    public function newline($thecount = 1, $thehtmlflag = null) {
+        if ($thehtmlflag === null) {
+            if (empty($this)) {
+                $thehtmlflag = (!empty($_SERVER['DOCUMENT_ROOT']));
+            } else {
+                if (is_subclass_of($this, "sdd")) {
+                    $thehtmlflag = $this->m_htmlflag;
+                } else {
+                    $thehtmlflag = (!empty($_SERVER['DOCUMENT_ROOT']));
+                }
+            }
+        }
+
+        if ($thehtmlflag) {
+            return str_repeat("
", max($thecount, 0)) . "\n"; + } else { + return str_repeat("\n", max($thecount, 0)); + } + } + + /* + * Dump any scalar value + * + * @param mixed $theVariable the variable to be dumped. + * @param boolean $theHtmlFlag true if html is to be generated. + */ + + public function scalar(&$thevariable, $thehtmlflag) { + if ($thehtmlflag) { + return "
" . preg_replace('|<|s', '<', var_export($thevariable, true)) . "
"; + } else { + return var_export($thevariable, true); + } + } + + /* + * Write data to the log file. + * + * @access public + * @abstract + * @parameter string $theData [by reference] the data to be written + * into the log file. + * @return integer the number of bytes written into the log file. + */ + + public function writelog(&$thedata) { + return strlen($this->m_log[] = $thedata); + } + + /* + * Return the state of the logging flag. + * + * @access public + * @return boolean + */ + + public function getlogging() { + return $this->m_logging; + } + + /* + * Set the state of the logging flag. + * + * @access public + * @return boolean + */ + + public function setlogging($thelogging=false) { + $this->m_logging = $thelogging; + } +} diff --git a/sudoku/sdd/class.logfile.php b/sudoku/sdd/class.logfile.php index 146b4aa..0735376 100755 --- a/sudoku/sdd/class.logfile.php +++ b/sudoku/sdd/class.logfile.php @@ -1,69 +1,75 @@ - - * @copyright copyright @ by Dick Munroe, 2004 - * @license http://opensource.org/licenses/gpl-license.php GNU Public License - * @package StructuredDataDumper - * @version 1.0.1 - */ - -// -// Edit History: -// -// Dick Munroe munroe@cworks.com 23-Dec-2004 -// Initial version created/ -// - -include_once('SDD/class.SDD.php') ; - -class logfile extends SDD -{ - - /** - * The open file handle. - * - * @access private - */ - - var $m_handle ; - - /** - * Constructor - * - * @access public - */ - - function logfile($theFileName) - { - if (file_exists($theFileName)) - { - $this->m_handle = fopen($theFileName, 'a') ; - } - else - { - $this->m_handle = fopen($theFileName, 'w') ; - } - } - - function close() - { - fclose($this->m_handle) ; - } - - /** - * Write a debugging value to a log file. - * - * @access public - * @abstract - * @param mixed Data to be logged. - * @return integer number of bytes written to the log. - */ - - function log(&$theData) - { - return fwrite($this->m_handle, date('[Y-m-d H:i:s]: ') . $this->dump($theData) . "\n") ; - } - -} -?> \ No newline at end of file +. + +/** + * @author Dick Munroe + * @copyright copyright @ by Dick Munroe, 2004 + * @license http://opensource.org/licenses/gpl-license.php GNU Public License + * @package StructuredDataDumper + * @version 1.0.1 + */ + +/* + * Edit History: + * + * Dick Munroe munroe@cworks.com 23-Dec-2004 + * Initial version created/ + */ + +require_once('SDD/class.SDD.php'); + +class logfile extends SDD { + + /* + * The open file handle. + * + * @access private + */ + + protected $m_handle; + + /* + * Constructor + * + * @access public + */ + + public function logfile($thefilename) { + if (file_exists($thefilename)) { + $this->m_handle = fopen($thefilename, 'a'); + } else { + $this->m_handle = fopen($thefilename, 'w'); + } + } + + public function close() { + fclose($this->m_handle); + } + + /* + * Write a debugging value to a log file. + * + * @access public + * @abstract + * @param mixed Data to be logged. + * @return integer number of bytes written to the log. + */ + + public function log(&$thedata) { + return fwrite($this->m_handle, date('[Y-m-d H:i:s]: ') . $this->dump($thedata) . "\n"); + } +} +