Mục tiêu

Game demo

  1. Truy cập repl.it và đăng ký tài khoản.
  2. Click repl-plus ở góc phải trên màn hình và chọn HTML/JS/CSS. Đặt tên project là Tìm Kho Báu hoặc bất cứ tên nào bạn muốn.

repl-1

  1. Thêm <h1>Tìm Kho Báu</h1> bên trong tag <body>.
  2. Click "Run" ở phía trên màn hình.

Bạn sẽ thấy kết quả giống thế này:

hello-world

Để đơn giản hơn, chúng ta sẽ chỉ tạo một mảng 3 x 3 ô (3 hàng, mỗi hàng 3 ô). Với mỗi ô, ta sẽ tạo một div tương ứng:

<div class="square"></div>

Trong css, ta sẽ đặt cho class square này chiều cao và độ rộng nhất định, đồng thời thêm viền để dễ phân biệt:

.square {
	border: 1px solid black;
	height: 100px;
	width: 100px;
}

Khi bấm run, đây sẽ là kết quả nhận được:

Copy/Paste nhiều lần div này, ta sẽ có:

<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
...

Tuy nhiên, vì div là một block nên mỗi div sẽ ở một hàng riêng. Ta có thể bắt chúng cùng một hàng bằng cách cho thuộc tính của mỗi div này là inline-block hoặc dùng flexbox. Để dùng flexbox, ta phải đặt các div này trong một div bọc khác và cho nó thuộc tính display: flex:

<div id="square-wrapper">
	<div class="square"></div>
	<div class="square"></div>
	<div class="square"></div>
	<div class="square"></div>
</div>
#square-wrapper {
	display: flex;
}

Đến bước này, tất cả các ô sẽ vào cùng một hàng:

Chúng ta có thể dùng flex-wrap để cho các ô xuống hàng khi thứ tự của nó lớn hơn 8 (vì ta muốn mỗi hàng có 8 ô). Ta thêm kèm width: 300px để giới hạn lại độ rộng của div bọc này.

.square-wrapper {
	display: flex;
	flex-wrap: wrap;
	width: 300px;
}

Để tạo một mảng 8 x 4 ô, ta có thể làm thủ công như trên với 32 div nằm trong 1 div bọc hoặc viết javascript để render một lần. Ở đây ta chỉ tạo một mảng 3 x 3 để có thể thực hành dễ dàng hơn.

Đây sẽ là kết quả cuối cùng sau khi render đủ các ô:

Link download hình ảnh: link

Trước hết, hãy thêm các hình ảnh trên vào project của bạn (có thể kéo - thả vào).

Bạn có thể dùng tấm hình trên dưới dạng background-image cho div bọc (square-wrapper):

.square-wrapper {
	background-image: url("map.jpg");
	background-size: cover;
	background-position: center;
}

Hoặc dùng tag img và chỉnh thuộc tính position thành absolute để có thể dễ dàng chỉnh sửa kích thước hình hơn.

Kết quả:

Giả sử kho báu nằm ở ô thứ ba. Ta có thể tính toán giá trị x và y và đặt vị trí của hình ảnh chiếc rương tương ứng. Tuy nhiên, để đơn giản hơn, ta có đặt trực tiếp kho báu này vào trong div thứ ba.

<div class="square">
	<img id="chest" src="chest.png" />
</div>

Kho báu đã nằm ở ô thứ hai nhưng nó không ở giữa ô. Để cho nó vào giữa, ta có thể sử dụng flexbox. Tuy nhiên, nếu ô này có nhiều phần tử con khác (ví dụ số đếm, chữ, v.v.) thì sẽ rất khó căn giữa bằng flexbox. Vì vậy, chúng ta sẽ dùng position: absolute để căn giữa.

Ba dạng position chính thường gặp:

Để căn giữa chiếc rương này, ta có thể cho ô đó có dạng relative và chiếc rương dạng absolute. Sau đó chỉnh top là 50%, và left là 50% (nghĩa là dịch chuyển xuống 50% từ trên, và dịch qua 50% từ bên trái).

Thêm position: relative vào .square, đồng thời:

#chest {
	position: absolute;
	top: 50%;
	left: 50%;
}

Tại sao nó không nằm giữa?

Nếu nhìn kỹ, ta sẽ thấy tấm hình rương bắt đầu từ góc trái trên, và góc này chính là vị trí chính giữa của ô! Muốn tấm hình ở giữa, ta phải cho tâm điểm của hình trùng với vị trí giữa ô này chứ không phải góc trái của hình!

Để làm được điều này, ta cần dịch chuyển tấm hình ngược lại qua trái 50% độ rộng của tấm hình, và lên trên 50% độ cao của hình. transform: translate sẽ giúp ta làm điều này.

#treasure {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
}

Giá trị topleft (hay bottomright):

Giá trị translate(-50%,-50%) hoặc translateX(-50%)translateY(-50%):

Hãy thử đặt chiếc rương này ở các ô khác nhau!

Cơ bản:

1. Rương kho báu và các ô sẽ được code cố định trong HTML
2. Gắn onclick vào tất cả các ô, và ô có rương sẽ chạy onclick một hàm khác
3. Xuất chữ khi click sai/đúng
4. Ẩn rương và chỉ hiện thị rương khi click đúng

🚀 Nâng cao:

1. Dùng JS để tạo 32 ô
2. Lấy một số ngẫu nhiên và đặt rương vào ô có số thứ tự đó
3. Gắn onclick vào tất cả các ô, và ô có rương sẽ chạy onclick một hàm khác
4. Xuất chữ khi click sai/đúng
5. Ẩn rương và chỉ hiện thị rương khi click đúng
6. Giới hạn số lần click mỗi game
7. Hiển thị vị trí tương đối của rương nếu click sai

Đầu tiên, ta gán cho div bọc square-wrapper một id. Ta sẽ dùng vòng lặp for để tạo 32 ô trong div có id này. Do vậy ta không cần tạo 32 div thủ công nữa:

<div id="game" class="square-wrapper"></div>

Ta dùng getElementById() để lấy phần tử div bọc này. Trong file script.js:

let gameDiv = document.getElementById("game");
for (let i = 0; i < 32; i++) {
	gameDiv.innerHTML += `<div class="square"></div>`;
}

Lấy một số ngẫu nhiên với Math.random()

Trong Javascript, để lấy được một số ngẫu nhiên, ta có thể dùng phương thức Math.random(). Phương thức này sẽ trả một số ngẫu nhiên trong khoảng từ 0 đến cận 1 (bao gồm 0 nhưng không bao gồm 1).

Để chuyển số này thành một số tự nhiên trong khoảng ta cần, ta có thể nhân nó lên và làm tròn xuống với Math.floor().

Ví dụ, nếu muốn một số ngẫu nhiên từ 0 đến 9 (10 số), ta có thể nhân Math.random() với 10 và làm tròn xuống. Do Math.random() sẽ không bao giờ đạt đến 1, số lớn nhất mà ta có sẽ nhỏ hơn 10, do đó ta cần làm tròn xuống.

console.log(Math.floor(Math.random()));
//  kết quả sẽ là 0 hoặc 1

console.log(Math.floor(Math.random() * 10));
// kết quả sẽ từ 0 đến 9

Để có số ngẫu nhiên từ 1 đến 10, ta có thể làm như trên và cộng kết quả với 1.

Ở game này, ta có 32 ô tất cả. Để ô hiển thị ở một ô bất kỳ, trước hết ta cần tạo một số ngẫu nhiên trong khoảng từ 0 đến 31 (hoặc 1 đến 32).

Trong file script.js, tạo một biến để lưu số thứ tự của ô chứa rương:

let randomIndex = Math.floor(Math.random() * 32);
console.log(randomIndex);

Nếu chạy nhiều lần, ta sẽ thấy kết quả luôn nằm trong khoảng từ 0 đến 31.

Sửa lại hàm tạo 32 ô để kèm rương vào một ô ngẫu nhiên. Nếu randomIndex bằng với giá trị của i trong vòng lặp thì ta sẽ kèm hình rương vào:

let gameDiv = document.getElementById("game");
for (let i = 0; i < 32; i++) {
	if (randomIndex === i) {
		gameDiv.innerHTML += `<div class="square"><img id="treasure" src="chest.png" /></div>`;
	} else {
		gameDiv.innerHTML += `<div class="square"></div>`;
	}
}

Nếu bạn chạy lại app nhiều lần, bạn sẽ thấy chiếc rương xuất hiện ở các vị trí khác nhau!

Khi click một ô bất kỳ, ta nên biết ô được click là ô nào, và ô đó có kho báu ở trong không. Để làm được điều này ta cần biết:

Để sử dụng onclick, ta có thể thêm trực tiếp vào HTML một thuộc tính onclick và giá trị của nó sẽ là một hàm ta đã định sẵn. Ví dụ:

<div onclick="clickSquare()">Click Me!</div>

Mỗi khi click vào div trên, hàm clickSquare() (nếu có) sẽ được chạy. Tuy nhiên, ta phải truyền số thứ tự của ô vào thì hàm mới biết ta đang click ô nào.

function clickSquare(index) {
	console.log("Bạn vừa click vào ô " + index);
}

Nếu bạn thử chạy thì khi click vào ô nào, trong console sẽ xuất ra một thông báo tương ứng với ô đó!

Cơ bản:

Ta đã để rương kho báu ở ô thứ 3, vì vậy số thứ tự index sẽ là 2. Ta có thể đặt điều kiện, để khi index là 2 thì ta sẽ chúc mừng người chơi. Còn lại thì chỉ xuất số thứ tự của ô:

function clickSquare(index) {
	if (index === 2) {
		console.log("Bạn đã tìm thấy kho báu!");
	} else {
		console.log("Bạn đã click vào ô số " + (index + 1));
	}
}

🚀 Nâng cao:

Tương tự, ta có thể viết một hàm thông báo và đính kèm vào tất cả 32 ô đã tạo.

function clickSquare(index) {
	console.log("Bạn vừa click vào ô " + index);
}

for (let i = 0; i < 32; i++) {
	if (randomIndex === i) {
		gameDiv.innerHTML += `<div class="square" onclick="clickSquare(${i})"><img id="chest" src="chest.png" /></div>`;
	} else {
		gameDiv.innerHTML += `<div class="square" onclick="clickSquare(${i})"></div>`;
	}
}

Ta có thể thêm điều kiện vào hàm clickSquare() để xuất các thông báo khác nhau, ví dụ như khi click đúng ô có chứa rương kho báu:

function clickSquare(index) {
	if (index === randomIndex) {
		console.log("bạn tìm được kho báu rồi!");
	} else {
		console.log("Bạn vừa click vào ô " + index);
	}
}

Ta có thể dùng class để ẩn hoặc hiện bất cứ phần tử nào. Trước hết, tạo một class mới tên là hidden. Ta có thể ẩn các phần tử có class này bằng cách thêm vào css thuộc tính opacity. 0 sẽ là ẩn hoàn toàn và 1 là hiển thị hoàn toàn.

.hidden {
	opacity: 0;
}

Để ẩn rương, ta có thể thêm class hidden vào, và để hiển thị nó, ta có thể bỏ class này đi. Mặc định chiếc rương sẽ bị ẩn đi nên ta sẽ thêm sẵn class này vào hình chiếc rương:

<img id="chest" class="hidden" src="chest.png" />

Khi click vào rương, ta sẽ bỏ class này khỏi phần tử img trên. Do ta đã gán id chest vào phần tử này, ta có thể truy đến nó và chỉnh sửa phần tử trong Javascript bằng id đó thông qua phương thức getElementById() và bỏ class qua .classList.remove(). Để thêm class mới ta có thể dùng .classList.add().

function clickSquare(index) {
	if (index === 2) {
		let chest = document.getElementById("chest");
		chest.classList.remove("hidden");
		console.log("Bạn đã tìm thấy kho báu!");
	} else {
		console.log("Bạn đã click vào ô số " + (index + 1));
	}
}

Để giới hạn số lần click, ta cần một biến để đếm số lần đã click, và khi đã đủ số lần quy định (hoặc khi đếm ngược về 0), ta sẽ không chạy clickSquare() nữa.

let clicks = 3;

function clickSquare(index) {
	if (clicks > 0) {
		if (index === randomIndex) {
			console.log("bạn tìm được kho báu rồi!");
		} else {
			console.log("Bạn vừa click vào ô " + index);
		}

		count -= 1;
	}
}

Nếu dùng mảng 2 chiều, ta có thể dễ dàng tính toán vị trí tương đối của rương bằng cách so sánh tọa độ x, y của click với tạo độ của rương. Ở đây ta đang dùng mảng 1 chiều nên việc so sánh sẽ khó hơn một chút.

Trước hết, hãy xem thử 1 ví dụ:

Để có được số hàng (y) và số cột (x) từ số index của ô, ta có thể rút ra được phép toán chung từ các ví dụ trên:

Số hàng = làm tròn lên (số index / 3) (vì 3 là số cột cao nhất)
Số cột = số index - 3 * (số hàng  - 1)

Ta có thể dùng phép toán này tính toán tọa độ của click và của kho báu, nếu số cột (x) nhỏ hơn thì nó ở bên trái, nếu số hàng (y) nhỏ hơn thì nó ở bên trên, và ngược lại.