Всем здравствуйте! Меня зовут Бен Мерриман. Я учусь на факультете разработки программного обеспечения в школе Flatiron, в прошлом инженер по управлению промышленной автоматизацией и начинающий блогер.

В этом сообщении блога я собираюсь создать тестовую игру в крестики-нолики, размещенную на GitHub, с использованием vanilla JS, HTML5 и CSS. Репозиторий размещен по адресу https://github.com/beingmerry/tic-tac-toe.

🎯 Требования 🚩 Тесты 🏗️ Функциональный код

Основное внимание я буду уделять демонстрации того, как я подхожу к проблемам, и первым шагам, которые я предпринимаю при формулировании 🎯 требований, построении 🚩 тестов (впервые делаю эту часть!), а затем создании большего количества 🏗️ кода, управляемого функциями, чтобы улучшить модульность и помочь уменьшить количество ошибок, которые я знаю, я представлю.

Примечание. Я буду постоянно создавать файл JavaScript test.js. Я впервые пробую разработку через тестирование! Любое тестирование будет выделено курсивом, чтобы продемонстрировать 🚩 тестирование и 🟢 прохождение.

Вопрос читателя: смайлики используются стратегически повсюду. Пожалуйста, дайте мне знать, если они не отображаются должным образом или присутствуют ошибки, спасибо! Кроме того… Мне бы хотелось узнать, что вы думаете об использовании эмодзи для кодирования статей в целом. Я обнаружил, что они отлично работают для быстрой организации идей и привлечения внимания к важным разделам, но я мог заметить, что некоторые определенные смайлики или чрезмерное использование смайликов в целом неуместны.

🎯 Требования

Список требований переводится из соответствующей пользовательской истории, данной нам клиентом, учителем или дурацкой идеей, которая однажды пришла нам в голову в час ночи. В крестиках-ноликах 📕 основная история пользователя передавалась из поколения в поколение как:

📕3 в строке из крестиков или ноликов в строке, столбце или диагонали побеждают в игре.

📕Сложнее всего по очереди! За пределами сетки 3x3 играть нельзя!

Но для требований к программированию требуется более буквальная интерпретация:

  1. 🎯 2 игрока ходят по очереди
  2. 🎯 Каждый игрок отмечает X или O в пустом квадрате на сетке из 9 квадратов в расположении 3x3.
  3. 🎯 Чтобы поставить отметку, квадрат должен быть пустым.
  4. 🎯 Теперь ваша очередь ставить метку.
  5. 🎯 Если в какой-либо момент в строке в строках, столбцах или диагоналях отмечены 3 X или O, этот игрок X или O выиграл.

5 требований правильно? Что может пойти не так?

- Бен Мерриман, 2022–11–16, поздно вечером, выбирая идеи для постов в блоге.

🛠️ Создайте репозиторий, настройте сайт

Мой первый шаг для проекта после уточнения некоторых требований — раскрутить базовую структуру сайта на GitHub и создать исходные файлы.

Мне нравится включать readme.md, чтобы передать мои требования и иметь документ рядом с тем, где я кодирую. Я всегда выбираю ‹none› для своего лицензирования, так как мой код не предназначен для коммерческого использования.

📖Readme.md

Хорошим шагом на этом этапе является перенос требований в файл readme.md на GitHub. Это можно сделать прямо в веб-редакторе.

А затем я создаю другие файлы и основные элементы в VS Code на Ubuntu после того, как репозиторий будет клонирован локально. Перейдите в папку, в которую будет помещен репозиторий, в терминале, git клонируйте репозиторий в локальную рабочую область и запустите VS Code, используя «код». команда.

git clone [email protected]:beingmerry/tic-tac-toe.git
cd tic-tac-toe
code .

🕸️Index.html

Оказавшись в VS Code, я начинаю создавать то, что будет первым коммитом в репозитории. Я знаю, что хочу использовать тег ‹table› вместе с ‹tr› и ‹td› в моем HTML5 для приложения крестики-нолики, поэтому я создаю базовый index.html

Но он не имеет формы во время выполнения. Я вижу, что это измерение в инспекторе имеет размер 2x2, без текста внутри него и без границ или формы. Поэтому я решил добавить файл styles.css и несколько значений-заполнителей, чтобы упростить работу с этими элементами.

Поэтому я помещаю числа от 1 до 9 в каждый элемент td и создаю стиль CSS, чтобы превратить их в квадраты.

И свяжите его в index.html

Чтобы получить начальную фиксацию сетки крестиков-ноликов

Обычно я не слишком беспокоюсь о первоначальном рендеринге, так как во время разработки многое изменится. Однако в этом случае, поскольку я создаю игру, я думаю, что внешний вид должен быть как минимум чем-то похож на сетку с некоторыми простыми добавлениями классов слева, посередине, справа на уровне строки элемента tr; и сверху, по центру, снизу на уровне ячейки td, я должен иметь возможность назначить доске некоторый CSS, чтобы она выглядела немного аккуратнее.

Я использовал назначения классов для каждой переменной HTML, максимально упрощая работу при проверке идей.

🤩Файл Styles.CSS

А изменение CSS для создания более толстых границ использует «противоположные» боковые границы для эффекта сетки. Сверху => граница-снизу, слева => граница-справа и т. д. Затем, чтобы таблица идеально выровнялась, я добавил к таблице border-spacing: 0; для придания окончательного вида.

td {
    height: 100px;
    width:  100px;
    font-size: 80px;
    text-align: center;
}
table{
    border-spacing: 0;
}
.top td {
    border-bottom: 5px solid black;
}
.bottom td {
    border-top: 5px solid black;
}
.left {
    border-right: 5px solid black;
}
.right {
    border-left: 5px solid black;
}

Теперь, когда плата появилась, я думаю, что пришло время попробовать построить простой тестовый файл, чтобы проверить, что все там. Этот дизайн будет довольно «заблокирован» для этого 🎯 требования иметь сетку 3x3, поэтому было бы здорово провести 🚩 тест, чтобы подтвердить, что я ничего не напортачил!

🚩 Тест № 1– сетка 3x3

Как и в большинстве случаев, я начинаю с создания нового файла (test.js) и получения ссылки на него в HTML с помощью тега script. Я знаю, что хочу проверить элемент таблицы, элемент строки и элемент столбца. Чтобы не слишком махать руками… но это было тяжело. Сначала я разработал простые проверки на равенство для элементов, но они терпели неудачу в крайних случаях, которые я пробовал. В итоге мне пришлось разобраться с операторами try…catch…finally, чтобы, если чего-то не существовало, тест проходил более чисто. Кроме того, мне также нужно было дисциплинировать себя, чтобы тестировать только одну вещь, для которой я назвал тест.

Короче говоря, тестирование должно быть проще остальной части этого маленького мини-проекта! - Бен Мерриман, 2022–11–19, суббота, начало дня.

Чтобы продемонстрировать тесты, ниже приведен скриншот тестов, которые я запускал в Firefox, и несколько примеров неудач.

И код файла test.js

// 🌐 Global element references for passing tests
let testTable = document.querySelectorAll("table");

// 🌐 Global variables for passing tests
let tableCheckPass = false;
let rowCheckPass = false;
let columnCheckPass = false;
// 🧪 3x3 grid tests
function gridTests() {
  checkTable();
  checkRows();
  checkColumns();
  function checkTable() {
    testTable = document.querySelectorAll("table");
    if (testTable.length !== 1) {
      console.log("🚫 Table test failed: EXACTLY 1 Table element needed");
    } else {
      console.log("✅ Table test passed! EXACTLY 1 Table element found");
      tableCheckPass = true;
    }
  }
  function checkRows() {
    let testPassed = false;
    try {
      testPassed = testTable[0].rows.length === 3;
    } catch (e) {
      testPassed = false;
    } finally {
      if (testPassed) {
        console.log("✅ Rows test passed! EXACTLY 3 tr (rows) elements found");
        rowCheckPass = true;
      } else {
        console.log("🚫 Rows test failed: EXACTLY 3 tr (rows) elements needed");
        rowCheckPass = false;
      }
    }
  }
  function checkColumns() {
    for (let i = 0; i < 3; i++) {
      let testPassed = false;
      try {
        testPassed = testTable[0].rows[i].cells.length === 3;
      } catch (e) {
        testPassed = false;
      } finally {
        if (testPassed) {
          console.log(
            `✅ Row ${
              i + 1
            } check passed! 3 td (cells or columns or data) in each row found`
          );
          columnCheckPass = true;
        } else {
          console.log(
            `🚫 Row ${
              i + 1
            } check failed: 3 td (cells or columns or data) elements needed`
          );
          columnCheckPass = false;
        }
      }
    }
  }
}

Теперь, когда я использую консоль отладчика F12, я получаю следующую обратную связь.

Теперь давайте начнем делать вещи!

🚩 Тест №2 — по клику?

Если на данном этапе я собираюсь придерживаться принципов разработки, основанной на тестировании, я думаю, что мне нужно разработать метод для проверки моих кликов, поскольку мне нужно иметь возможность нажимать на разные «ячейки» и видеть их. изменить значение. Однако я понятия не имею, как в моем коде выполнять ПРОСЛУШИВАНИЕ или ЗАПУСКнажатий, подобных событиям. Я могу вернуться к этому позже

🎯 Тесты кликов игнорируются! Давайте создадим несколько ответов на клик по ячейке

Эти ячейки должны помечаться крестиками и ноликами при кликах! Добавление прослушивателей событий к каждому элементу — это способ заставить систему двигаться вперед. Следующий код, похоже, выполняет свою работу.

Который был создан с помощью приведенного ниже кода, чтобы сделать простую интерактивную доску ячеек.

// 🌐 Global Variables
const cells = document.querySelectorAll("td");
// 1️⃣ Init Called on document loaded
const init = () => {
  initCellHandler();
};
// 💡 Event Handlers
const initCellHandler = () => {
  let i = 1;
  cells.forEach((cell) => {
    cell.id = i++;
    cell.addEventListener("click", (e) => {
      cell.textContent = (cell.textContent !== "X") ?
        "X" : "O" 
      console.log(`${cell.id} clicked`);
    });
  });
};
// ⌛ Call init function when DOM loaded
document.addEventListener("DOMContentLoaded", init);

🧪 (в сторону)

Почему бы не создать «проверку» функции тайм-аута, которая просматривает каждый элемент и делает «что-то» похожее на функцию clickEvent для каждой вещи в течение определенного периода времени. GIF станет показательной проверкой успешности мероприятия!

Поскольку я не уверен, как правильно протестировать функцию «щелчка» (пока!) я думаю, что хорошим тестом для разработки будет 🎯 «Квадрат должен быть пустым, чтобы поставить отметку» требование. Для этого я бы смотрел на состояние ячейки (пустой или заполненной) и должен ли быть разрешен щелчок и разрешен ли он.

Даже здесь я вижу, что щелчок нужно будет симулировать очень часто. Поэтому я собираюсь реорганизовать событие функции клика… и встроить систему поворота, очистив HTML и добавив несколько инструкций для игроков. Вначале это обязательно будет игра для 2 игроков.

// 🌐 Global Variables
const cells = document.querySelectorAll("td");
// 🤹 Tracking Game States
let xTurn = true;
// 1️⃣ Init Called on document loaded
const init = () => {
  initCellHandler();
};
// 💡 Init Event Handlers
const initCellHandler = () => {
  let i = 1;
  cells.forEach((cell) => {
    // debugger
    cell.id = i++;
    cell.addEventListener("click", () => cellHandler(cell))
  });
};
// 🎯 Declare Cell Handler
const cellHandler = (cell) => {
  const markToPlace = (xTurn ? "X" : "O")
  if (cell.textContent === "") {
    cell.textContent = markToPlace
    xTurn = !xTurn // Flip xTurn false for next players turn
  }
  console.log(`${cell.id} clicked`)
  if (testingActive) {return cell}
}
// ⌛ Call init function when DOM loaded
document.addEventListener("DOMContentLoaded", init);

Рефакторинг теперь встроен в ход... вместе с некоторой логикой, определяющей, какую метку ставить в зависимости от «состояния» ячейки и состояния игровой переменной xTurn.

В этот момент я начал просматривать MDN и нашел триггер события click(), который я могу использовать для тестирования имитации кликов!

Я использовал событие click() для создания некоторых тестов с визуальными подсказками, основанными на setTimeout() функциях. Мне нужно было увеличить таймеры, чтобы он визуально показывал действия кликов и демонстрировал совпадающие шаблоны. В противном случае его можно было бы «ускорить», установив параметр timeBetweenTests в 0. Больше для моего мозга было наблюдать щелчки, которые я построил в системе степпинга, таймаута.

// 🌐 Global element references for passing tests
let testTable = document.querySelectorAll("table");
let testCells = document.querySelectorAll("td")

// 🌐 Global variables for passing tests
let cellClickPass   = false;
let tableCheckPass  = false;
let rowCheckPass    = false;
let columnCheckPass = false;

// 🧪 click grid tests, building a "visual" click test runner...
function testCellClick(timeBetweenClicks = 500) {
  // 1️⃣ First test pattern for the board, linear click on each cell 1-9
  // Pattern match XOX,OXO,XOX
  const testPattern1 = [ // tests clicks, tests turns (alternating game states?)
    "X", "O", "X", 
    "O", "X", "O", 
    "X", "O", "X",
  ]
  testCells.forEach(cell => {
    setTimeout(() => {
      cell.click() // Click on each cell
      console.log(`clicking Cell ${cell.id}...`)
      if (cell.textContent === testPattern1[cell.id -1]) {
        console.log(`✅ Expected Cell ${cell.id} to match "${testPattern1[cell.id - 1]}" and the value is ${cell.textContent}!`)
        cellClickPass = true
      } else {
        console.log(`🚫 Expected Cell ${cell.id} to match "${testPattern1[cell.id - 1]}" and the value is ${cell.textContent}!`)
        cellClickPass = false
      }
    }, timeBetweenClicks * [cell.id])  // not necessary, but nice for the visual tests
  })
  return "🚩 Starting cell click tests..." 
}

И GIF-демонстрация визуального выполнения новых тестов, работающих

Последняя часть - условия победы! — Бен Мерриман, воскресенье

🥳 Условия победы

Условия победы должны быть еще одной функцией, которую можно запускать на различных конфигурациях платы. Для начала: давайте назовем каждый штат, который является победой с помощью системы ID из предыдущей.

Мы знаем, что есть 9 различных ячеек для мониторинга, это будет соответствовать БОЛЬШИНСТВУ 9 состояний для условий победы (мониторинг внутреннего состояния каждой ячейки по отношению к другим ее ячейкам). В более крупных и сложных перестановках условий победы это не было бы идеальным способом, но давайте просто перепробуем этот способ.

Для меня это выглядит как 3 «типа» победы: горизонтальный (строки), вертикальный (столбцы) и диагонали. Давайте построим проверку для каждого из них в математике массива. А пока: я просто встрою «тестирование» прямо в лог.

// 🥳 Victory Conditions
const victoryConditions = () => {
  console.log("horizontalWinCondition: ", horizontalWinCondition(xTurn ? "X" : "O")); // Only check the mark of the current player
  console.log("verticalWinCondition: ", verticalWinCondition(xTurn ? "X" : "O")); // Only check the mark of the current player
  console.log("diagonalWinCondition: ", diagonalWinCondition(xTurn ? "X" : "O")); // Only check the mark of the current player
  function horizontalWinCondition(markToCheck = "X") {
    let horizontalWinCheck = false;
    for (let i = 0; i < 3; i++) {
      if (
        cells[i * 3 + 0].textContent === markToCheck && // need to check 0, 3, and 6 (element in array)
        cells[i * 3 + 1].textContent === markToCheck && // need to check 1, 4, and 7
        cells[i * 3 + 2].textContent === markToCheck // need to check 2, 5, and 8
      ) {
        horizontalWinCheck = true;
        return horizontalWinCheck;
      }
    }
    return horizontalWinCheck;
  }
  function verticalWinCondition(markToCheck = "X") {
    let verticalWinCheck = false;
    for (let i = 0; i < 3; i++) {
      if (
        cells[i + 0].textContent === markToCheck && // need to check 0, 1, and 2 (element in array)
        cells[i + 3].textContent === markToCheck && // need to check 3, 4, and 5
        cells[i + 6].textContent === markToCheck // need to check 6, 7, and 8
      ) {
        verticalWinCheck = true;
        return verticalWinCheck;
      }
    }
    return verticalWinCheck;
  }
  function diagonalWinCondition(markToCheck = "X") {
    let diagonaWinCheck = false;
    for (let i = 0; i < 2; i++) {
      if (
        cells[i * 2].textContent === markToCheck &&  // need to check 0, 2
        cells[4].textContent === markToCheck &&      // need to check 4, 4
        cells[8 - i * 2].textContent === markToCheck // need to check 8, 6
      ) {
        diagonaWinCheck = true;
        return diagonaWinCheck;
      }
    }
    return diagonaWinCheck;
  }
};

И визуальная проверка функций условий победы

А по диагонали

Поэтому я добавляю дополнительную проверку этих условий победы, а затем прикрепляю кнопку, которая позволит сбросить игру. В настоящее время я не блокирую клики после завершения игры, и я не проверяю напрямую все требования в файле test.js, но я думаю, что это будет хорошей задачей для моей прямой трансляции во вторник!

const resetGame = () => {
  cells.forEach((cell) => {
    cell.innerHTML = "";
  });
  instructions.textContent = "Click on the squares to make your mark. Play with a friend! Three in a row, column, or diagonal wins!"
  xTurn = true
}
// 🎯 Declare Cell Handler
const cellHandler = (cell) => {
  const markToPlace = xTurn ? "X" : "O";
  if (cell.textContent === "") {
    cell.textContent = markToPlace;
    if (victoryConditions()) {
      // Run victoryConditions to check for win
      console.log(`${markToPlace} wins!`)
      instructions.textContent = `${markToPlace} wins! Reset game?`;
      const resetButton = document.createElement("button")
      resetButton.textContent = "Reset Game"
      instructions.appendChild(resetButton);
      resetButton.addEventListener("click", resetGame);
    }
    xTurn = !xTurn; // Flip xTurn for next players turn
  }
};

const victoryConditions = () => {
  const horizontalWin = horizontalWinCondition(xTurn ? "X" : "O"); // Only check the mark of the current player
  const verticalWin = verticalWinCondition(xTurn ? "X" : "O"); // Only check the mark of the current player
  const diagonalWin = diagonalWinCondition(xTurn ? "X" : "O"); // Only check the mark of the current player
  return (horizontalWin || verticalWin || diagonalWin)
...
}

И это игра! Кажется, это работает. Я постараюсь создать дополнительные тесты, когда узнаю больше о тестировании на этапе 2 во Flatiron.

🤯 Мысли

Разработка через тестирование — это сложно! Мне пришлось действительно мыслить нестандартно, чтобы создать свои тесты и придумать способы проверки моей собственной работы.

Мне нравилось придумывать для себя «задачи», которые я мог удовлетворить, и я многому научился при создании тестовых сценариев и математике массивов.

Ждем новых испытаний!

🔌 Вилки

Пожалуйста, приходите на встречу программистов во вторник, 22 ноября 2022 г., в 19:00 по восточному времени!

🧑‍🤝‍🧑 Вот ссылка на Meetup: https://www.meetup.com/flatiron-software-engineering-students/

🤹 Вот ссылка на репозиторий крестиков-ноликов: https://github.com/beingmerry/tic-tac-toe

🕹️ А вот и ссылка на настоящую живую игру в крестики-нолики: https://beingmerry.github.io/tic-tac-toe/