Commit d9214264 authored by Yannick Li's avatar Yannick Li
Browse files

Refactor and update tests of single-player version

parent d3941362
......@@ -36,58 +36,14 @@ beforeEach(() => {
describe("Testing UI", () => {
/**
* This test evaluates that the cell displays the initial value.
* This test evaluates that the cell displays the value.
* Here it should display 6.
*/
test("Cell value initialization", () => {
test("Cell value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
<Cell index={3} value={"6"} onChange={() => ({})} error={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that after updating the cell value,
* the cell display the right value.
* Here it should display 2.
*/
test("Cell value changed", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("2");
});
/**
* This test evaluates that the value of a locked cell cannot be modified.
* Here it should display 2 even if we try to modify it.
*/
test("Cell locked", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={true} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that the value of a cell can only be modified with integers from 1 to 9.
*/
test("Cell wrong value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "0" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "10" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "1.1" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "abc" } });
expect(wrapper.find("textarea").text()).toBe("6");
const input = wrapper.find("input");
expect(input.instance().value).toBe("6");
});
});
......@@ -51,10 +51,6 @@ class Cell extends React.Component<ICellProps> {
}
if (event.target.value === "" || validInput.test(event.target.value)) {
this.props.onChange(this.props.index, event.target.value);
} else {
console.error(
"Invalid input in cell " + this.props.index + " : " + event.target.value
);
}
}
......
This diff is collapsed.
......@@ -23,15 +23,259 @@
*/
import React from "react";
import Grid from "./Grid";
import assert from "assert";
import Grid, { cellsIndexOfBlock } from "./Grid";
import { validInput, GRIDS } from "../constants";
/**
* Interface for the state of a Game.
* Keep a reference to the opened session and opened MVMap.
*/
interface IGameState {
cells: { value: string; modifiable: boolean; error: boolean }[];
isFinished: boolean;
}
/**
* This class represent the Game that glues all components together.
*/
class Game extends React.Component {
class Game extends React.Component<Record<string, unknown>, IGameState> {
constructor(props: Record<string, unknown>) {
super(props);
const cells = new Array(81)
.fill(null)
.map(() => ({ value: "", modifiable: false, error: false }));
this.state = {
cells: cells,
isFinished: false,
};
}
/**
* Called after the component is rendered.
* It set a timer to refresh cells values.
*/
componentDidMount(): void {
this.initFrom(generateStaticGrid());
}
/**
* Initialize the grid with the given values.
* @param values values to be set in the grid.
*/
initFrom(values: string): void {
assert.ok(values.length === 81);
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
cells[index].value = values[index] === "." ? "" : values[index];
cells[index].modifiable = values[index] === "." ? true : false;
}
this.updateState(cells);
}
/**
* Reset the value of all modifiable cells.
*/
reset(): void {
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
if (cells[index].modifiable) {
cells[index].value = "";
}
}
this.updateState(cells);
}
/**
* This handler is called when the value of a cell is changed.
* @param index The index of the cell changed.
* @param value The new value of the cell.
*/
handleChange(index: number, value: string): void {
assert.ok(value === "" || (Number(value) >= 1 && Number(value) <= 9));
assert.ok(index >= 0 && index < 81);
if (!this.state.cells[index].modifiable) {
console.error(
"Trying to change an non modifiable cell. Should not happend"
);
}
const cells = this.state.cells;
cells[index].value = value;
this.updateState(cells);
}
render(): JSX.Element {
return <Grid />;
return (
<div className="sudoku">
<div>
<button onClick={this.reset.bind(this)}>Reset</button>
</div>
<br />
<Grid
cells={this.state.cells}
isFinished={this.state.isFinished}
onChange={(index: number, value: string) =>
this.handleChange(index, value)
}
/>
</div>
);
}
/**
* Check if a line respect Sudoku lines rules.
* @param line The line number to be checked.
*/
checkLine(line: number): boolean {
assert.ok(line >= 0 && line < 9);
const cpt = Array(9).fill(0);
for (let column = 0; column < 9; column++) {
const index = line * 9 + column;
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* Check if a column respect Sudoku columns rules.
* @param column The column number to be checked.
*/
checkColumn(column: number): boolean {
assert.ok(column >= 0 && column < 9);
const cpt = Array(9).fill(0);
for (let line = 0; line < 9; line++) {
const index = line * 9 + column;
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* Check if a block respect Sudoku blocks rules.
* @param block The block number to be checked.
*/
checkBlock(block: number): boolean {
assert.ok(block >= 0 && block < 9);
const cpt = Array(9).fill(0);
const indexList = cellsIndexOfBlock(block);
for (const index of indexList) {
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* This function check if all lines respect Sudoku lines rules.
*/
checkLines(): number[] {
const indexList = [];
for (let line = 0; line < 9; line++) {
if (this.checkLine(line) === false) {
for (let column = 0; column < 9; column++) {
indexList.push(line * 9 + column);
}
}
}
return indexList;
}
/**
* This function check if all columns respect Sudoku columns rules.
*/
checkColumns(): number[] {
const indexList = [];
for (let column = 0; column < 9; column++) {
if (this.checkColumn(column) === false) {
for (let line = 0; line < 9; line++) {
indexList.push(line * 9 + column);
}
}
}
return indexList;
}
/**
* This function check if all blocks respect Sudoku blocks rules.
*/
checkBlocks(): number[] {
let indexList: number[] = [];
for (let block = 0; block < 9; block++) {
if (this.checkBlock(block) === false) {
indexList = indexList.concat(cellsIndexOfBlock(block));
}
}
return indexList;
}
/**
* This function check if cells contains multiple values.
*/
checkCells(): number[] {
const indexList = [];
for (let cell = 0; cell < 81; cell++) {
const val = this.state.cells[cell].value;
if (val.length > 1) {
indexList.push(cell);
}
}
return indexList;
}
/**
* Check if all cells respect Sudoku rules and update cells.
* @param cells Current cells values.
*/
updateState(
cells: { value: string; modifiable: boolean; error: boolean }[]
): void {
let errorIndexList = this.checkLines();
errorIndexList = errorIndexList.concat(this.checkColumns());
errorIndexList = errorIndexList.concat(this.checkBlocks());
errorIndexList = errorIndexList.concat(this.checkCells());
const errorIndexSet = new Set(errorIndexList);
for (let index = 0; index < 81; index++) {
if (errorIndexSet.has(index)) {
cells[index].error = true;
} else {
cells[index].error = false;
}
}
if (errorIndexSet.size) {
this.setState({ cells: cells, isFinished: false });
return;
}
for (let index = 0; index < 81; index++) {
if (!validInput.test(this.state.cells[index].value)) {
this.setState({ cells: cells, isFinished: false });
return;
}
}
this.setState({ cells: cells, isFinished: true });
}
}
/**
* Return a predefined Sudoku grid as a string.
*/
function generateStaticGrid(): string {
return GRIDS["1"];
}
export default Game;
This diff is collapsed.
......@@ -25,83 +25,22 @@
import React from "react";
import assert from "assert";
import Cell from "./Cell";
import { validInput, GRIDS } from "../constants";
/**
* Interface for the state of the Grid
* Interface for the properties of the Grid
*/
interface IGridState {
interface IGridProps {
cells: { value: string; modifiable: boolean; error: boolean }[];
isFinished: boolean;
onChange: (index: number, value: string) => void;
}
/**
* This class represent the grid of the Sudoku
*/
class Grid extends React.Component<Record<string, unknown>, IGridState> {
constructor(props: Record<string, unknown>) {
super(props);
const cells = new Array(81)
.fill(null)
.map(() => ({ value: "", modifiable: false, error: false }));
this.state = {
cells: cells,
isFinished: false,
};
}
/**
* Called after the component is rendered.
* It set a timer to refresh cells values.
*/
componentDidMount(): void {
this.initFrom(generateStaticGrid());
}
/**
* Initialize the grid with the given values.
* @param values values to be set in the grid.
*/
initFrom(values: string): void {
assert.ok(values.length === 81);
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
cells[index].value = values[index] === "." ? "" : values[index];
cells[index].modifiable = values[index] === "." ? true : false;
}
this.updateState(cells);
}
/**
* Reset the value of all modifiable cells.
*/
reset(): void {
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
if (cells[index].modifiable) {
cells[index].value = "";
}
}
this.updateState(cells);
}
/**
* This handler is called when the value of a cell is changed.
* @param index The index of the cell changed.
* @param value The new value of the cell.
*/
class Grid extends React.Component<IGridProps> {
handleChange(index: number, value: string): void {
assert.ok(value === "" || (Number(value) >= 1 && Number(value) <= 9));
assert.ok(index >= 0 && index < 81);
if (!this.state.cells[index].modifiable) {
console.error(
"Trying to change an non modifiable cell. Should not happend"
);
}
const cells = this.state.cells;
cells[index].value = value;
this.updateState(cells);
this.props.onChange(index, value);
}
/**
......@@ -113,13 +52,13 @@ class Grid extends React.Component<Record<string, unknown>, IGridState> {
return (
<Cell
index={index}
value={this.state.cells[index].value}
value={this.props.cells[index].value}
onChange={
this.state.cells[index].modifiable
this.props.cells[index].modifiable
? (index: number, value: string) => this.handleChange(index, value)
: null
}
error={this.state.cells[index].error}
error={this.props.cells[index].error}
/>
);
}
......@@ -130,7 +69,7 @@ class Grid extends React.Component<Record<string, unknown>, IGridState> {
*/
renderBlock(blockNum: number): JSX.Element {
assert.ok(blockNum >= 0 && blockNum < 9);
const index = blockIndex(blockNum);
const index = cellsIndexOfBlock(blockNum);
return (
<td>
{this.renderCell(index[0])}
......@@ -153,11 +92,7 @@ class Grid extends React.Component<Record<string, unknown>, IGridState> {
*/
render(): JSX.Element {
return (
<div className="sudoku">
<div>
<button onClick={this.reset.bind(this)}>Reset</button>
</div>
<br />
<div>
<table className="grid">
<tbody>
{[0, 1, 2].map((line) => (
......@@ -169,7 +104,7 @@ class Grid extends React.Component<Record<string, unknown>, IGridState> {
))}
</tbody>
</table>
{this.state.isFinished && (
{this.props.isFinished && (
<h2 className="status" id="status">
Sudoku completed
</h2>
......@@ -177,167 +112,13 @@ class Grid extends React.Component<Record<string, unknown>, IGridState> {
</div>
);
}
/**
* Check if a line respect Sudoku lines rules.
* @param line The line number to be checked.
*/
checkLine(line: number): boolean {
assert.ok(line >= 0 && line < 9);
const cpt = Array(9).fill(0);
for (let column = 0; column < 9; column++) {
const index = line * 9 + column;
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* Check if a column respect Sudoku columns rules.
* @param column The column number to be checked.
*/
checkColumn(column: number): boolean {
assert.ok(column >= 0 && column < 9);
const cpt = Array(9).fill(0);
for (let line = 0; line < 9; line++) {
const index = line * 9 + column;
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* Check if a block respect Sudoku blocks rules.
* @param block The block number to be checked.
*/
checkBlock(block: number): boolean {
assert.ok(block >= 0 && block < 9);
const cpt = Array(9).fill(0);
const indexList = blockIndex(block);
for (const index of indexList) {
const val = this.state.cells[index].value;
if (val.length === 0 || val.length > 1) {
continue;
}
cpt[Number(val) - 1]++;
}
return cpt.every((c) => c <= 1);
}
/**
* This function check if all lines respect Sudoku lines rules.
*/
checkLines(): number[] {
const indexList = [];
for (let line = 0; line < 9; line++) {
if (this.checkLine(line) === false) {
for (let column = 0; column < 9; column++) {
indexList.push(line * 9 + column);
}
}
}
return indexList;
}
/**
* This function check if all columns respect Sudoku columns rules.
*/
checkColumns(): number[] {
const indexList = [];
for (let column = 0; column < 9; column++) {
if (this.checkColumn(column) === false) {
for (let line = 0; line < 9; line++) {
indexList.push(line * 9 + column);
}
}
}
return indexList;
}
/**
* This function check if all blocks respect Sudoku blocks rules.
*/
checkBlocks(): number[] {
let indexList: number[] = [];
for (let block = 0; block < 9; block++) {
if (this.checkBlock(block) === false) {
indexList = indexList.concat(blockIndex(block));
}
}
return indexList;
}
/**
* This function check if cells contains multiple values.
*/
checkCells(): number[] {
const indexList = [];
for (let