<div id="app">
<div class="board" :class="[isXTurn ? 'x' : 'circle']">
<div
:ref="setCellRefs"
class="cell"
v-for="cell in cells"
:key="cell.key"
@click.once="handleClick"
></div>
</div>
<div class="winning-message" :class="{show: isShowRestart}">
<div>{{ winningMessageText }}</div>
<button type="button" @click="startGame">再來一局</button>
</div>
</div>
*,
*::after,
*::before {
box-sizing: border-box;
}
:root {
--cell-size: 100px;
--mark-size: calc(var(--cell-size) * 0.9);
}
body {
margin: 0;
font-family: 'Microsoft JhengHei', 'PingFang SC', 'Helvetica', sans-serif;
}
/* 棋盤布局 starts */
.board {
width: 100%;
height: 100vh;
display: grid;
justify-content: center;
align-content: center;
justify-items: center;
align-items: center;
grid-template-columns: repeat(3, auto);
}
.cell {
width: var(--cell-size);
height: var(--cell-size);
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
position: relative;
cursor: pointer;
}
/* 去除邊框 */
.cell:first-child,
.cell:nth-child(2),
.cell:nth-child(3) {
border-top: none;
}
.cell:nth-child(3n + 1) {
border-left: none;
}
.cell:nth-child(3n + 3) {
border-right: none;
}
.cell:nth-child(7),
.cell:nth-child(8),
.cell:last-child {
border-bottom: none;
}
/* 棋盤布局 ends */
/* 棋子樣式 starts */
.cell.x::before,
.cell.x::after,
.cell.circle::before {
background: #121212;
}
.board.x .cell:not(.x):not(.circle):hover::before,
.board.x .cell:not(.x):not(.circle):hover::after,
.board.circle .cell:not(.x):not(.circle):hover::before {
background-color: lightgrey;
}
.cell.x,
.cell.circle {
cursor: not-allowed;
}
.cell.x::before,
.cell.x::after,
.board.x .cell:not(.x):not(.circle):hover::before,
.board.x .cell:not(.x):not(.circle):hover::after {
content: '';
position: absolute;
width: calc(var(--mark-size) * 0.15);
height: var(--mark-size);
}
.cell.x::before,
.board.x .cell:not(.x):not(.circle):hover::before {
transform: rotate(45deg);
}
.cell.x::after,
.board.x .cell:not(.x):not(.circle):hover::after {
transform: rotate(-45deg);
}
.cell.circle::before,
.cell.circle::after,
.board.circle .cell:not(.x):not(.circle):hover::before,
.board.circle .cell:not(.x):not(.circle):hover::after {
position: absolute;
content: '';
border-radius: 50%;
}
.cell.circle::before,
.board.circle .cell:not(.x):not(.circle):hover::before {
width: var(--mark-size);
height: var(--mark-size);
}
.cell.circle::after,
.board.circle .cell:not(.x):not(.circle):hover::after {
width: calc(var(--mark-size) * 0.7);
height: calc(var(--mark-size) * 0.7);
background: #fff;
}
/* 棋子樣式 ends */
/* winning message starts */
.winning-message {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
justify-content: center;
align-items: center;
flex-direction: column;
color: white;
font-size: 5rem;
}
.winning-message button {
font-size: 3rem;
background-color: white;
border: 1px solid #000;
padding: 0.25em 0.5em;
cursor: pointer;
transition: 0.2s;
}
.winning-message button:hover {
background-color: #121212;
color: white;
border-color: white;
}
.winning-message.show {
display: flex;
}
/* winning message ends */
import { createApp, ref, onBeforeUpdate } from 'https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.31/vue.esm-browser.min.js'
const X_CLASS = 'x'
const CIRCLE_CLASS = 'circle'
const WINNING_COMBINATIONS = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
const app = createApp({
setup() {
const isXTurn = ref(true)
const isShowRestart = ref(false)
const winningMessageText = ref('')
const { cells, reRenderCells } = initGame()
const startGame = () => {
isShowRestart.value = false
isXTurn.value = true
reRenderCells()
}
const handleClick = (e) => {
const cell = e.target
const currentClass = isXTurn.value ? X_CLASS : CIRCLE_CLASS
placeMark(cell, currentClass)
if (checkWin(currentClass)) {
endGame(false)
} else if (isDraw()) {
endGame(true)
} else {
swapTurns()
}
}
// 落子
const placeMark = (cell, currentClass) => {
cell.classList.add(currentClass)
}
// 換邊
const swapTurns = () => {
isXTurn.value = !isXTurn.value
}
// 檢查是否勝利
const { setCellRefs, checkWin, isDraw } = useCheckWin()
const endGame = (draw) => {
if (draw) {
winningMessageText.value = '和局!'
} else {
winningMessageText.value = `${isXTurn.value ? 'X ' : 'O '}贏了!`
}
isShowRestart.value = true
}
return {
isXTurn,
isShowRestart,
winningMessageText,
cells,
setCellRefs,
handleClick,
startGame
}
}
})
const initGame = () => {
const cells = ref([
{ key: '1' },
{ key: '2' },
{ key: '3' },
{ key: '4' },
{ key: '5' },
{ key: '6' },
{ key: '7' },
{ key: '8' },
{ key: '9' }
])
const reRenderCells = () => {
cells.value.forEach((cell, i) => {
cells.value[i].key = cells.value[i].key + '1'
})
}
return {
cells,
reRenderCells
}
}
const useCheckWin = () => {
let cellRefs = []
const setCellRefs = (el) => {
if (el) cellRefs.push(el)
}
onBeforeUpdate(() => cellRefs = [])
const checkWin = (currentClass) => {
return WINNING_COMBINATIONS.some(combination => {
return combination.every(index => {
return cellRefs[index].classList.contains(currentClass)
})
})
}
const isDraw = () => {
return cellRefs.every(cell => {
return cell.classList.contains(X_CLASS) || cell.classList.contains(CIRCLE_CLASS)
})
}
return {
setCellRefs,
checkWin,
isDraw
}
}
app.mount('#app')
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.