Features
- Different types (3x3, 2x4, 3x6 etc)
- Numbers or letters
- Many variations (seed)
How it works
- Generate complete grid
- Remove a block, use solver to check if it's still valid, repeat until isn't.
- Draw
What I learned
This was fun. I made some mistakes. First, I thought it would be possible to generate random numbers in a grid, hoping it would yield valid. Second, I assumed it would be possible to randomly remove some cells from a valid grid, and it would be still solvable for users. I ended up needing a simple solver.
#game
Log in to post a comment.
const turtle = new Turtle(); const text = new Text(); // make all types (2x3,2x4,2x5, 3x3, 3x4 etc) const types = []; const a = getRange(5); for(let aa=0;aa<a.length;aa++) { for(let bb=0;bb<a.length;bb++) { types.push([aa+2, bb+2]); } } // returns array containing 0...n function getRange(total) { var arr = []; for (let i = 0; i < total; ++i) arr.push(i); return arr; } //////////////////////////////////////////////////////////////// // Sudoku generator + solver. Created by Mark Knol 2020 // https://turtletoy.net/turtle/5098380d82 //////////////////////////////////////////////////////////////// class Sudoku { constructor() { const type = 6; // min=0, max=23, step=1 this.type = type; const seed = 1; // min=1, max=200, step=1 this.seed = seed; const useLetters = 0; // min=0, max=1, step=1 (No, Yes) this.hardMode = useLetters; this.gridBlockSize = [types[type][0], types[type][1]]; this.gridBlockCount = this.gridBlockSize[0] * this.gridBlockSize[1]; this.gridTotalCount = this.gridBlockCount * this.gridBlockCount; //const difficulty = 0.8; // min=0.6, max=0.95, step=0.05 //this.difficulty = difficulty const showSolution = 0; // min=0, max=1, step=1 (No, Yes) this.showSolution = showSolution; this.random = new Random(seed); this.grid = []; this.letters = (getRange(100).map(v => String.fromCharCode(v + 97))); // 0...gridBlockCount this.checks = getRange(this.gridBlockCount); this.generate(this.grid); this.valid = this.isValidSudoku(this.grid); } // check if row/colum is correct (has 0-9 all once) isValidPos(grid, pos, horizontal) { const check = this.checks.slice(); for (let x=0; x<this.gridBlockCount; x++) { const value = this.grid[horizontal ? pos * this.gridBlockCount + x : x * this.gridBlockCount + pos]; const idx = check.indexOf(value) if (idx >= 0) { check.splice(idx, 1); } else { return false; } } return true; } // check if block (section of grid) is correct (has 0-9 all once) isValidBlock(grid, row, col) { const check = this.checks.slice(); const gridBlockCount = this.gridBlockCount; const gridBlockSize = this.gridBlockSize; for (let y=0; y<gridBlockSize[1]; y++) { for (let x=0; x<gridBlockSize[0]; x++) { const value = grid[(y+row*gridBlockSize[1]) * gridBlockCount + (x+col*gridBlockSize[0])]; const idx = check.indexOf(value) if (idx >= 0) { check.splice(idx, 1); } else { return false; } } } return true; } // validate if grid is valid (rows, columns and grids are all correct) isValidSudoku(grid) { const gridBlockCount = this.gridBlockCount; const gridBlockSize = this.gridBlockSize; // check horizontal and vertical rows for (let y=0; y<gridBlockCount; y++) { if (!this.isValidPos(grid, y, false)) return false; for (let x=0; x<gridBlockCount; x++) { if (!this.isValidPos(grid, x, true)) return false; } } // validate blocks for (let y=0; y<gridBlockSize[1]; y++) { for (let x=0; x<gridBlockSize[0]; x++) { if (!this.isValidBlock(grid, x, y)) return false; } } return true; } // generate a complete sudoku grid // based on <https://gamedev.stackexchange.com/a/138228> generate(grid) { const gridBlockSize = this.gridBlockSize; const gridBlockCount = this.gridBlockCount; const options = this.random.shuffle(this.checks.slice()); // generate [1,2,3]. is used to offset a row of block in other blocks const shiftsY = this.random.shuffle(getRange(gridBlockSize[0])); // generate [1,2,3], [4,5,6], [7,8,9] and shuffles per block. is used to offset columns const shiftsX = []; for (let shift2=0; shift2<gridBlockSize[1]; ++shift2) { const values = this.random.shuffle(getRange(gridBlockSize[0])); values.forEach(v => shiftsX.push(shift2 * gridBlockSize[0] + v)); } // generate [1,2,3], [4,5,6], [7,8,9] and shuffles per block. is used to offset rows const shiftsXY = []; for (let shift2=0; shift2<gridBlockSize[0]; ++shift2) { const values = this.random.shuffle(getRange(gridBlockSize[1])); values.forEach(v => shiftsXY.push(shift2 * gridBlockSize[1] + v)); } // generate [1,2,3], [4,5,6], [7,8,9] and shuffles per block. is used to offset rows const shiftsXY2 = []; for (let shift2=0; shift2<gridBlockSize[0]; ++shift2) { const values = this.random.shuffle(getRange(gridBlockSize[1])); values.forEach(v => shiftsXY2.push(shift2 * gridBlockSize[1] + v)); } for (let x=0; x<gridBlockCount; x++) { let row = 0; for (let shift1=0; shift1<gridBlockSize[0]; ++shift1) { for (let shift2=0; shift2<gridBlockSize[1]; ++shift2) { grid[(shiftsXY[row++]) * gridBlockCount + shiftsX[x]] = options[(x + shiftsY[shift1] + shift2*gridBlockSize[0]) % gridBlockCount]; } } } } draw(turtle, text, i) { const gridBlockSize = this.gridBlockSize; const gridBlockCount = this.gridBlockCount; const grid = this.grid; //const difficulty = this.difficulty; const showSolution = this.showSolution; if ( this.valid) { const x = i % gridBlockCount; const y = i / gridBlockCount | 0; const size = 8/gridBlockCount; const dist = size * 10; const dist2 = dist*2; const pos = [(-gridBlockCount/2 + x) * dist2 + dist, (-gridBlockCount/2 + y) * dist2 + dist]; // sudoku labels if (i < this.gridTotalCount) { if (grid[i] != -1) { const label = this.hardMode ? this.letters[grid[i]] : `${(grid[i] + 1)}`; turtle.jump(label.length === 1 ? pos[0] : pos[0] - size * 2.5, pos[1]); text.print(turtle, label, size * 0.25); } } // title if (i === 0) { const seedLabel = `${this.seed}`.padStart(3, '0') const label = `Sudoku ${gridBlockSize[0]}x${gridBlockSize[1]} ~ #${seedLabel}`; turtle.jump(pos[0] - size, -90); text.print(turtle, label, .22); } pos[0] -= dist * 0.8; pos[1] -= dist; // grid lines if (y === 0 && x !== 0) { turtle.jump(pos[0], pos[1]); turtle.goto(pos[0], pos[1] + (dist2*gridBlockCount)); } if (x === 0 && y !== 0) { turtle.jump(pos[0], pos[1]); turtle.goto(pos[0] + (dist2*gridBlockCount), pos[1]); } // double lines if (x%gridBlockSize[0]===0 && y %gridBlockSize[1]===0) { pos[0] -= 1*size; if (y === 0 && x !== 0) { turtle.jump(pos[0], pos[1]); turtle.goto(pos[0], pos[1] + (dist2*gridBlockCount)); } pos[0] += 1*size; pos[1] -= 1*size; if (x === 0 && y !== 0) { turtle.jump(pos[0], pos[1]); turtle.goto(pos[0] + (dist2*gridBlockCount), pos[1]); } } return i < this.gridTotalCount - 1; } else { turtle.jump(-30,0); text.print(turtle, 'invalid', 0.5); return false; } } } let sudoku; let solver; function walk(i) { if (i === 0) { sudoku = new Sudoku(); const gridLength = sudoku.grid.length; let tests = [ test(sudoku, "0-n",() => getRange(gridLength)), test(sudoku, "random-1",() => new Random(sudoku.seed).shuffle(getRange(gridLength))), test(sudoku, "n-0",() => getRange(gridLength).reverse()), test(sudoku, "odd-even", () => { var arr = []; for (var i = 0; i < gridLength; ++i) { arr.push(i / gridLength >= 0.5 ? (i*2)%gridLength : ((i*2)%gridLength) +1); } return arr; }), test(sudoku, "odd-even-reversed", () => { var arr = []; for (var i = 0; i < gridLength; ++i) { arr.push(i / gridLength >= 0.5 ? (i*2)%gridLength : ((i*2)%gridLength) +1); } return arr.reverse(); }), test(sudoku, "odd-even-grid-1", () => { var arr = []; t=gridLength; m = sudoku.gridBlockSize[0]; for (var i = 0; i < t; ++i) { arr.push(((i * m) % t) + Math.floor(i / t * m)); } return arr; }), test(sudoku, "odd-even-grid-2", () => { var arr = []; t=gridLength; m = sudoku.gridBlockSize[1]; for (var i = 0; i < t; ++i) { arr.push(((i * m) % t) + Math.floor(i / t * m)); } return arr; }), test(sudoku, "indexOf-lastIndexOf", () => { var arr = []; t=gridLength; m = sudoku.gridBlockSize[1]; for (var i = 0; i < t; ++i) { arr.push(sudoku.grid.indexOf(i)); arr.push(sudoku.grid.lastIndexOf(i)); } for (var i = 0; i < t; ++i) { arr.push(i); } return arr; }), test(sudoku, "index-weird-0..n", () => { var arr1 = []; var arr2 = []; var grid = sudoku.grid.slice(); const rr1 = []; const rr2 = []; let c = 0; for (let i = 0; i < gridLength; ++i) { let idx; while((idx = grid.indexOf(i)) >= 0) { (c%2==0 )?arr1.push(idx) : arr2.push(idx); (c%2==0)?rr1.push(i) : rr2.push(i); grid[idx] = -1; c++; } } // console.log("weird:"); //console.log( [...rr1, ...rr2]) //console.log( [...arr1, ...arr2]) return [...arr1, ...arr2]; }), test(sudoku, "random-2",() => new Random(sudoku.seed*2).shuffle(getRange(gridLength))), ]; let testResult = tests[(sudoku.seed) % (tests.length -1)](); console.log("#"+sudoku.seed, " best: " + testResult.totalRemoved + "/" + sudoku.grid.length + " = " + ((testResult.totalRemoved / sudoku.grid.length * 100 )|0) + "%", "strategy: "+testResult.strategy); /* testResult.sort((a, b) => b.totalRemoved - a.totalRemoved); const best = testResult[0]; const worst = testResult[testResult.length-1]; console.log("best: " + best.totalRemoved + "/" + sudoku.grid.length + " = " + ((best.totalRemoved / sudoku.grid.length * 100 )|0) + "%", "strategy: "+best.strategy); console.log("worst: " + worst.totalRemoved + "/" + sudoku.grid.length + " = " + ((worst.totalRemoved / sudoku.grid.length * 100 )|0) + "% ", "strategy: "+worst.strategy); */ if (!sudoku.showSolution) { sudoku.grid = testResult.grid; } } return sudoku.draw(turtle, text, i); } // creates a solver, attempt to remove with given order until solver isnt valid anymore function test(sudoku, strategy, removeOrderFn) { return () => { const solver = new Solver(sudoku.gridBlockSize); const removeOrder = removeOrderFn(); const testGrid = sudoku.grid.slice(); let totalRemoved = 0; while (removeOrder.length) { let index = removeOrder.pop(); if (testGrid[index] != -1) { const testSolution = testGrid.map((v, i) => i != -1 && i != index ? [v] : getRange(sudoku.gridBlockCount)); solver.solve(testSolution); if (solver.isAllSingle(testSolution)) { // grid is still valid with this mutation, write open possibility in testGrid testGrid[index] = -1; totalRemoved ++; } } } return { totalRemoved, strategy, grid: testGrid }; } } // Solver. Folds a possibility grid until all possibility items have one item left class Solver { constructor(gridBlockSize) { this.gridBlockSize = gridBlockSize; this.gridBlockCount = gridBlockSize[0] * this.gridBlockSize[1]; } format(arr) { let s = []; let c = 0; for(let v of arr) { s.push( "[" + v.join(",") + "]" + (++c % (this.gridBlockCount) == 0 ? "\n":"") ); } return s.join(", "); } solve(possibilities, count = 0) { if (count == 100 || this.isAllSingle(possibilities)) { // console.log("stopped after " + count, this.isAllSingle(possibilities)); return; } let hasReduced = false; // filter on singles, sort on with most length, then reduce var p = possibilities.filter(v => this.isSingle(v)); p.sort((a,b) => b.length - a.length); p.forEach(p => { if (this.reduceAt(possibilities, possibilities.indexOf(p))) { hasReduced = true }}); if (hasReduced) { // recurse this.solve(possibilities, count + 1); } } isSingle(possibility) { return possibility.length === 1; } isAllSingle(possibilities = []) { for (let possibility of possibilities) { if (!this.isSingle(possibility)) { return false; } } return true; } reduceAt(possibilities, i) { let value = possibilities[i][0]; if (value < 0) return false; let hasReduced = false; let x1 = i % this.gridBlockCount; let y1 = i / this.gridBlockCount | 0; //console.log(`reduce "${value}" [x=${x1},y=${y1}]`); // remove from col/row for (let c=0; c<this.gridBlockCount; c++) { // reduce possibilities from value in rows { let j = x1 + c*this.gridBlockCount; if (j !== i) { //let x2 = j % this.gridBlockCount; //let y2 = j / this.gridBlockCount | 0; //console.log(` remove row "${value}" at [x=${x2},y=${y2}]`); if (this.remove(possibilities[j],value)) { hasReduced = true; } } } // reduce possibilities from value in cols { let j = c + y1*this.gridBlockCount; if (j !== i) { //let x2 = j % this.gridBlockCount; //let y2 = j / this.gridBlockCount | 0; // console.log(` remove col "${value}" at [x=${x2},y=${y2}]`); if (this.remove(possibilities[j],value)) { hasReduced = true; } } } } let thisBlock = []; // remove possibilities from block { const gbsx = this.gridBlockSize[0]; const gbsy = this.gridBlockSize[1]; for(let y2=0;y2<gbsy;++y2) { for(let x2=0;x2<gbsx;++x2) { let x3=x2+ (x1/gbsx|0) * gbsx; let y3=y2+(y1/gbsy|0)*gbsy; let j=x3+y3*this.gridBlockCount; if (i !== j) { if (this.remove(possibilities[j], value)) { hasReduced = true; } } thisBlock.push(possibilities[j]); } } } // 1. check if current value is only available on //if ( return hasReduced; } // returns true if removed something remove(arr, value) { let idx = arr.indexOf(value); if (idx >= 0) { arr.splice(idx, 1); return true; } return false; } } //////////////////////////////////////////////////////////////// // Text utility code. Created by Reinder Nijhoff 2019 // https://turtletoy.net/turtle/1713ddbe99 //////////////////////////////////////////////////////////////// function Text() { class Text { print (t, str, scale) { t.radians(); let pos = [t.x(), t.y()], h = t.h(), o = pos; str.split('').map(c => { const i = c.charCodeAt(0) - 32; if (i < 0 ) { pos = o = this.rotAdd([0, 48*scale], o, h); } else if (i > 96 ) { pos = this.rotAdd([16*scale, 0], o, h); } else { const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2]; paths.map( p => { t.up(); p.map( s=> { t.goto(this.rotAdd([s[0]*scale - lt, s[1]*scale], pos, h)); t.down(); }); }); pos = this.rotAdd([rt - lt, 0], pos, h); } }); } rotAdd (a, b, h) { return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0], Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]]; } } const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefg'+ 'gmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfph'+ 'nmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>c'+ 'qfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti'+ '^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrk'+ 'shserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlping'+ 'kfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjd'+ 'hcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wag'+ 'sg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmh'+ 'n<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o'+ '_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnop'+ 'mrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<'+ 'q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c'+ '^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcnd'+ 'pfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqpo'+ 'rlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs'+ '<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkc'+ 'ieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfeh'+ 'dkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_j'+ 'bjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^'+ 'k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofp'+ 'ips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkd'+ 'mepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsks'+ 'mrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj['+ 'i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjci'+ 'eigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhill'+ 'nmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map( r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret = []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0) -106));} return ret; })]; }); return new Text(); } // Seeded random - Mulberry32 function Random(seed) { class Random { constructor(seed) { this.seed = seed; } next() { var t = this.seed += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; } shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(this.next() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } } return new Random(seed); }