Bạn thích lối chơi của game Worldle ? Vậy thì đây là cách bạn có thể tạo phiên bản riêng của Wordle bằng JavaScript.
Wordle là một game phổ biến, gây “sốt” trên thế giới vào đầu năm 2022. Thiết kế lại game Wordle hay ít nhất là xây dựng một phiên bản đơn giản hơn của nó là điều mà lập trình viên mới với làm quen JavaScript nên thử.
Cơ chế hoạt động của game Wordle
Game Wordle có một từ 5 chữ cái bí mật. Người chơi có 6 lần thử và phải đoán 5 từ khác nhau để xem mức độ gần đúng của nó với từ bí mật.
Sau khi người chơi gửi dự đoán, Wordle dùng màu để cho người chơi biết mức độ gần đúng của chúng với từ bí mật. Nếu một chữ cái có màu vàng, có nghĩa chữ cái đó nằm trong từ bí mật, nhưng ở sai vị trí.
Màu xanh lá cho người dùng biết chữ cái có trong từ bí mật và ở đúng vị trí, còn màu xám cho người chơi biết chữ cái không nằm trong từ đó.
Thiết lập server lập trình
Dự án dùng công cụ build Vite qua giao diện dòng lệnh Command Line Interface (CLI) để scaffolding. Đảm bảo bạn đã cài Yarn trên máy tính bởi nó thường chạy nhanh hơn Node Package Manager (NPM). Mở terminal của bạn và chạy lệnh sau:
yarn create vite
Điều này sẽ tạo một dự án Vite mới. Framework này sẽ là Vanilla và biến thể phải được đặt thành JavaScript. Giờ chạy:
Yarn
Điều này sẽ cài tất cả phần phụ thuộc cần thiết để dự án hoạt động. Sau cài đặt này, chạy lệnh sau để khởi động server lập trình:
yarn dev
Thiết lập game và thiết kế bàn phím
Mở dự án trong trình chỉnh sửa code, xóa nội dung của file main.js và đảm bảo thư mục dự án trông như sau:
Giờ thay nội dung của file index.html bằng code soạn sẵn sau:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JS Wordle</title>
</head>
<body>
<div id="app">
<div>
<h1>Wordle Clone</h1>
<div id="controls">
<button id="restart-btn">Replay</button>
<button id="show-btn">Show Answer</button>
</div>
<div id="message">Please wait. The Game is loading...</div>
</div>
<div id="interface">
<div id="board"></div>
<div class="keyboard"></div>
</div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
Đối với CSS, tới GitHub Repository của dự án này và sao chép nội dung của file style.css vào file style.css.
Giờ trong terminal, cài đặt gói Toastify NPM bằng cách chạy lệnh sau:
yarn add toastify -S
Toastify là một gói JavaScript thông dụng, cho phép bạn hiện cảnh báo tới người dùng. Tiếp theo, trong file main.js, nhập file style.css và tiện ích toastify.
import "./style.css"
import Toastify from 'toastify-js'
Xác định các biến sau để thực hiện tương tác với những thành phần DOM dễ hơn:
let board = document.querySelector("#board");
let message = document.querySelector("#message");
let keys = "QWERTYUIOPASDFGHJKLZXCVBNM".split("");
let restartBtn = document.querySelector("#restart-btn");
let showBtn = document.querySelector("#show-btn");
showBtn.setAttribute("disabled", "true");
keys.push("Backspace");
let keyboard = document.querySelector(".keyboard");
Thiết lập bảng game
Vì Wordle là game mà người dùng phải đoán được từ 5 chữ cái trong 6 lần, xác định một biến tên boardContent chứa một mảng trong 6 mảng. Sau đó, xác định biến currentRow và currentBox để nó dễ dàng đi qua boardContent.
let boardContent = [
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
];
let currentRow = 0;
let currentBox = 0;
let secretWord;
Để kết xuất bảng này với 5 box trong mỗi 6 hàng bằng phần tử HTML, dùng các vòng lặp lồng nhau để lặp lại và tạo các phần tử. Cuối cùng, thêm chúng vào bảng.
for (let i = 0; i <= 5; i++) {
let row = document.createElement('div')
for (let y = 0; y <= 4; y++) {
let box = document.createElement('span');
row.appendChild(box);
row.className = `row-${i + 1}`
}
board.appendChild(row);
}
Thêm bàn phím và nghe nhập bàn phím
Để tạo bàn phím, lặp qua các phím bằng forEach, tạo thành phần nút bấm cho từng mục. Đặt nội dung của nút bấm sang Backspace nếu mục nhập là *
, nếu không, đặt nó sang giá trị nhập.
Gắn phím class vào nút bấm, đặt thuộc tính data-key sang giá trị mục nhập viết hoa. Tiếp theo, thêm một trình nghe sự kiện click vào nút bấm gọi hàm insertKey với giá trị mục nhập là chữ viết hoa.
keys.forEach(entry => {
let key = document.createElement("button");
if (entry === "*") {
key.innerText = "Backspace";
} else {
key.innerText = entry;
}
key.className = "key";
key.setAttribute("data-key", entry.toUpperCase());
key.addEventListener("click", () => {
insertKey(entry.toUpperCase())
setTimeout(() => {
document.querySelector(`button[data-key=${entry.toUpperCase()}]`).blur();
}, 250)
})
keyboard.append(key);
})
Lấy một từ mới trong API
Khi người dùng lần đầu tải trò chơi, game này sẽ tìm nạp một từ 5 ký tự mới trong API Random word. Từ này sau đó được lưu trong biến secretWord.
function getNewWord() {
async function fetchWord() {
try {
const response = await fetch("https://random-word-api.herokuapp.com/word?length=5");
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error("Something went wrong!")
}
} catch (error) {
message.innerText = `Something went wrong. \n${error}\nCheck your internet connection.`;
}
}
fetchWord().then(data => {
secretWord = data[0].toUpperCase();
main();
})
}
Trong khối code trên, hàm main chạy nếu từ ngẫu nhiên được tìm nạp thành công. Xác định một hàm main ngay dưới hàm getNewWord:
function main(){
}
Để tạo kiểu cho từng box trên bảng, bạn cần một danh sách chứa tất cả box trong mỗi hàng. Khai báo một biến, row tải toàn bộ hàng trong DOM. Ngoài ra, đặt message hiện kiểu sang none:
rows.forEach(row => [...row.children].forEach(child => boxes.push(child)))
boxes.forEach((box) => {
box.classList.add("empty");
})
message.style.display = "none";
Tiếp theo, thêm trình nghe sự kiện keyup vào đối tượng cửa sổ và kiểm tra xem liệu khóa được phát hành có hợp lệ hay không. Nếu hợp lệ, tập trung vào nút phản hồi, mô phỏng một click và làm mờ nó sau khoảng thời gian trì hoãn 250 giây.
window.addEventListener('keyup', (e) => {
if (isValidCharacter(e.key)) {
document.querySelector(`button[data-key=${e.key.toUpperCase()}]`).focus();
document.querySelector(`button[data-key=${e.key.toUpperCase()}]`).click();
setTimeout(() => {
document.querySelector(`button[data-key=${e.key.toUpperCase()}]`).blur();
}, 250)
}
})
Trong trình nghe sự kiện keyup, thiết lập các trình nghe sự kiện cho 2 nút bấm: showBtn và restartBtn. Khi người chơi click showBtn, hiện thông báo ngắn về giá trị của biến secretWord.
Click restartBtn tải lại trang. Ngoài ra, đảm bảo bạn bao gồm hàm isValidCharacter để kiểm tra xem liệu một khóa nào đó có phải ký tự hợp lệ.
showBtn.addEventListener('click', () => {
Toastify({
text: `Alright fine! the answer is ${secretWord}`,
duration: 2500,
className: "alert",
}).showToast();
})
restartBtn.addEventListener('click', () => {
location.reload();
})
function isValidCharacter(val) {
return (val.match(/^[a-zA-Z]+$/) && (val.length === 1 || val === "Backspace"))
}
Ngoài hàm main, tạo hàm renderBox và cung cấp 3 tham số: row (số hàng), box (chỉ mục box trong hàng), và data (nội dung văn bản để update).
function renderBox(row, box, data) {
[...document.querySelector(`.row-${row}`).children][box].innerText = data;
}
Xử lý nhập bàn phím bằng một hàm
Để xử lý tác vụ nhập bàn phím và update bảng, tạo hàm insertKye bằng tham số key. Hàm này sẽ hoạt động theo tham số đã chuyển.
function insertKey(key) {
if (key === "Backspace".toUpperCase() && currentRow < boardContent.length) {
boardContent[currentRow][currentBox] = 0;
if (currentBox !== 0) {
currentBox--;
renderBox(currentRow + 1, currentBox, "");
}
} else {
if (currentRow < boardContent.length) {
boardContent[currentRow][currentBox] = key;
renderBox(currentRow + 1, currentBox, key);
currentBox++;
}
if (currentRow < boardContent.length && boardContent[currentRow][currentBox] !== 0) {
evaluate(currentRow, key);
currentBox = 0;
currentRow++;
}
}
}
Đánh giá dự đoán của người chơi
Tạo hàm evaluate chấp nhận một tham số hàng. Hàm này chịu trách nhiệm đánh giá dự đoán của người chơi.
function evaluate(row){
}
Mỗi game đều có nút bấm Show Answer chỉ hiện sau khi người dùng thực hiện 4 lần đoán. Vì thế, trong hàm này, triển khai tính năng thực hiện điều đó:
if (currentRow === 4) {
showBtn.removeAttribute('disabled')
}
Sau đó, xác định biến đoán và một biến trả lời, kiểm tra xem các chữ cái có nằm ở vị trí chính xác.
let guess = boardContent[row].join('').toUpperCase();
let answer = secretWord.split("");
Thuật toán tô màu ô sẽ xuất hiện ở đây:
let colors = guess
.split("")
.map((letter, idx) => letter == answer[idx] ? (answer[idx] = false) : letter)
.map((letter, idx) =>
letter
? (idx = answer.indexOf(letter)) < 0
? "grey"
: (answer[idx] = "yellow")
: "green"
);
Khối code trên triển khai so sánh theo từng nhân tố giữa mảng guess và answer. Dựa trên kết quả so sánh, code này update mảng colors.
Tiếp theo, xác định hàm setColors có thể lấy mảng colors làm tham số và màu ô phù hợp:
function setColor(colors) {
colors.forEach((color, index) => {
document.querySelector(`button[data-key=${guess[index].toUpperCase()}]`).style.backgroundColor = color;
document.querySelector(`button[data-key=${guess[index].toUpperCase()}]`).style.color= "black";
[...document.querySelector(`.row-${row + 1}`).children][index].style.backgroundColor = color;
})
}
Game này giờ đã hoàn thành. Toàn bộ việc bạn phải làm hiện tại là gọi hàm getNewWord:
getNewWord();
Chúc mừng, bạn đã tạo xong game giống Wordle rồi đấy!