--description--
Build a full stack JavaScript app that is functionally similar to this: https://sudoku-solver.freecodecamp.rocks/. Working on this project will involve you writing your code using one of the following methods:
- Clone this GitHub repo and complete your project locally.
- Use our Gitpod starter project to complete your project.
- Use a site builder of your choice to complete the project. Be sure to incorporate all the files from our GitHub repo.
--instructions--
- All puzzle logic can go into
/controllers/sudoku-solver.js
- The
validate
function should take a given puzzle string and check it to see if it has 81 valid characters for the input. - The
check
functions should be validating against the current state of the board. - The
solve
function should handle solving any given valid puzzle string, not just the test inputs and solutions. You are expected to write out the logic to solve this.
- The
- All routing logic can go into
/routes/api.js
- See the
puzzle-strings.js
file in/controllers
for some sample puzzles your application should solve - To run the challenge tests on this page, set
NODE_ENV
totest
without quotes in the.env
file - To run the tests in the console, use the command
npm run test
.
Write the following tests in tests/1_unit-tests.js
:
- Logic handles a valid puzzle string of 81 characters
- Logic handles a puzzle string with invalid characters (not 1-9 or
.
) - Logic handles a puzzle string that is not 81 characters in length
- Logic handles a valid row placement
- Logic handles an invalid row placement
- Logic handles a valid column placement
- Logic handles an invalid column placement
- Logic handles a valid region (3x3 grid) placement
- Logic handles an invalid region (3x3 grid) placement
- Valid puzzle strings pass the solver
- Invalid puzzle strings fail the solver
- Solver returns the expected solution for an incomplete puzzle
Write the following tests in tests/2_functional-tests.js
- Solve a puzzle with valid puzzle string: POST request to
/api/solve
- Solve a puzzle with missing puzzle string: POST request to
/api/solve
- Solve a puzzle with invalid characters: POST request to
/api/solve
- Solve a puzzle with incorrect length: POST request to
/api/solve
- Solve a puzzle that cannot be solved: POST request to
/api/solve
- Check a puzzle placement with all fields: POST request to
/api/check
- Check a puzzle placement with single placement conflict: POST request to
/api/check
- Check a puzzle placement with multiple placement conflicts: POST request to
/api/check
- Check a puzzle placement with all placement conflicts: POST request to
/api/check
- Check a puzzle placement with missing required fields: POST request to
/api/check
- Check a puzzle placement with invalid characters: POST request to
/api/check
- Check a puzzle placement with incorrect length: POST request to
/api/check
- Check a puzzle placement with invalid placement coordinate: POST request to
/api/check
- Check a puzzle placement with invalid placement value: POST request to
/api/check
--hints--
You should provide your own project, not the example URL.
(getUserInput) => {
const url = getUserInput('url');
assert(!/.*\/sudoku-solver\.freecodecamp\.rocks/.test(getUserInput('url')));
};
You can POST
/api/solve
with form data containing puzzle
which will be a string containing a combination of numbers (1-9) and periods .
to represent empty spaces. The returned object will contain a solution
property with the solved puzzle.
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output =
'769235418851496372432178956174569283395842761628713549283657194516924837947381625';
const data = await fetch(getUserInput('url') + '/api/solve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input })
});
const parsed = await data.json();
assert.property(parsed, 'solution');
assert.equal(parsed.solution, output);
};
If the object submitted to /api/solve
is missing puzzle
, the returned value will be { error: 'Required field missing' }
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output = 'Required field missing';
const data = await fetch(getUserInput('url') + '/api/solve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notpuzzle: input })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
};
If the puzzle submitted to /api/solve
contains values which are not numbers or periods, the returned value will be { error: 'Invalid characters in puzzle' }
async (getUserInput) => {
const input =
'AA9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output = 'Invalid characters in puzzle';
const data = await fetch(getUserInput('url') + '/api/solve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
};
If the puzzle submitted to /api/solve
is greater or less than 81 characters, the returned value will be { error: 'Expected puzzle to be 81 characters long' }
async (getUserInput) => {
const inputs = [
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6.',
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6...'
];
const output = 'Expected puzzle to be 81 characters long';
for (const input of inputs) {
const data = await fetch(getUserInput('url') + '/api/solve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
}
};
If the puzzle submitted to /api/solve
is invalid or cannot be solved, the returned value will be { error: 'Puzzle cannot be solved' }
async (getUserInput) => {
const input =
'9.9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output = 'Puzzle cannot be solved';
const data = await fetch(getUserInput('url') + '/api/solve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
};
You can POST
to /api/check
an object containing puzzle
, coordinate
, and value
where the coordinate
is the letter A-I indicating the row, followed by a number 1-9 indicating the column, and value
is a number from 1-9.
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const coordinate = 'A1';
const value = '7';
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'valid');
assert.isTrue(parsed.valid);
};
The return value from the POST
to /api/check
will be an object containing a valid
property, which is true
if the number may be placed at the provided coordinate and false
if the number may not. If false, the returned object will also contain a conflict
property which is an array containing the strings "row"
, "column"
, and/or "region"
depending on which makes the placement invalid.
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const coordinate = 'A1';
const value = '1';
const conflict = ['row', 'column'];
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'valid');
assert.isFalse(parsed.valid);
assert.property(parsed, 'conflict');
assert.include(parsed.conflict, 'row');
assert.include(parsed.conflict, 'column');
};
If value
submitted to /api/check
is already placed in puzzle
on that coordinate
, the returned value will be an object containing a valid
property with true
if value
is not conflicting.
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const coordinate = 'C3';
const value = '2';
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'valid');
assert.isTrue(parsed.valid);
};
If the puzzle submitted to /api/check
contains values which are not numbers or periods, the returned value will be { error: 'Invalid characters in puzzle' }
async (getUserInput) => {
const input =
'AA9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const coordinate = 'A1';
const value = '1';
const output = 'Invalid characters in puzzle';
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
};
If the puzzle submitted to /api/check
is greater or less than 81 characters, the returned value will be { error: 'Expected puzzle to be 81 characters long' }
async (getUserInput) => {
const inputs = [
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6.',
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6...'
];
const coordinate = 'A1';
const value = '1';
const output = 'Expected puzzle to be 81 characters long';
for (const input of inputs) {
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
}
};
If the object submitted to /api/check
is missing puzzle
, coordinate
or value
, the returned value will be { error: 'Required field(s) missing' }
async (getUserInput) => {
const inputs = [
{
puzzle: '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..',
value: '1',
},
{
puzzle: '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..',
coordinate: 'A1',
},
{
coordinate: 'A1',
value: '1'
}
];
for (const input of inputs) {
const output = 'Required field(s) missing';
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input)
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
}
};
If the coordinate submitted to api/check
does not point to an existing grid cell, the returned value will be { error: 'Invalid coordinate'}
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output = 'Invalid coordinate';
const coordinates = ['A0', 'A10', 'J1', 'A', '1', 'XZ18'];
const value = '7';
for (const coordinate of coordinates) {
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
}
};
If the value
submitted to /api/check
is not a number between 1 and 9, the returned value will be { error: 'Invalid value' }
async (getUserInput) => {
const input =
'..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
const output = 'Invalid value';
const coordinate = 'A1';
const values = ['0', '10', 'A'];
for (const value of values) {
const data = await fetch(getUserInput('url') + '/api/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ puzzle: input, coordinate, value })
});
const parsed = await data.json();
assert.property(parsed, 'error');
assert.equal(parsed.error, output);
}
};
All 12 unit tests are complete and passing.
async (getUserInput) => {
try {
const getTests = await $.get(getUserInput('url') + '/_api/get-tests');
assert.isArray(getTests);
const unitTests = getTests.filter((test) => {
return !!test.context.match(/Unit\s*Tests/gi);
});
assert.isAtLeast(unitTests.length, 12, 'At least 12 tests passed');
unitTests.forEach((test) => {
assert.equal(test.state, 'passed', 'Test in Passed State');
assert.isAtLeast(
test.assertions.length,
1,
'At least one assertion per test'
);
});
} catch (err) {
throw new Error(err.responseText || err.message);
}
};
All 14 functional tests are complete and passing.
async (getUserInput) => {
try {
const getTests = await $.get(getUserInput('url') + '/_api/get-tests');
assert.isArray(getTests);
const functTests = getTests.filter((test) => {
return !!test.context.match(/Functional\s*Tests/gi);
});
assert.isAtLeast(functTests.length, 14, 'At least 14 tests passed');
functTests.forEach((test) => {
assert.equal(test.state, 'passed', 'Test in Passed State');
assert.isAtLeast(
test.assertions.length,
1,
'At least one assertion per test'
);
});
} catch (err) {
throw new Error(err.responseText || err.message);
}
};