การใช้งาน JavaScript Tic Tac Toe
ฉันพยายามพัฒนาเว็บดังนั้นฉันจึงใช้ Tic Tac Toe อย่างรวดเร็ว มันรู้สึกยุ่งสำหรับฉัน แต่ฉันไม่แน่ใจว่าอะไรคือแนวทางปฏิบัติที่ดีและอะไรไม่จริง การปฏิบัติที่ไม่ดีในรหัสของฉันหรือสิ่งที่ฉันสามารถปรับปรุงได้?
https://github.com/jason-shepherd/tictactoe
//Get the board, record, and select html elements
const gridSpaces = document.querySelectorAll('[data-spaces]');
const recordText = document.querySelector('[data-record]');
const difficultySelect = document.querySelector('[data-select]')
const gridWidth = Math.sqrt(gridSpaces.length);
let opponent = "O"
let player = "X"
let difficulty;
let record = {
X: 0,
O: 0,
ties: 0
}
let moveCount = 0;
let inPlay = true;
function init() {
updateDifficulty();
//Init the board spaces with an event listener
for(let i = 0; i < gridSpaces.length; i++) {
gridSpaces[i].addEventListener('click', () => {
if(!inPlay) {
reset();
return;
}
if(getSpaceValue(i) != '') return;
//Player's move
setSpaceValue(i, player);
gridSpaces[i].style.cursor = "default";
win = getWin(Math.floor(i % gridWidth), Math.floor(i / gridWidth), player);
displayWin(win, player);
moveCount++;
//AI move
if(inPlay) {
if(difficulty != 0)
makeAiMove();
else
player = player == "O" ? "X" : "O";
}
});
}
}
function checkRowSpace(index, x, y, board) {
return getGridSpace(index, y, board);
}
function checkColSpace(index, x, y, board) {
return getGridSpace(x, index, board);
}
function checkDiagonal(index, x, y, board) {
if(x == y)
return getGridSpace(index, index, board);
else
return null;
}
function checkAntiDiagonal(index, x, y, board) {
if(x + y == gridWidth - 1)
return getGridSpace(index, gridWidth - 1 - index, board);
else
return null
}
const checkFunctions = [checkRowSpace, checkColSpace, checkDiagonal, checkAntiDiagonal];
function getWin(x, y, currentPlayer, board) {
let winSequence = [];
for(let i = 0; i < 4; i++) {
for(let j = 0; j < gridWidth; j++) {
let currentSpace = checkFunctions[i](j, x, y, board);
if(board == undefined) {
if(getSpaceValue(currentSpace) != currentPlayer) {
winSequence = [];
break;
}
} else if(currentSpace != currentPlayer) {
winSequence = [];
break;
}
winSequence.push(currentSpace);
if(j == gridWidth - 1) {
return winSequence;
}
}
}
if(moveCount == Math.pow(gridWidth, 2) - 1) {
return gridSpaces;
}
return winSequence;
}
function displayWin(win, currentPlayer) {
if(win.length !== 0) {
let condition = "win";
if(win.length === gridSpaces.length) {
record.ties++;
condition = "draw";
} else {
record[currentPlayer]++;
}
recordText.textContent = `X ${record.X}-${record.ties}-${record.O} O`;
win.forEach(space => {
space.firstChild.classList.add(condition);
});
gridSpaces.forEach(space => {
space.style.cursor = "pointer";
});
inPlay = false;
return;
}
}
function makeAiMove() {
let bestVal = -11;
let bestMove;
let newBoard = [];
gridSpaces.forEach(space => {
newBoard.push(getSpaceValue(space));
});
let possibleMoves = getBoardChildren(newBoard, "O");
if(difficulty != 9)
possibleMoves.sort((a, b) => {return 0.5 - Math.random()})
possibleMoves.forEach(child => {
let value = minimax(child, difficulty, false);
if(value > bestVal) {
bestVal = value;
bestMove = child;
}
});
for(let i = 0; i < bestMove.length; i++) {
if(getSpaceValue(i) != bestMove[i]) {
setSpaceValue(i, 'O');
let win = getWin(Math.floor(i % gridWidth), Math.floor(i / gridWidth), opponent);
displayWin(win, opponent);
}
}
moveCount++;
}
function minimax(board, depth, maximizingPlayer) {
let score = scoreBoard(board, depth);
if(depth == 0 || isTerminating(board) || score != 0)
return score;
if(maximizingPlayer) {
let value = -10;
getBoardChildren(board, opponent).forEach(child => {
value = Math.max(value, minimax(child, depth - 1, false));
});
return value;
} else {
let value = 10;
getBoardChildren(board, player).forEach(child => {
value = Math.min(value, minimax(child, depth - 1, true));
});
return value;
}
}
function getBoardChildren(board, currentPlayer) {
let children = [];
for(let i = 0; i < board.length; i++) {
if(board[i] == '') {
board[i] = currentPlayer;
children.push([...board]);
board[i] = '';
}
}
return children;
}
function isTerminating(board) {
for(let i = 0; i < board.length; i++) {
if(board[i] == '')
return false;
}
return true;
}
function scoreBoard(board, depth) {
let currentPlayer = "O";
for(let i = 0; i < 2; i++) {
for(let j = 0; j < 3; j++) {
if(getWin(j, j, currentPlayer, board).length == 3) {
if(currentPlayer == "O")
return 10 - (difficulty - depth);
else
return -10 + (difficulty - depth);
}
}
currentPlayer = "X";
}
return 0;
}
function updateDifficulty() {
if(difficultySelect.value != "friend") {
switch(difficultySelect.value) {
case "easy":
difficulty = 1;
break;
case "medium":
difficulty = 4;
break;
case "unbeatable":
difficulty = 9;
break;
}
if(player == "O") {
player = "X";
makeAiMove();
}
} else {
difficulty = 0;
}
}
function reset() {
player = "X";
moveCount = 0;
inPlay = true;
for(let i = 0; i < gridSpaces.length; i++) {
gridSpaces[i].firstChild.classList.remove("win");
gridSpaces[i].firstChild.classList.remove("draw");
setSpaceValue(i, "");
}
}
function getSpaceValue(x, y, board) {
if(x == null)
return;
else if(typeof x === 'object')
return x.firstChild.textContent;
else if(y == undefined)
return gridSpaces[x].firstChild.textContent;
else
return gridSpaces[y * gridWidth + x].firstChild.textContent;
}
function setSpaceValue(index, value) {
gridSpaces[index].firstChild.textContent = value;
}
function getGridSpace(x, y, board) {
if(board != undefined)
return board[y * gridWidth + x];
else
return gridSpaces[y * gridWidth + x];
}
init();
body {
background-color: #353A47;
}
.tictactoe-container {
background-color: #353A47;
width: 77vh;
height: auto;
position: absolute;
left: 50%;
top: 20%;
transform: translate(-50%, -20%);
}
.grid-container {
background-color: #2B303B;
display: inline-grid;
width: auto;
height: auto;
grid-gap: 1vh;
grid-template-columns: repeat(3, 25vh);
grid-template-rows: repeat(3, 25vh);
}
.grid-item {
background-color: #353A47;
color: #F0F7EE;
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
font-family: 'Varela Round', sans-serif;
font-weight: bold;
font-size: 25vh;
}
.record {
color: #F0F7EE;
font-weight: bold;
text-align: center;
font-family: 'Varela Round', sans-serif;
font-size: 10vh;
white-space: nowrap;
margin: 2vh auto;
}
.ai-select {
color: #F0F7EE;
background-color: #353A47;
font-size: 3vh;
width: 40%;
height: 15%;
margin: 1vh 30%;
}
.win {
color: #4BB3FD;
animation: shake 0.5s;
animation-iteration-count: 3;
}
.draw {
color: #FF312E;
animation: shake 0.5s;
animation-iteration-count: 6;
}
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
10% { transform: translate(-1px, -2px) rotate(-1deg); }
20% { transform: translate(-3px, 0px) rotate(1deg); }
30% { transform: translate(3px, 2px) rotate(0deg); }
40% { transform: translate(1px, -1px) rotate(1deg); }
50% { transform: translate(-1px, 2px) rotate(-1deg); }
60% { transform: translate(-3px, 1px) rotate(0deg); }
70% { transform: translate(3px, 1px) rotate(-1deg); }
80% { transform: translate(-1px, -1px) rotate(1deg); }
90% { transform: translate(1px, 2px) rotate(0deg); }
100% { transform: translate(1px, -2px) rotate(-1deg); }
}
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
@media screen and (orientation:portrait) {
.tictactoe-container {
width: 77vw;
}
.grid-container {
grid-gap: 1vw;
grid-template-columns: repeat(3, 25vw);
grid-template-rows: repeat(3, 25vw);
}
.grid-item {
font-size: 25vw;
}
.record {
font-size: 10vw;
}
.ai-select {
font-size: 3vw;
}
}
<!DOCTYPE html>
<html>
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
<head>
<title>Tic Tac Toe</title>
<link rel="stylesheet" type = "text/css" href = "style.css">
<link href="https://fonts.googleapis.com/css2?family=Varela+Round&display=swap" rel="stylesheet">
</head>
<body>
<div class="tictactoe-container">
<p data-record class="record noselect">X 0-0-0 O</p>
<div class="grid-container noselect">
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
<div data-spaces class="grid-item"><p></p></div>
</div>
<select data-select class="ai-select" onchange="updateDifficulty()">
<option value="easy">Easy</option>
<option value="medium" selected="selected">Medium</option>
<option value="unbeatable">Unbeatable</option>
<option value="friend">Play with a friend</option>
</select>
</div>
<script src="tictactoe.js" defer></script>
</body>
</html>
คำตอบ
ต่อไปนี้เป็นคำแนะนำและเวอร์ชันที่พยายามอธิบายประเด็นส่วนใหญ่เหล่านี้
1. แยกความกังวล
ควรมีการแยกบางส่วนระหว่างรหัสที่จัดการกับ (i) การแสดงผลและ (ii) ตรรกะของเกมและการเป็นตัวแทนของกระดาน คนมักจะใช้รูปแบบการออกแบบเช่นรุ่น View-Controller
วิธีการแยกแบบคลาสสิกคือการสร้างคลาสโมดูลหรือวัตถุสำหรับการแสดงผลและอีกวิธีหนึ่งสำหรับตรรกะของเกม วัตถุทั้งสองเรียกเฉพาะเมธอดของกันและกันในกรณีที่ จำกัด และกำหนดไว้อย่างชัดเจน หากคุณไม่ต้องการใช้วัตถุคุณสามารถใช้ชื่อฟังก์ชันและความคิดเห็นเพื่อแบ่งเขตที่ชัดเจนขึ้นระหว่างการจัดการการแสดงผล / เหตุการณ์และตรรกะของเกม
แต่คุณอยู่ในจุดต่างๆโดยใช้ html ทั้งเพื่อแสดงข้อมูลและการแสดงผล สำหรับโปรแกรมง่ายๆวิธีนี้อาจใช้งานได้และทำให้โค้ดง่ายขึ้น แต่ก็ไม่ดีสำหรับโปรแกรมของคุณเนื่องจากคุณมีฟังก์ชันที่คุณต้องการจัดการทั้งกระดาน 'จินตภาพ' และกระดานจริงที่แสดงบนหน้าจอ สำหรับเกมที่ซับซ้อนมากขึ้นจะมีความซับซ้อนมากขึ้นในการทำงานกับการแสดง HTML ของกระดาน หากคุณอนุญาตให้ทุกฟังก์ชันเปลี่ยนหรืออ่าน html โดยตรงจะยากที่จะติดตามว่าการเปลี่ยนแปลงนั้นมาจากที่ใด นอกจากนี้ยังผูกโปรแกรมของคุณไว้ใกล้ชิดเกินไปกับการแสดงเฉพาะ ตัวอย่างเช่นสมมติว่าคุณต้องการใช้canvasหรือใช้svgกราฟิกแทนข้อความและ css สิ่งนี้จะต้องมีการเขียนทุกอย่างใหม่ด้วยการตั้งค่าปัจจุบัน
getWinมีความสับสนเป็นพิเศษเนื่องจากดูเหมือนว่าจะทำงานแตกต่างกันอย่างสิ้นเชิงขึ้นอยู่กับว่าคุณส่งผ่านพารามิเตอร์ของบอร์ดหรือไม่ คุณอาจพยายามเพิ่มประสิทธิภาพโดยตรวจสอบเฉพาะแถวที่มี x หรือ y เฉพาะ แต่การเขียนฟังก์ชันทั่วไปที่จะตรวจสอบบอร์ดใด ๆ นั้นง่ายกว่ามาก ฟังก์ชันจะส่งคืนลำดับซึ่งเป็นลำดับการชนะสำหรับการชนะหรือทั้งกริดหากเป็นการเสมอกัน
2. ฟังก์ชั่นเล็ก ๆ ที่ทำสิ่งเดียว
ฟังก์ชั่นของคุณดูยุ่งเหยิงส่วนหนึ่งเป็นเพราะพวกเขาใช้ส่วนผสมของการแสดงบนหน้าจอของบอร์ดและการแทนอาร์เรย์แยกต่างหากเพื่อค้นหาว่ามีช่องว่างเต็มหรือไม่ แต่ยังเป็นเพราะพวกเขาพยายามทำงานในกรณีต่างๆมากเกินไป เช่นgetWin(ตามที่กล่าวไว้ข้างต้น) และgetSpaceValueซึ่งยอมรับค่าxที่เป็นโมฆะองค์ประกอบ html หรือตัวเลขและyยังสามารถระบุไม่ได้หรือตัวเลข ฟังก์ชันส่วนใหญ่ควรยอมรับอินพุตประเภทเดียว (ข้อยกเว้นบางครั้งอนุญาตให้ละเว้นอาร์กิวเมนต์ด้วยค่าเริ่มต้น) และส่งคืนค่าส่งคืนที่คาดเดาได้
3. ใช้Arrayวิธีการที่ทันสมัยมากขึ้น
filter, find, mapสามารถช่วยเหลือทั้งหมดเขียนรหัสสั้นและอ่านได้มากขึ้น ( แต่ยังสามารถกลายเป็นอ่านไม่ได้ถ้าใช้ในการแพ็คมากเกินไปในหนึ่งซับ)
4. ใช้ฟังก์ชันยูทิลิตี้
คุณอาจไม่ต้องการใช้ไลบรารีเช่น lodash แต่ฟังก์ชันยูทิลิตี้มาตรฐานบางอย่างจะทำให้โค้ดของคุณกระชับและอ่านได้ง่ายขึ้นและการใช้ไลบรารียอดนิยมหมายความว่าโปรแกรมเมอร์คนอื่น ๆ จะสามารถอ่านโค้ดของคุณและเข้าใจได้อย่างรวดเร็วว่ากำลังทำอะไรอยู่ ตัวอย่างเช่น maxBy ของlodashจะมีประโยชน์ในการเลือกการย้าย AI ที่ดีที่สุด คุณสามารถเขียนของคุณเองได้อย่างง่ายดายหากคุณไม่ต้องการใช้ห้องสมุด
5. รหัสสำหรับพิจารณาการชนะ
รหัสของคุณใช้งานได้และฉลาดในทางหนึ่ง แต่ซับซ้อนมาก! บางครั้งคุณส่งต่อ coords ของการเคลื่อนไหวปัจจุบัน แต่เมื่อถึงจุดหนึ่งคุณผ่านj, jเป็นพิกัดซึ่งยังคงใช้งานได้อย่างลึกลับ วิธีที่ง่ายและโปร่งใสมากขึ้นในการตรวจสอบสำหรับผู้ชนะคือการเก็บอาร์เรย์ของลำดับที่ชนะไปได้ ( [[0, 1, 2], [3, 4, 5], ...]) playerแล้วหาลำดับแรกที่สี่เหลี่ยมของบอร์ดจะกำหนดให้
const range = [...Array(gridWidth).keys()];
const lines = [
...range.map(i => range.map(j => i * gridWidth + j)), // rows
...range.map(i => range.map(j => i + j * gridWidth)), // columns
range.map(j => j * (gridWidth + 1)), // diagonal
range.map(j => (gridWidth - 1) * (j + 1)) // antidiagonal
];
function win(player, board) {
return lines.find(line => line.every(i => board[i] === player));
}
หากคุณสามารถระบุพิกัดของการเคลื่อนไหวปัจจุบันคุณสามารถเขียนเวอร์ชันที่มีประสิทธิภาพมากกว่าซึ่งจะตรวจสอบเฉพาะ (สูงสุด) 4 บรรทัดที่ผ่านพิกัดเหล่านั้น:
const range = [...Array(gridWidth).keys()];
function win(player, x, y, board) {
let lines = [
range.map(i => y * gridWidth + i), // current row
range.map(i => i * gridWidth + x) // current column
];
if (x === y) { // diagonal
lines.push(range.map(i => i * gridWidth + i));
}
if (gridWidth - x === y) { // antidiagonal
lines.push(range.map(i => (gridWidth - 1) * (i + 1)));
}
return lines.find(line => line.every(i => board[i] === player));
}
ในทางปฏิบัติสำหรับบอร์ด 3x3 การเพิ่มประสิทธิภาพนี้ไม่น่าจะช่วยเพิ่มความเร็วได้ (และอาจแย่ลงเนื่องจากlinesต้องสร้างอาร์เรย์ใหม่ทุกครั้ง)
6. ตัวจัดการเหตุการณ์ html
อย่าใส่ตัวจัดการเหตุการณ์ใน html เมื่อตรรกะส่วนใหญ่ของคุณอยู่ในจาวาสคริปต์ มันยุ่งและสับสนเกินไป ใส่การจัดการเหตุการณ์ทั้งหมดในจาวาสคริปต์
7. ข้อคิดเห็น / เอกสารประกอบ
ในขณะที่คุณแบ่งปันรหัสและขอความคิดเห็นคุณควรบันทึกรหัสของคุณ อย่างง่ายที่สุดนี่อาจเป็นความคิดเห็นหนึ่งหรือสองบรรทัดเพื่ออธิบายว่าแต่ละฟังก์ชันทำหน้าที่อะไรและโครงสร้างโดยรวม แต่มีวิธีการจัดทำเอกสารมาตรฐานที่เรียกว่าjsdoc ( ดูคำนำนี้ด้วย ) นี่เป็นสิ่งสำคัญอย่างยิ่งหากคุณยังคงต้องมีฟังก์ชั่นที่ทำงานในรูปแบบที่น่าประหลาดใจด้วยเหตุผลบางประการ
เพื่อให้ได้แนวทางปฏิบัติที่ดีที่สุดจะต้องมีปริมาณและขอความคิดเห็นมากมาย ไม่ว่าอย่างไรก็ตามการพยายามตั้งโปรแกรมให้ไกลเกินความเข้าใจของคุณนั้นไม่ดีดังนั้นฉันจะไม่อธิบายรายละเอียด โดยทั่วไปสิ่งแรกที่เกิดขึ้นกับฉันคือถ้าคุณจริงจังกับการเขียนโปรแกรมจริง ๆ จงเป็นประโยชน์ต่ออนาคตของคุณด้วย การเขียนโปรแกรมเริ่มต้นจากกระบวนการคิดและความคิดของคุณจะเปลี่ยนไปเมื่อทักษะของคุณเติบโตขึ้น ทิ้งบันทึกแบบเสรี (ความคิดเห็นเกี่ยวกับรหัส) และหกเดือนหรือหกปีนับจากนี้เมื่อคุณ (หรืออาจจะเป็นฉัน!) กลับมาดูรหัสของคุณอีกครั้งคุณสามารถสร้างกระบวนการที่สร้างรหัสขึ้นใหม่
ความรุ่งโรจน์ในการตั้งชื่อฟังก์ชันและตัวแปรของคุณเพื่อให้เจตนาของพวกเขาชัดเจน รูปแบบการเยื้องและการเข้ารหัสของคุณสอดคล้องกันและเป็นสิ่งสำคัญ ตอนนี้ทำความคุ้นเคยกับการเว้นบรรทัดหรือสองบรรทัดสำหรับแต่ละบล็อกโค้ด
ฉันเขียนโปรแกรมสำหรับเว็บมาสองสามทศวรรษแล้วและคุณจะไม่เชื่อว่าความเครียดจากการพยายามเปลี่ยนแปลงเล็กน้อยกับตรรกะ 600 บรรทัดที่คุณไม่เข้าใจเมื่อ บริษัท ที่คุณทำงานเพื่อเรียกเก็บเงินเป็นล้าน ปีและมีพนักงานหลายสิบคน หรือตกใจเมื่อรู้ว่าเป็นการเขียนโปรแกรมของคุณเอง!