~eda-qa/dhlib/main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
/* <license>
 * This file is part of the dis-Emi-A HaXe Library. Copyright © edA-qa mort-ora-y
 * For full copyright and license information please refer to doc/license.txt.
 * </license> 
 */
package sudoku;

import mathx.Matrix;
import mathx.MatPoint;

/**
 * The board is the logical construct of the grid and all
 * the numbers which it contains
 */
class Board extends Matrix<Int>
{
	/**
	 * Creates a new board, either from the provided data, or completely
	 * empty.
	 */
	public function new( ?fromData : Matrix<Int> )
	{
		if( fromData == null )
		{
			fromData = Matrix.create( GConst.boardSize, GConst.boardSize, GConst.cellUnset );
		}
		else
		{
			Assert.isTrue( fromData.size.x == GConst.boardSize );
			Assert.isTrue( fromData.size.y == GConst.boardSize );
		}
		super( fromData );		
			
	}

	/**
	 * Determines if all values have been filled in
	 */
	public function isComplete() : Bool
	{
		for( x in 0...GConst.boardSize )
			for( y in 0...GConst.boardSize )
				if( get( x, y ) == GConst.cellUnset )
					return false;
		return true;
	}
	
	/**
	 * Determines whether the board so far is part of a valid solution,
	 * that is, are all the values currently in place in order.  Empty cells
	 * are just skipped, use isComplete to determine if the board is 
	 * complete.
	 */
	public function isValid() : Bool
	{
		//NOTE: There are quicker ways to check for validity other than
		//generating the matrix, but we don't need that optimization now
		var m = getValidityMatrix();
		for( mp in m.xOrderIter() )
			if( !m.get(mp.x,mp.y) )
				return false;
		return true;
	}
	
	/**
	 * Creates a matrix showing the validity of every cell (based on simple
	 * calculations of the duplication of numbers.
	 * In the returned matrix true indicates valid, or empty, false indicates
	 * invalid (and it will always come in a pair)
	 */
	public function getValidityMatrix() : Matrix<Bool>
	{
		var ret = Matrix.create( GConst.boardSize, GConst.boardSize, true );
		
		//do the rows/columns
		for( row in [true, false] )
		{
			for( i in 0...GConst.boardSize )
			{
				var line = row ? extractRow( i ) : extractCol( i );
				var count = ArrayUtil.countElementsInt( line );
				
				//mark as invalid
				for( j in 0...GConst.boardSize )
				{
					var c = get( row ? j : i, row ? i : j );
					if( c == GConst.cellUnset )
						continue;
					if( c < GConst.cellLow || c > GConst.cellHigh 	
						|| count.get(c) > 1 )
						ret.set( row ? j : i, row ? i : j, false );
				}
			}
		}
		
		//do the groups
		for( gx in 0...GConst.numGroups )
			for( gy in 0...GConst.numGroups )
			{
				//count
				var line = new Array<Int>();
				for( mp in 
					subIter( 
						MatPoint.at( gx * GConst.groupSize, gy * GConst.groupSize ),
						MatPoint.at( GConst.groupSize, GConst.groupSize )
						)
					)
					line.push( get( mp.x, mp.y ) );
				var count = ArrayUtil.countElementsInt( line );
				
				//mark as invalid
				for( mp in 
					subIter( 
						MatPoint.at( gx * GConst.groupSize, gy * GConst.groupSize ),
						MatPoint.at( GConst.groupSize, GConst.groupSize )
						)
					)
				{
					var c = get( mp.x, mp.y );
					if( c == GConst.cellUnset )
						continue;
					if( c < GConst.cellLow || c > GConst.cellHigh 	
						|| count.get(c) > 1 )
						ret.set( mp.x, mp.y, false );
				}
			}
		
		return ret;
	}
	
	/**
	 * Creates a random filled Sudoku board.  All existing data will
	 * be lost.
	 *
	 * This works by creating a short random diagonal seed, then
	 * combining those seeds to fill in the remaining spots.  Only the
	 * first two diagonals are set since if more are set there is a possibility
	 * that the board is unsolvable.
	 * In theory we don't even need a seed, the solve algorithm could 
	 * simply randomly order it's available values and then a solution
	 * of an empty board would be okay.  The seed here is thus to break
	 * any pattern which may occur in the solver logic.
	 */
	public function fillRandom() : Int
	{
		var seed = [createRandomGroup(),createRandomGroup()];
		
		//fill in first two diagonals, the seed of the board
		replace( Matrix.create( GConst.boardSize, GConst.boardSize, 0 ) );
		for( i in 0...2 )
			paste( 
				i * GConst.groupSize, 
				i * GConst.groupSize,
				seed[i]
				);
		var at = findSolution();
		Assert.isFalse( at == -1 );
		return at;
	}
	
	/**
	 * Attempts to find a solution for the current Board state.
	 *
	 * NOTE: This is not a great solver, in that it can't actually solve many
	 * boards (though it seems to always be able to solve our fillRandom
	 * scenario, which is all we needed it for).  Sudoku is just a graph-coloring
	 * problem, so a proper solution would really just be a special coloring
	 * algorithm.  I do however suspect with a bit of back-tracking code
	 * here (rather than random retesting) this might also work reasonably
	 * well (though whether it always works would be in question)
	 *
	 * @return	[out] the count of how many iterations were tested before
	 *		a solution was found, -1 if not solution was found (which doesn't
	 *		necessarily mean none exists, just that the current algorithm can't
	 *		find it)
	 */
	public function findSolution() : Int
	{
		//make backup
		var backup = clone();
		
		//loop since we still have an ordering problem in the solver, which
		//occassionally doesn't choose the right items first -- randomizing
		//the order of those items helps, and in a loop of 10K creations
		//no invalid boards were created, thus I suspect there must be some
		//simple change to the ordering to make it work...
		
		/* No such ordering has yet been found, and counting the number
		 * of choices made there looks to be a staggering number of 
		 * possible options for even a 2-diagonal seed. TODO: check
		 * existing literature for solution, or demonstration of NP-completness
		 * Since a loop of 10 works, I'm guessing most choices are irrelevant
		 * and only in a few cases does it make the board unsolvable -- which
		 * would be then like Minesweeper: usually simple games, other
		 * times virtually impossible (though rare)
		 */
		 
		/* Further interesting, the SolveStep is usually zero, and virtually
		 * never higher than one, which would strengthen the claim that
		 * in most choice situations the choice made is irrelevant
		 * By 10K: [9606, 383, 10, 1, 0, 0, 0, 0, 0, 0] (from zero seed)
		 */
		for( i in 0...10 )
		{
			if( solve() )
				return i;
				
			replace( backup );
		}
		
		return -1;
	}
	
	private var solveRemain : Int;
	private function solve() : Bool
	{
		//setup a grid of allowed values first
		var baseSet = ArrayUtil.incList( GConst.cellLow, GConst.cellHigh );
		var solveAllow = Matrix.create( GConst.boardSize, GConst.boardSize, baseSet );
		for( mp in solveAllow.xOrderIter() )
			solveAllow.set( mp.x, mp.y, baseSet.copy() );
			
		//setup total possibility counts (number of cells on Board where a number could still appear)
		solveRemain = GConst.boardSize * GConst.boardSize;
		var poss = new Array<Int>();
		var iPoss = GConst.boardSize * GConst.boardSize;
		for( i in GConst.cellLow...(GConst.cellHigh+1) )
			poss[i] = iPoss;
			
		//setup group possibility number counts (number of cells in a group where a number could still appear)
		var basePoss = new Array<Int>();
		var igPoss = GConst.cellRange;
		for( i in GConst.cellLow...(GConst.cellHigh+1) )
			basePoss[i] = igPoss;
		var possInGroup = Matrix.create( GConst.numGroups, GConst.numGroups, basePoss );
		for( mp in possInGroup.xOrderIter() )
			possInGroup.set( mp.x, mp.y, basePoss.copy() );
		
		//setup remaining cells per group
		var remainInGroup = Matrix.create( GConst.numGroups, GConst.numGroups, GConst.cellRange );
		
		//remove existing elements in line/column
		for( y in 0...GConst.boardSize )
			for( x in 0...GConst.boardSize )
				solvePlace( get( x, y ), x, y, solveAllow, poss, remainInGroup, possInGroup );
			
		var remain = solveRemain;	//afterwards count in solvePlace may be wrong
		
		//now, loop over the group allow numbers, and work on the target
		//with the lowest count next always.
		var totalChoiceCount = 0;
		for( i in 0...remain )
		{
			//get smallest group
			var gc = GConst.cellRange+1;
			var ggc = GConst.cellRange+1;	//TODO: actually only 10 is needed
			var at = null;
			for( mp in solveAllow.xOrderIter() )
			{
				var l = solveAllow.get(mp.x,mp.y);
				var gx = Math.floor( mp.x / GConst.groupSize );
				var gy = Math.floor( mp.y / GConst.groupSize );
				if( l.length > 0 && ( l.length < gc 
					//|| (l.length == gc && mathx.Random.s_nextBool()) ) //add random order to those of equal length
					|| (l.length == gc
					&& remainInGroup.get( gx, gy ) < ggc ) )
					)
				{
					gc = solveAllow.get(mp.x,mp.y).length;
					ggc = remainInGroup.get( gx, gy );
					at = mp.clone();
				}
			}
			if( at == null )
			{
				//trace( solveAllow );
				//dump();
				return false;
			}
			
			var gx = Math.floor( at.x / GConst.groupSize );
			var gy = Math.floor( at.y / GConst.groupSize );
				
			//identify lowest
			var low = iPoss+1;
			var glow = igPoss+1;
			var which = GConst.cellUnset;
			var avail = solveAllow.get( at.x, at.y );
			var choiceCount = 0;
			for( i in avail )
			{
				if( poss[i] == 0 )
					continue;
					
				if( poss[i] > low )
					continue;
					
				if( poss[i] == low )
				{
					choiceCount = 1;
					if( mathx.Random.s_nextBool() )
						continue;
						
					//interestingly they always have the same counts...
					//trace( which + ":" + low + "/" + glow + " => " 
					//	+ i +":" +poss[i] + "/" + possInGroup.get( gx, gy )[i] );
				}
				else
					choiceCount = 0;
						
				low = poss[i];
				glow = possInGroup.get( gx, gy )[i];
				which = i;
			}
			totalChoiceCount += choiceCount;
					
			solvePlace( which, at.x, at.y, solveAllow, poss, remainInGroup, possInGroup );
		}
		
		//between 20 and 30 choices usually (from 2 diagonal seeds)
		//from ~28 to 35 with 1 diagonal seed
		//!37 to 42 with no seeds (an empty board)
		//trace( "Choices: " + totalChoiceCount );
		return true;
	}
	
	private function solvePlace( value : Int, x : Int, y : Int, 
		solveAllow : Matrix<Array<Int>>, poss, remainInGroup,
		possInGroup )
	{
		if( value == GConst.cellUnset )
			return;
			
		solveRemain--;
		set( x, y, value );
			
		var gx = Math.floor( x / GConst.groupSize );
		var gy = Math.floor( y / GConst.groupSize );
		
		//remove from all in that row/col
		for( i in 0...GConst.boardSize )
		{
			var gi = Math.floor( i / GConst.groupSize );
			if( solveAllow.get( i, y ).remove( value ) )
			{
				poss[value]--;
				possInGroup.get( gi, gy )[value]--;
			}
			if( solveAllow.get( x, i ).remove( value ) )
			{
				poss[value]--;
				possInGroup.get( gx, gi )[value]--;
			}
		}
		
		//remove from the group
		for( i in 0...GConst.groupSize )
			for( j in 0...GConst.groupSize )
				if( solveAllow.get( i + gx * GConst.groupSize, j + gy * GConst.groupSize ).remove( value ) )
				{
					poss[value]--;
					possInGroup.get( gx, gy )[value]--;
				}
		remainInGroup.set( gx, gy, remainInGroup.get(gx,gy)-1 );
				
		//adjust counts (remove remaining items from the allowed set for this cell)
		var avail = solveAllow.get( x, y );
		for( i in avail )
		{
			poss[i]--;
			possInGroup.get( gx, gy )[i]--;
		}
		solveAllow.set(x,y,new Array<Int>());	//empty out this one
	}
	
	/** 
	 * Creates a random block the size of a group, with the properties
	 * of a group.
	 */
	private function createRandomGroup() : Matrix<Int>
	{
		var seedList = ArrayUtil.shuffle( ArrayUtil.incList( GConst.cellLow, GConst.cellRange ) );
		return Matrix.fromArrayFlat( GConst.groupSize, seedList );
	}
	
	/**
	 * Removes random cells (unsets) from the Board.
	 */
	public function removeCount( num : Int )
	{
		Assert.isTrue( num <= maxLinearIndex() );
		var all = ArrayUtil.incList( 0, maxLinearIndex() );
		var rand = ArrayUtil.shuffle( all );
		for( i in 0...num )
		{
			var mp = fromLinearIndex( rand[i] );
			set( mp.x, mp.y, GConst.cellUnset );
		}
	}
	
	public function dump()
	{
		for( y in 0...GConst.boardSize )
		{
			if( y % GConst.groupSize == 0 )
				trace( "" );
			var line = "";
			for( x in 0...GConst.boardSize )
			{
				if( x % GConst.groupSize == 0 )
					line = line + " ";
				line = line + get( x, y );
			}
			trace( line );
		}
	}
}