Quality Assurance Projects - Sudoku Solver

Tell us what’s happening:

Hello. I have a few issues with this project I have problems to solve.

Project files

controllers/sudoku-solver.js

class SudokuSolver {

  validate(puzzleString) {
    if (/[^1-9\.]/.test(puzzleString)) {
      return { valid: false, error: 'Invalid characters in puzzle' };
    }
  
    if (puzzleString.length !== 81) {
      return { valid: false, error: 'Expected puzzle to be 81 characters long' };
    }
  
    return { valid: true };
  }

  checkRowPlacement(puzzleString, row, column, value) {
    const rowStart = row * 9;
    const rowValues = puzzleString.slice(rowStart, rowStart + 9);
    if (rowValues.includes(value)) {
      return false;
    }
    return true;
  }

  checkColPlacement(puzzleString, row, column, value) {
    for (let i = 0; i < 9; i++) {
      if (puzzleString[column + i * 9] === value) {
        return false;
      }
    }
    return true;
  }

  checkRegionPlacement(puzzleString, row, column, value) {
    const startRow = Math.floor(row / 3) * 3;
    const startCol = Math.floor(column / 3) * 3;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (puzzleString[(startRow + i) * 9 + (startCol + j)] === value) {
          return false;
        }
      }
    }
    return true;
  }
  
  solve(puzzleString) {
    const validationResult = this.validate(puzzleString);
    if (!validationResult.valid) {
      return 'No solution exists';
    }
  
    const solveRecursive = (puzzle) => {
      const emptyIndex = puzzle.indexOf('.');
      if (emptyIndex === -1) {
        return puzzle;
      }
  
      const row = Math.floor(emptyIndex / 9);
      const col = emptyIndex % 9;
  
      for (let value = 1; value <= 9; value++) {
        const stringValue = value.toString();
        if (
          this.checkRowPlacement(puzzle, row, col, stringValue) &&
          this.checkColPlacement(puzzle, row, col, stringValue) &&
          this.checkRegionPlacement(puzzle, row, col, stringValue)
        ) {
          const newPuzzle = puzzle.slice(0, emptyIndex) + stringValue + puzzle.slice(emptyIndex + 1);
          const solvedPuzzle = solveRecursive(newPuzzle);
          if (solvedPuzzle) {
            return solvedPuzzle;
          }
        }
      }
      return null;
    };
  
    return solveRecursive(puzzleString) || 'No solution exists';
  }
    
}

module.exports = SudokuSolver;

routes/api.js

'use strict';

const SudokuSolver = require('../controllers/sudoku-solver.js');

module.exports = function (app) {
  
  let solver = new SudokuSolver();

  app.route('/api/solve')
  .post((req, res) => {
    const { puzzle } = req.body;

    if (!puzzle) {
      return res.json({ error: 'Required field missing' });
    }

    const validationResult = solver.validate(puzzle);
    if (!validationResult.valid) {
      return res.json({ error: validationResult.error });
    }

    const solution = solver.solve(puzzle);
    if (solution === 'No solution exists') {
      return res.json({ error: 'Puzzle cannot be solved' });
    }

    res.json({ solution });
  });

  app.route('/api/check')
  .post((req, res) => {
    const { puzzle, coordinate, value } = req.body;
    
    if (!puzzle || !coordinate || !value) { 
      return res.json({ error: 'Required field(s) missing' });
    }

    if (puzzle.length > 81 || puzzle.length < 81) {
      return res.json({ error: 'Expected puzzle to be 81 characters long' });
    }

    const validationResult = solver.validate(puzzle);
    if (!validationResult.valid) {
      return res.json({ error: validationResult.error });
    }

    const rowLetters = 'ABCDEFGHI';
    const rowLetter = coordinate[0].toUpperCase();
    const column = parseInt(coordinate[1], 10) - 1;

    const row = rowLetters.indexOf(rowLetter);

    if (row === -1 || column < 0 || column > 8) {
      return res.json({ error: 'Invalid coordinate' });
    }

    if (!/^[1-9]$/.test(value)) {
      return res.json({ error: 'Invalid value' });
    }

    if (puzzle[row * 9 + column] === value) {
      return res.json({ valid: true });
    }

    const rowValid = solver.checkRowPlacement(puzzle, row, column, value);
    const colValid = solver.checkColPlacement(puzzle, row, column, value);
    const regionValid = solver.checkRegionPlacement(puzzle, row, column, value);

    if (rowValid && colValid && regionValid) {
      return res.json({ valid: true });
    } else {
      let conflict = [];
      if (!rowValid) conflict.push('row');
      if (!colValid) conflict.push('column');
      if (!regionValid) conflict.push('region');
      return res.json({ valid: false, conflict });
    }
  });

};

tests/1_unit-tests.js

const chai = require('chai');
const assert = chai.assert;

const Solver = require('../controllers/sudoku-solver.js');
let solver = new Solver();

suite('Unit Tests', () => {
  test('Logic handles a valid puzzle string of 81 characters', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isTrue(solver.validate(puzzleString).valid);
  });

  test('Logic handles a puzzle string with invalid characters', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....X..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.deepEqual(solver.validate(puzzleString), { valid: false, error: 'Invalid characters in puzzle' });
  });

  test('Logic handles a puzzle string that is not 81 characters in length', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37'; // 80 characters
    assert.deepEqual(solver.validate(puzzleString), { valid: false, error: 'Expected puzzle to be 81 characters long' });
  });

  test('Logic handles a valid row placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isTrue(solver.checkRowPlacement(puzzleString, 0, 1, '3'));
  });

  test('Logic handles an invalid row placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isFalse(solver.checkRowPlacement(puzzleString, 0, 1, '5'));
  });

  test('Logic handles a valid column placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isTrue(solver.checkColPlacement(puzzleString, 0, 1, '3'));
  });

  test('Logic handles an invalid column placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isFalse(solver.checkColPlacement(puzzleString, 0, 1, '6'));
  });

  test('Logic handles a valid region placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isTrue(solver.checkRegionPlacement(puzzleString, 0, 1, '3'));
  });

  test('Logic handles an invalid region placement', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.isFalse(solver.checkRegionPlacement(puzzleString, 0, 1, '5'));
  });

  test('Valid puzzle strings pass the solver', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    let solution = solver.solve(puzzleString);
    assert.equal(solution, '135762984946381257728459613694517832812936745357824196473298561581673429269145378');
  });

  test('Invalid puzzle strings fail the solver', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....X..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    assert.equal(solver.solve(puzzleString), 'No solution exists');
  });
  
  test('Solver returns the expected solution for an incomplete puzzle', function() {
    let puzzleString = '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.';
    let expectedSolution = '135762984946381257728459613694517832812936745357824196473298561581673429269145378';
    assert.equal(solver.solve(puzzleString), expectedSolution);
  });
});

tests/2_functional-tests.js

const chai = require("chai");
const chaiHttp = require('chai-http');
const assert = chai.assert;
const server = require('../server');

chai.use(chaiHttp);

suite('Functional Tests', () => {
  test('Solve a puzzle with valid puzzle string: POST request to /api/solve', function(done) {
    chai.request(server)
      .post('/api/solve')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.solution, '135762984946381257728459613694517832812936745357824196473298561581673429269145378');
        done();
      });
  });

  test('Solve a puzzle with missing puzzle string: POST request to /api/solve', function(done) {
    chai.request(server)
      .post('/api/solve')
      .send({})
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Required field missing');
        done();
      });
  });

  test('Solve a puzzle with invalid characters: POST request to /api/solve', function(done) {
    chai.request(server)
      .post('/api/solve')
      .send({ puzzle: '1.5..2.84..63.12.X.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Invalid characters in puzzle');
        done();
      });
  });

  test('Solve a puzzle with incorrect length: POST request to /api/solve', function(done) {
    chai.request(server)
      .post('/api/solve')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Expected puzzle to be 81 characters long');
        done();
      });
  });

  test('Solve a puzzle that cannot be solved: POST request to /api/solve', function(done) {
    chai.request(server)
      .post('/api/solve')
      .send({ puzzle: '5.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Puzzle cannot be solved');
        done();
      });
  });

  test('Check a puzzle placement with all fields: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: '3' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.valid, true);
        done();
      });
  });
  
  test('Check a puzzle placement with single placement conflict: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: '5' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.valid, false);
        assert.include(res.body.conflict, 'row');
        done();
      });
  });

  test('Check a puzzle placement with multiple placement conflicts: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: '2' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.valid, false);
        assert.include(res.body.conflict, 'row');
        assert.include(res.body.conflict, 'column');
        done();
      });
  });

  test('Check a puzzle placement with all placement conflicts: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: '1' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.valid, false);
        assert.include(res.body.conflict, 'row');
        assert.include(res.body.conflict, 'column');
        assert.include(res.body.conflict, 'region');
        done();
      });
  });

  test('Check a puzzle placement with missing required fields: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Required field(s) missing');
        done();
      });
  });

  test('Check a puzzle placement with invalid characters: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.X.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: '3' })
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Invalid characters in puzzle');
        done();
      });
  });

  test('Check a puzzle placement with incorrect length: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37' })  // 80 chars
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Expected puzzle to be 81 characters long');
        done();
      });
  });

  test('Check a puzzle placement with invalid placement coordinate: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'J1', value: '3' })  // Invalid row 'J'
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Invalid coordinate');
        done();
      });
  });

  test('Check a puzzle placement with invalid placement value: POST request to /api/check', function(done) {
    chai.request(server)
      .post('/api/check')
      .send({ puzzle: '1.5..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.47...8..1..16....926914.37.', coordinate: 'A1', value: 'X' })  // Invalid value 'X'
      .end((err, res) => {
        assert.equal(res.status, 200);
        assert.equal(res.body.error, 'Invalid value');
        done();
      });
  });
});

Your project link(s)

solution: https://3000-freecodecam-boilerplate-6zsq7aqxzgf.ws-eu116.gitpod.io

Problems

One of my freeCodeCamp tests is not passing:

  1. If the coordinate submitted to api/check does not point to an existing grid cell, the returned value will be { error: 'Invalid coordinate'}.

I tried to adjust the order in api/check of the two if conditions, but then two additional freeCodeCamp tests are failing.

And in addition, these functional tests are failing:

  1) Functional Tests
       Check a puzzle placement with all fields: POST request to /api/check:

      Uncaught AssertionError: expected false to equal true
      + expected - actual

      -false
      +true
      
      at /workspace/boilerplate-project-sudoku-solver/tests/2_functional-tests.js:70:16
      at Test.Request.callback (node_modules/superagent/lib/node/index.js:716:12)
      at /workspace/boilerplate-project-sudoku-solver/node_modules/superagent/lib/node/index.js:916:18
      at IncomingMessage.<anonymous> (node_modules/superagent/lib/node/parsers/json.js:19:7)
      at IncomingMessage.emit (node:events:531:35)
      at endReadableNT (node:internal/streams/readable:1696:12)
      at processTicksAndRejections (node:internal/process/task_queues:82:21)

  2) Functional Tests
       Check a puzzle placement with all placement conflicts: POST request to /api/check:

      Uncaught AssertionError: expected true to equal false
      + expected - actual

      -true
      +false
      
      at /workspace/boilerplate-project-sudoku-solver/tests/2_functional-tests.js:106:16
      at Test.Request.callback (node_modules/superagent/lib/node/index.js:716:12)
      at /workspace/boilerplate-project-sudoku-solver/node_modules/superagent/lib/node/index.js:916:18
      at IncomingMessage.<anonymous> (node_modules/superagent/lib/node/parsers/json.js:19:7)
      at IncomingMessage.emit (node:events:531:35)
      at endReadableNT (node:internal/streams/readable:1696:12)
      at processTicksAndRejections (node:internal/process/task_queues:82:21)

  3) Functional Tests
       Check a puzzle placement with incorrect length: POST request to /api/check:

      Uncaught AssertionError: expected 'Required field(s) missing' to equal 'Expected puzzle to be 81 characters long'
      + expected - actual

      -Required field(s) missing
      +Expected puzzle to be 81 characters long
      
      at /workspace/boilerplate-project-sudoku-solver/tests/2_functional-tests.js:142:16
      at Test.Request.callback (node_modules/superagent/lib/node/index.js:716:12)
      at /workspace/boilerplate-project-sudoku-solver/node_modules/superagent/lib/node/index.js:916:18
      at IncomingMessage.<anonymous> (node_modules/superagent/lib/node/parsers/json.js:19:7)
      at IncomingMessage.emit (node:events:531:35)
      at endReadableNT (node:internal/streams/readable:1696:12)
      at processTicksAndRejections (node:internal/process/task_queues:82:21)

Could someone give me any indication? I am unclear where to look for problems. Thank you very much!

Your browser information:

User Agent is: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36

Challenge Information:

Quality Assurance Projects - Sudoku Solver