大家好,上一篇介紹了網站的基本架構以及 socket.io 的加入房間與開始遊戲的事件,這篇主要會介紹遊戲主要核心事件,那就讓我們開始吧!
上圖解說:遊戲一開始從牌庫抽取五張牌,每張牌只能用一次,為了讓算式等於 24 分別使用了數字牌 8 與 3 搭配乘法來完成。
// client
socket.emit(SocketEvent.SelectCard, {
roomId: roomInfo?.roomId,
number,
symbol,
});
點選牌的時候由前端發送事件到後端,參數可帶數字或符號。
// server
socket.on(SocketEvent.SelectCard, ({ roomId, number, symbol }) => {
const { room, msg } = selectCard(roomId, number, symbol);
if (room) {
io.sockets.to(roomId).emit(SocketEvent.RoomUpdate, {
room,
});
} else {
socket.emit(SocketEvent.ErrorMessage, msg);
}
});
後端接收到事件後呼叫 selectCard 方法。
function selectCard(
roomId: string,
number: NumberCard,
symbol: Symbol,
): Response {
try {
const roomIndex = _getCurrentRoomIndex(roomId);
if (roomIndex === -1) return { msg: '房間不存在' };
const selectedCards = _rooms[roomIndex].selectedCards;
if (selectedCards.length === 0 && symbol && symbol !== Symbol.LeftBracket) {
return {
msg: '第一個只能用左括號或數字',
};
}
if (number) {
const currentSelect = selectedCards[selectedCards.length - 1];
// 如果前一個是數字則不能選
if (currentSelect?.number && currentSelect?.number.id !== number.id) {
return {
msg: '數字牌不能連續使用',
};
}
const isExistIndex = selectedCards.findIndex(
s => s.number?.id === number.id,
);
if (isExistIndex !== -1) {
_rooms[roomIndex].selectedCards.splice(isExistIndex, 1);
} else {
_rooms[roomIndex].selectedCards.push({ number });
}
}
if (symbol) {
const lastCard = selectedCards[selectedCards.length - 1];
if (lastCard?.symbol === Symbol.Minus && symbol === Symbol.Minus) {
return {
msg: '減號不能連續用',
};
}
if (lastCard?.symbol === Symbol.Plus && symbol === Symbol.Plus) {
return {
msg: '加號不能連續用',
};
}
if (lastCard?.symbol === Symbol.Times && symbol === Symbol.Times) {
return {
msg: '乘號不能連續用',
};
}
if (lastCard?.symbol === Symbol.Divide && symbol === Symbol.Divide) {
return {
msg: '除號不能連續用',
};
}
if (
lastCard?.symbol === Symbol.LeftBracket &&
[Symbol.Plus, Symbol.Minus].includes(symbol)
) {
return {
msg: '左括號後面無法使用減號或加號',
};
}
if (symbol === Symbol.LeftBracket && lastCard?.number) {
_rooms[roomIndex].selectedCards.push({ symbol: Symbol.Times });
}
_rooms[roomIndex].selectedCards.push({ symbol });
}
return {
room: _rooms[roomIndex],
};
} catch (e) {
return { msg: '發生錯誤,請稍後再試 (select card)' };
}
}
選牌的需要判斷的邏輯主要分為兩個判斷,第一個是數字,數字比較簡單只要判斷前一個牌不是數字就可以,因為數字牌不能連續出,第 26 行那段是判斷如果已經選取了這張牌再次點選的時候要取消選取。
第二個是符號,符號需要判斷是否連續使用,當然上面的程式碼沒有寫到所有符號的交集,不過套件會幫我們判斷掉,差在判斷時機且會影響使用者體驗,是要在選牌的時候判斷還是出牌的時候判斷,因為跳 Toast(提示框) 的時間點不同,如果玩家在點選的時候跳提示訊息體驗會比較好。
第 63 行的判斷左括號前不能使用減號或加號是為了防止洗分數,同樣是 8 X 3 我可以使用 -(8) X -(3) 答案都是 24 這樣分數就會變高。
// client
socket.emit(SocketEvent.PlayCard, {
roomId: roomInfo?.roomId,
});
// server
socket.on(SocketEvent.PlayCard, ({ roomId }) => {
const { room, msg, isCorrect } = playCard(roomId, playerId);
if (msg) {
socket.emit(SocketEvent.ErrorMessage, msg);
return;
}
if (isCorrect) {
// 答對
io.sockets.to(roomId).emit(SocketEvent.RoomUpdate, {
room,
extra: {
event: SocketEvent.PlayCardResponse,
data: true,
},
});
} else {
// 答錯
io.sockets.to(roomId).emit(SocketEvent.RoomUpdate, {
room,
extra: {
event: SocketEvent.PlayCardResponse,
data: false,
},
});
}
});
後端接收到事件後呼叫 playCard 方法。
function playCard(
roomId: string,
playerId: string,
): Response & { isCorrect?: boolean } {
try {
const roomIndex = _getCurrentRoomIndex(roomId);
if (roomIndex === -1) return { msg: '房間不存在' };
const playerIndex = _getCurrentPlayerIndex(
_rooms[roomIndex].players,
playerId,
);
if (playerIndex === -1) return { msg: '玩家不存在' };
const selectedCards = _rooms[roomIndex].selectedCards;
const answer = calculateAnswer(selectedCards);
if (answer === 24) {
// 使用的數字牌
const numberCards = selectedCards
.filter(c => c.number)
.map(c => c.number?.id);
// 移除數字牌
const newCards = _rooms[roomIndex].players[playerIndex].handCard.filter(
c => !numberCards.includes(c.id),
);
_rooms[roomIndex].players[playerIndex].handCard = newCards;
return {
isCorrect: true,
room: _rooms[roomIndex],
};
}
return {
isCorrect: false,
room: _rooms[roomIndex],
};
} catch (e) {
return { msg: '算式有誤 (play card)' };
}
}
第 16 行呼叫 calculateAnswer 方法計算算式結果。
function calculateAnswer(selectedCards: SelectedCard[]) {
const expression = selectedCards.map(s => {
if (s.number) {
return s.number.value;
}
if (s.symbol) {
return s.symbol;
}
});
try {
const answer = evaluate(expression.join(''));
return answer;
} catch (error) {
throw Error('算式有誤');
}
}
這個方法把使用到的牌組合成一個字串,例如:'(1+2)*8' 然後使用 mathjs 套件幫我們判斷算是合不合理及結果並回傳答案。
在上一段程式碼中第 25 行,我們需要把用到的數字牌從手牌中移除。
當算式成立後,我們要計算分數,像下面圖片顯示,等動畫跑完後才去打 UpdateScore 的事件,詳細程式碼可以看 main-play-area.tsx。
// client
socket.emit(SocketEvent.UpdateScore, {
roomId: roomInfo?.roomId,
});
socket.on(SocketEvent.UpdateScore, ({ roomId }) => {
const { room, msg, winner } = updateScore(roomId, playerId);
if (room) {
io.sockets.to(roomId).emit(SocketEvent.RoomUpdate, {
room,
extra: {
event: SocketEvent.UpdateScore,
},
});
if (winner) {
io.sockets.to(roomId).emit(SocketEvent.GameOver, {
name: winner.name,
score: winner.score,
});
}
} else {
socket.emit(SocketEvent.ErrorMessage, msg);
}
});
後端接收到是事件後呼叫 updateScore 方法。
function updateScore(
roomId: string,
playerId: string,
): Response & { winner?: Player } {
try {
const roomIndex = _getCurrentRoomIndex(roomId);
if (roomIndex === -1) return { msg: '房間不存在' };
const playerIndex = _getCurrentPlayerIndex(
_rooms[roomIndex].players,
playerId,
);
if (playerIndex === -1) return { msg: '玩家不存在' };
const selectedCards = _rooms[roomIndex].selectedCards;
// 使用的數字牌
const numberCards = selectedCards
.filter(c => c.number)
.map(c => c.number?.id);
// 計算分數
let score = 0;
// +, - 各加一分
const plusAndMinusCount =
selectedCards.filter(
c => c.symbol && [Symbol.Plus, Symbol.Minus].includes(c.symbol),
).length || 0;
score += plusAndMinusCount;
// * 兩分, / 三分
const timesCount =
selectedCards.filter(c => c.symbol === Symbol.Times).length || 0;
const divideCount =
selectedCards.filter(c => c.symbol === Symbol.Divide).length || 0;
score += timesCount * 2;
score += divideCount * 3;
// 如果有兩個 * 額外加一分
if (timesCount >= 2) {
score += 1;
}
// 如果有兩個 / 額外加一分
if (divideCount >= 2) {
score += 1;
}
// 使用到的數字牌數量額外加分
const bonusNumberCardsScore = calculateNumbersScore(numberCards.length);
if (bonusNumberCardsScore) {
score += bonusNumberCardsScore;
}
// 寫入分數
_rooms[roomIndex].players[playerIndex].score += score;
_rooms[roomIndex].selectedCards = [];
const { room, winner, msg } = drawCard(
roomId,
playerId,
numberCards.length,
);
if (msg) {
return { msg };
}
return { room, winner };
} catch (e) {
return { msg: '發生錯誤,請稍後再試 (update score)' };
}
}
計算分數的邏輯很簡單,統計使用到的符號牌數量增加分數以及總共使用的牌數增加分數,依照遊戲規則來算分數。
這篇文章介紹了選牌、出牌、計算分數的事件方法,是整個遊戲的核心,文章比較著重於事件邏輯的部份,關於前端 React UI 邏輯的部份比較沒有介紹,有興趣的朋友們可以看原始碼追一下 code,雖然不是寫得很完美,但他可以動 XD謝謝大家看到這邊,我們下篇見。
要來預告下一個系列的遊戲了🥁🥁🥁,記憶翻牌?