Files
freeCodeCamp/curriculum/challenges/japanese/06-quality-assurance/quality-assurance-projects/sudoku-solver.md
2022-02-25 03:41:18 +09:00

18 KiB
Raw Blame History

id, title, challengeType, forumTopicId, dashedName
id title challengeType forumTopicId dashedName
5e601bf95ac9d0ecd8b94afd 数独ソルバー 4 462357 sudoku-solver

--description--

https://sudoku-solver.freecodecamp.rocks/ と同様の機能を持つフルスタック JavaScript アプリを構築してください。 プロジェクトに取り組むにあたり、以下の方法のうち 1 つを用いてコードを記述します。

  • GitHub レポジトリをクローンし、ローカル環境でチャレンジを完了させる。
  • Replit 始動プロジェクトを使用して、プロジェクトを完了させる。
  • 使い慣れたサイトビルダーを使用してプロジェクトを完了させる。 必ず GitHub リポジトリのすべてのファイルを取り込む。

完了したら、プロジェクトの動作デモをどこか公開の場にホストしてください。 そして、Solution Link フィールドでデモへの URL を送信してください。 必要に応じて、GitHub Link フィールドでプロジェクトのソースコードへのリンクを送信してください。

--instructions--

  • すべてのパズルロジックを /controllers/sudoku-solver.js に含めてください。
    • validate 関数は、与えられたパズル文字列を受け取り、入力に 81 の有効な文字があるかどうかを確認する必要があります。
    • check 関数は、ボードの現在の状態を確認する必要があります。
    • solve 関数は、テスト入力と解答だけでなく、与えられた任意の有効なパズル文字列を解く処理を行う必要があります。 パズルを解くためのロジックを記述することが求められます。
  • すべてのルーティングロジックを /routes/api.js に含めてください。
  • アプリで解くべきパズルのサンプルについては、/controllerspuzzle-strings.js ファイルを参照してください。
  • このページでチャレンジテストを実行するには、.env ファイル内で引用符を付けずに NODE_ENVtest に設定してください。
  • コンソールでテストを実行するには、コマンド npm run test を使用してください。 Replit コンソールを開くには、Ctrl+Shift+P (Macの場合はCmd) を押して「open shell」と入力してください。

tests/1_unit-tests.js に以下のテストを記述してください。

  • ロジックは、81 文字の有効なパズル文字列を処理します。
  • ロジックは、(19 でも . でもない) 無効な文字が含まれているパズル文字列を処理します。
  • ロジックは、81 文字ではないパズル文字列を処理します。
  • ロジックは、有効な行の配置を処理します。
  • ロジックは、無効な行の配置を処理します。
  • ロジックは、有効な列の配置を処理します。
  • ロジックは、無効な列の配置を処理します。
  • ロジックは、有効な領域 (3x3 グリッド) の配置を処理します。
  • ロジックは、無効な領域 (3x3 グリッド) の配置を処理します。
  • 有効なパズルの文字列は、ソルバーをパスします。
  • 無効なパズル文字列は、ソルバーをパスしません。
  • 不完全なパズルの場合、ソルバーは期待される解答を返します。

tests/2_functional-tests.js に以下のテストを記述してください。

  • 有効なパズル文字列のパズルを解いてください: /api/solve への POST リクエスト
  • パズル文字列が不足しているパズルを解いてください: /api/solve への POST リクエスト
  • 無効な文字のパズルを解いてください: /api/solve への POST リクエスト
  • 誤った長さのパズルを解いてください: /api/solve への POST リクエスト
  • 解くことができないパズルを解いてください: /api/solve への POST リクエスト
  • すべてのフィールドのパズル配置を確認してください: /api/check への POST リクエスト
  • 1 つの配置が競合しているパズル配置を確認してください: /api/check への POST リクエスト
  • 複数の配置が競合しているパズル配置を確認してください: /api/check への POST リクエスト
  • すべての配置が競合しているパズルの配置を確認してください: /api/check への POST リクエスト
  • 必須フィールドがないパズル配置を確認してください: /api/checkへの POST リクエスト
  • 無効な文字のパズル配置を確認してください: /api/check への POST リクエスト
  • 誤った長さのパズル配置を確認してください: /api/check への POST リクエスト
  • 無効な配置座標のパズル配置を確認してください: /api/check への POST リクエスト
  • 無効な配置値のパズル配置を確認してください: /api/check への POST リクエスト

--hints--

サンプルの URL ではなく、自分で作成したプロジェクトを提供する必要があります。

(getUserInput) => {
  const url = getUserInput('url');
  assert(!/.*\/sudoku-solver\.freecodecamp\.rocks/.test(getUserInput('url')));
};

数字 (19) と空白を表すピリオド . の組み合わせを含む文字列である puzzle を含むフォームデータを指定して、POST /api/solve を実行することができます。 返されるオブジェクトには、解いたパズルを含む solution プロパティが含まれます。

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);
};

/api/solve へ送信されたオブジェクトに puzzle がない場合、戻り値は { 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);
};

/api/solve へ送信されたパズルに数字でもピリオドでもない値が含まれている場合、戻り値は { 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);
};

/api/solve へ送信されたパズルの文字数が 81 文字より多いまたは少ない場合、戻り値は { 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);
  }
};

/api/solve へ送信されたパズルが無効もしくは解けない場合、戻り値は { 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);
};

/api/check オブジェクトへの POST で、puzzlecoordinate および value を含むオブジェクトを指定できます。coordinate は行を示す文字 AI で、その後に列を示す 19 の数字が続きます。value は 19 の数字です。

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);
};

/api/check への POST の戻り値は、valid プロパティを含むオブジェクトになります。このプロパティは、指定された座標に数字を配置できる場合は true になり、そうでない場合は false になります。 false の場合、返されるオブジェクトには conflict プロパティも含まれます。このプロパティは、文字列 "row""column""region" の任意の組み合わせを含む配列になります (どれが配置を無効にしているのかによります)。

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');
};

/api/check へ送信された value がすでに puzzle のその coordinate に配置されている場合、戻り値は valid プロパティを含むオブジェクトになり、value が競合していなければ true となります。

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);
};

/api/check へ送信されたパズルに数字でもピリオドでもない値が含まれている場合、戻り値は { 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);
};

/api/check へ送信されたパズルが 81 文字より多いか少ない場合、戻り値は { 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);
  }
};

/api/check へ送信されたオブジェクトに puzzlecoordinate もしくは value がない場合、戻り値は、{ 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);
  }
};

api/check へ送信された座標が既存のグリッドセルを指し示していない場合、戻り値は { 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);
  }
};

/api/check へ送信された value が 1 から 9 の数字でない場合、戻り値は { 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);
  }
};

12 種類のテストがすべて完了し、合格しています。 テストを記述すべき期待される動作については、/tests/1_unit-tests.js を参照してください。

async (getUserInput) => {
  try {
    const getTests = await $.get(getUserInput('url') + '/_api/get-tests');
    assert.isArray(getTests);
    const units = getTests.filter((el) => el.context.includes('UnitTests'));
    assert.isAtLeast(units.length, 12, 'At least 12 tests passed');
    units.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);
  }
};

14 種類の機能テストがすべて完了し、合格しています。 テストを記述すべき機能については、/tests/2_functional-tests.js を参照してください。

async (getUserInput) => {
  try {
    const getTests = await $.get(getUserInput('url') + '/_api/get-tests');
    assert.isArray(getTests);
    const funcs = getTests.filter((el) =>
      el.context.includes('Functional Tests')
    );
    assert.isAtLeast(funcs.length, 14, 'At least 14 tests passed');
    funcs.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);
  }
};

--solutions--

/**
  Backend challenges don't need solutions,
  because they would need to be tested against a full working project.
  Please check our contributing guidelines to learn more.
*/