Skip to content

Commit

Permalink
Merge pull request #3 from sondregj/improvements/solver
Browse files Browse the repository at this point in the history
✒️ Improvements and refinements
  • Loading branch information
sondregj authored Sep 26, 2019
2 parents 89101e2 + 9b1c988 commit c06a21c
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 142 deletions.
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ env:
- NODE_ENV=CI
matrix:
include:
- name: Node 8
node_js: "8"
- name: Node 10
node_js: "10"
#- name: Node 8
# node_js: "8"
#- name: Node 10
# node_js: "10"
- name: Node 12
node_js: "12"
install: npm install
Expand Down
97 changes: 55 additions & 42 deletions src/board.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { InvalidArrayError } from './errors'
import { checkAllRowsAndColumnsOfBoard } from './checks'
import { transformAllRowsAndColumnsOfBoard } from './transforms'

import { isSolved } from './solver/validator'
import { transposeBoard } from './utils'

import {
BoardTransformer,
IHitoriBoard,
IHitoriCell,
IHitoriColumn,
IHitoriRow,
LineChecker,
LineTransformer,
} from './types'

import { isSolved } from './solver/validator'
import { transposeBoard } from './utils'
import { InvalidArrayError } from './errors'

class HitoriBoard implements IHitoriBoard {
/* Constructors */

public static from2DArray(array: number[][]): HitoriBoard {
const size = array.length

Expand Down Expand Up @@ -72,16 +79,55 @@ class HitoriBoard implements IHitoriBoard {
this.rows = board.rows
}

public getCoordinate(x: number, y: number): IHitoriCell {
if (Math.min(x, y) < 0 || Math.max(x, y) + 1 > this.size) {
throw new Error('Index out of bounds.')
}

return this.rows[y].cells[x]
}

/* Checks */

public solved(): boolean {
return isSolved(this)
}

/* Transforms */

public transformAllRowsAndColumns(lineTransformer: LineTransformer): HitoriBoard {
const { size } = this

const rows = this.asRows
const transformedRows = rows.map(row => lineTransformer(row))

const intermediateBoard: HitoriBoard = new HitoriBoard({
rows: transformedRows,
size,
})

const columns = intermediateBoard.asColumns
const transformedColumns = columns.map(column => lineTransformer(column))

return new HitoriBoard({ rows: transposeBoard(transformedColumns), size })
}

public transformBoard(boardTransformer: BoardTransformer): HitoriBoard {
const transformedBoard = boardTransformer(this)

return new HitoriBoard(transformedBoard)
}

/* Representations */

public get asRows(): IHitoriRow[] {
return this.rows
}

public get asColumns(): IHitoriColumn[] {
const rows = this.rows

return rows[0].cells.map((col, i) => ({
cells: rows.map(row => row.cells[i]),
}))
return transposeBoard(this.rows)
}

public to2DArray(): number[][] {
Expand All @@ -95,17 +141,11 @@ class HitoriBoard implements IHitoriBoard {
*/
public toString = (): string =>
this.rows
.map(row => row.cells)
.flat()
//.flatMap(row => row.cells)
.reduce<IHitoriCell[]>((all, row) => [...all, ...row.cells], [])
.reduce((prev, curr) => prev + (curr.confirmedBlack ? 'X' : curr.value), '')

public getCoordinate(x: number, y: number): IHitoriCell {
if (Math.min(x, y) < 0 || Math.max(x, y) + 1 > this.size) {
throw new Error('Index out of bounds.')
}

return this.rows[y].cells[x]
}
/* Operations */

public copy(): HitoriBoard {
const rows = this.asRows.map(row => ({
Expand All @@ -122,33 +162,6 @@ class HitoriBoard implements IHitoriBoard {

return { size: this.size, rows }
}

public transformAllRowsAndColumns(lineTransformer: LineTransformer): HitoriBoard {
const { size } = this

const rows = this.asRows
const transformedRows = rows.map(row => lineTransformer(row))

const intermediateBoard: HitoriBoard = new HitoriBoard({
rows: transformedRows,
size,
})

const columns = intermediateBoard.asColumns
const transformedColumns = columns.map(column => lineTransformer(column))

return new HitoriBoard({ rows: transposeBoard(transformedColumns), size })
}

public transformBoard(boardTransformer: BoardTransformer): HitoriBoard {
const transformedBoard = boardTransformer(this)

return new HitoriBoard(transformedBoard)
}

public solved(): boolean {
return isSolved(this)
}
}

export default HitoriBoard
26 changes: 26 additions & 0 deletions src/checks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import HitoriBoard from '../board'
import { LineChecker } from '../types'
import { transposeBoard } from '../utils'

export function checkAllRowsAndColumnsOfBoard(
board: HitoriBoard,
lineChecker: LineChecker,
): boolean {
const boardToCheck = board.copy()

const checkedRows = boardToCheck.asRows.map(row => lineChecker(row))
const checkedColumns = boardToCheck.asColumns.map(column => lineChecker(column))

return [...checkedRows, ...checkedColumns].reduce<boolean>(
(result, current) => result && current,
true,
)
}

export { noAdjacentBlackOnLine, onlyDistinctOnLine } from './line'
export {
allCellsOrthogonallyConnected,
conflictsHorizontallyOrVertically,
noAdjacentBlackOnBoard,
onlyDistinctOnLinesOnBoard,
} from './board'
6 changes: 3 additions & 3 deletions src/solver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ export function solve({
if (!skipPatterns) {
let proposal: HitoriBoard

// TODO 1. Starting techniques
// 1. Starting techniques
proposal = startingTechniques(lastProposal())
iterations.push(proposal)

// TODO 2. Corner techniques
// 2. Corner techniques
proposal = cornerTechniques(lastProposal())
iterations.push(proposal)

// TODO 3. Advanced techniques
// 3. Advanced techniques
proposal = advancedTechniques(lastProposal())
iterations.push(proposal)
}
Expand Down
2 changes: 2 additions & 0 deletions src/solver/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export const cellBetweenTwoEqual = (
export const pairInduction = (
line: IHitoriRow | IHitoriColumn,
): IHitoriRow | IHitoriColumn => {
// TODO

return line
}

Expand Down
121 changes: 44 additions & 77 deletions src/solver/recursive.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,95 @@
import HitoriBoard from '../board'
import { IHitoriRow } from '../types'

import { conflictsHorizontallyOrVertically } from '../checks/board'
import { isSolved } from './validator'

import { IHitoriRow } from '../types'

export function solveRecursive(board: HitoriBoard): HitoriBoard | undefined {
const { size } = board

// Make a copy of the board
const testRows: IHitoriRow[] = [...board.rows].map(row => ({
cells: row.cells.map(cell => ({ ...cell })),
}))

// Cache cells with no conflicts before recursing for performance
const noConflicts: boolean[][] = testRows.map((row, yIndex) =>
testRows[0].cells.map(
(column, xIndex) =>
!conflictsHorizontallyOrVertically(
new HitoriBoard({ size, rows: testRows }),
xIndex,
yIndex,
),
),
)
// Make a copy of the board so that we're certain we don't change it
const testRows: IHitoriRow[] = board.copy().asRows

const solvedRows = _solveRecursive(
testRows,
board.asRows,
board.size,
0,
0,
noConflicts,
)
// Check all valid configurations of the board recursively
const solvedRows = _solveRecursive(testRows, board.asRows, board.size)

if (!solvedRows) {
return undefined
}

return new HitoriBoard({ size: board.size, rows: solvedRows })
return solvedRows && new HitoriBoard({ size: board.size, rows: solvedRows })
}

function _solveRecursive(
rows: IHitoriRow[],
confirmed: IHitoriRow[],
size: number,
x: number,
y: number,
noConflicts: boolean[][],
/*noConflicts: boolean[][],*/
x: number = 0,
y: number = 0,
): IHitoriRow[] | undefined {
// * - OPTIMALIZATION - *
// The check for solved currently takes around 1/8 of the time.
// For the main test set that currently means going from 800 secs for main test,
// to 100 secs, which is way too much. Best efforts may be placed in pattern techniques,
// and similar things, as each cell confirmed before recursing cuts the time in half.
// - Further optimalizations
//
// The check for solved currently takes around 7/8 of the time.
// For the main test set that currently means going from 800 secs for main test,
// to 100 secs, which is way too much anyway. Best efforts may be placed in pattern techniques,
// and similar things, as each cell confirmed before recursing cuts the time in half.

// Base case, all rules satisfied
if (isSolved(new HitoriBoard({ rows, size }))) {
return rows
}

// Out of bounds
if (Math.max(x, y) >= size) {
// Out of bounds, this means we have exhausted all options without
// finding a solution
if (y >= size) {
return undefined
}

// Compute the indices to check in next recursive call
const nextX = x >= size - 1 ? 0 : x + 1
const nextY = x >= size - 1 ? y + 1 : y

// For each choice that can be made
// Here, we branch of into all the valid solution trees
// We respect the choices made in previous pattern checkers
// Another goal here is also to eliminate decision trees that we know
// are going to fail, by the rules of Hitori
// -> Make that choice and recur

// - There are no conflicts
// if (noConflicts[y][x]) {
// return _solveRecursive(rows, confirmed, size, nextX, nextY, noConflicts)
// }
// For each valid choice
// -> Make that choice and recur

// - We have already concluded on this cell before recursing
/**
* - We have already concluded on this cell before recursing
*
* In this case we assume it is correct and move on.
*/
if (confirmed[y].cells[x].confirmedWhite || confirmed[y].cells[x].confirmedBlack) {
return _solveRecursive(rows, confirmed, size, nextX, nextY, noConflicts)
return _solveRecursive(rows, confirmed, size, nextX, nextY)
}

const noAdjacentBlackCell: boolean = !adjacentBlackCell(rows, x, y)

// - Test making black

// We only need to test a black cell in this position if there
// are none adjacent to it
if (noAdjacentBlackCell) {
/**
* - Test making black
*
* We only need to test a black cell in this position if there
* are none adjacent to it
*/
if (!adjacentBlackCell(rows, x, y)) {
rows[y].cells[x].confirmedBlack = true
const cellBlackResult = _solveRecursive(
rows,
confirmed,
size,
nextX,
nextY,
noConflicts,
)
const cellBlackResult = _solveRecursive(rows, confirmed, size, nextX, nextY)

if (cellBlackResult) {
return cellBlackResult
}
}

// - Test not making black
/**
* - Test not making black
*
* Any number of cells in a row can be white, so we need to
* check all of them.
*/
rows[y].cells[x].confirmedBlack = false

const cellNotBlackResult = _solveRecursive(
rows,
confirmed,
size,
nextX,
nextY,
noConflicts,
)
const cellNotBlackResult = _solveRecursive(rows, confirmed, size, nextX, nextY)

if (cellNotBlackResult) {
return cellNotBlackResult
}

// No choices remain
// After exhausting all the valid decisions, there is no where else to go
return undefined
}

Expand Down
Loading

0 comments on commit c06a21c

Please sign in to comment.