สร้างเกมเป่ายิ้งฉุบ.. ด้วย LINE Chatbot พร้อมเทคนิค ตัวอย่างการใช้ Clipboard Action!

Thepnatee Phojan
LINE Developers Thailand
11 min readMar 11, 2024

สวัสดีครับชาว LINE Developers วันนี้มีเรื่องสนุกๆมาอีกเช่นเคย นั่นก็คือ การสร้างเกมเป่ายิ้ง…ฉุบผ่าน LINE Chatbot ฉบับหาทำนั่นเอง (ยกมือเป่ายิ้งฉุบธรรมดามันเหนื่อยมือเกินไป 555+) ก็เป็นบทความให้ Idea กับเพื่อนๆ เอาไปต่อยอด เผื่อนำไปประยุกต์และทำกิจกรรมอื่นๆได้ด้วย LINE Platform นั่นเอง

เอาล่ะ..บทความนี้ก็ได้แรงบันดาลใจจากที่ทำงาน คือเขาเป่ายิ้งฉุบพร้อมกัน 20–30 คนแล้วให้ยกมือค้างไว้ และตัดคนออกเรื่อยๆ ก็คิดว่าน่าจะเอามาใช้บน LINE ได้ แถมมีรายชื่อชัดเจนทำให้.. สายตาหลอกกันมั๊ยยยด๊าย (เสียงคูมอิ้งค์ วรันธร) 🎼

สิ่งที่เราจะใช้กันในบทความนี้ครับ

  1. LINE Messaging API Channel

2. Firebase โดยบทความนี้เราจะใช้ 3 บริการนั่นก็คือ

  • Cloud Fuctions for firebase
  • Cloud Firestore
  • Firebase Emulator Suite

3. ผมจะมีการ Upload Url รูปต่างๆ ผมจะใช้ บริการ Upload ฟรีจาก EX10

4. Source Code ของบทความนี้ครับ แนะนำให้เพื่อนๆ ต้อง Clone หรือ Download Source มาให้เรียบร้อยครับเพื่อเปรียบเทียบ

ในบทความนี้เราจะมีการใช้ firestore , functions, emulator ตอนสร้าง Firebase Project ให้เลือกดังนี้

  • Functions: Configure a Cloud Functions directory and its files
    หาก เพื่อนๆ จะต้อง Deploy แนะนำให้ปรับ package เป็น Blaze Plan ด้วยครับ
  • Emulators: Set up local emulators for Firebase products
    กรณีที่ไม่ได้ Deploy สามารถใช้ทดสอบบนเครื่องได้
  • Firestore: Configure security rules and indexes files for Firestore

ขั้นตอนที่ 1 วางแผน ตามโจทย์

โจทย์ของเรามีอยู่ว่า …

  1. ต้องเปิดสิทธิ์ให้สามารถดึง LINE Official Account ให้เข้ากลุ่มได้ เนื่องจาก เราอนุญาตให้ใช้งานเฉพาะ LINE Group Only

2. เมื่อลาก LINE Chatbot เข้ากลุ่ม ต้องเริ่มการทักทาย และสามารถเริ่มเล่นเกมด้วยการ ใช้ Quick Reply และ Flex Message เป็นหลัก
หมายเหตุ : ปัจจุบัน Quick Reply สามารถใช้ได้ทั้ง Mobile และ PC

3. เราต้องกำหนดความสามารถของน้องเมื่อเข้ากลุ่มดังนี้

  • สามารถแสดง “กติกา” เงื่อนไข ก่อนเล่นเกมได้ ร่วมถึง เราสามารถกำหนด กิจกรรมต่างๆ ให้การเล่นเกมสนุกมากขึ้นเหมือน ชนะ แพ้ หรือ เสมอ
  • สามารถเริ่มเกม ด้วยปุ่ม Quick Reply ได้ ด้วยเงื่อนไขการเริ่มเกม ผู้ที่กดจะเป็นผู้ที่สร้างเกม ซึ่งเป็นใครก็ได้ ไม่จำเป็นต้องเป็นเจ้าของห้อง ด้วยภายใต้เงื่อนไขดังนี้
  • เมื่อมีการ สร้าง เกม แล้ว จะไม่สามารถ สร้างเกมทับกันได้ จนกว่าจะจบเกมก่อนหน้าจะจบ หรือ มีการ เตะ Chatbot ออกจากกลุ่ม ก็จะเป็นการเคลียร์เกม
  • เมื่อสร้างเกม สามารถให้ผู้เข้าร่วมเล่นเกม และ ผู้สร้างเกมเลือก ค้อน กรรไกร หรือ กระดาษ ด้วย Flex Message โดยให้เล่นตามเงื่อนไขหรือกติกาของเกมนั้นๆ เช่น ต้องเลือกภายใน กี่วินาที หรือ หากชนะ จะทำอะไร แพ้ จะโดนอะไร ก่อนกดปุ่มจบเกม
  • เมื่อจบเกม จะแสดงรายชื่อ ผู้ชนะ ผู้แพ้ และผู้เสมอ
  • ผู้สร้างเกมสามารถจบเกม ก่อนได้หาก ผู้เล่นไม่ได้เลือก โดยการกดปุ่ม จบเกม

เรียบร้อย เมื่อเรากำหนด ทิศทางความสามารถ ของน้องคราวๆ แล้วมาเริ่มกัน ลุย!!

ขั้นตอนที่ 2 วางแผน กำหนดใช้งาน LINE Event Type(Code File : webhook.js)

เมื่อแกะจากโจทย์เราจะได้ Event Type และ Source Type ทั้งหมดดังนี้

  1. Event Source Type Group
if (event.source.type !== "group") {
return res.end();
}

โดยเราจะกำหนดให้ใช้แค่ LINE Group เท่านั้น

2. Event Type : join

// Invite LINE Offcial Account : เป่ายิ้งฉุบ
// [IMPORTANT] Enable Toggle : Feature Allow account to join groups and multi-person chats
// https://developers.line.biz/en/reference/messaging-api/#join-event
if (event.type === "join") {
await line.reply(event.replyToken, [messages.textMessageQuickReply("สวัสดี ทุกคน มาเริ่มเกมเป่ายิ้งฉุบกันนน \n ถ้าทุกคนพร้อมแล้วไปดู กติกา กันก่อน ")])
return res.end();
}

เราต้องการให้ LINE Chatbot ของเรามีการ Interactive กับผู้ใช้งาน เราเลยทำให้ เมื่อมีการดึง Chatbot เข้ามาแล้ว ก็ให้ทำการทักทายทุกคน

3. Event Type : memberJoined

// Invite User Player 
// https://developers.line.biz/en/reference/messaging-api/#member-joined-event
if (event.type === "memberJoined") {
for (let member of event.joined.members) {
if (member.type === "user") {
// Check Game Running Status in Group
// For Message New Joiner Group
const count = await firebase.getCountGameGroupStatus(groupId, false)
// If Games in the group haven't started yet
if (count === 0) {
await line.reply(event.replyToken, [messages.textMessageQuickReply("ยินดีต้อนรับสมาชิกใหม่ กด 'เข้าร่วม' เพื่อทักทายทุกคนและ เข้าร่วมเกม ")])
} else {
// Game has begun.
await line.reply(event.replyToken, [messages.textMessage("ยินดีต้อนรับสมาชิกใหม่ ขณะนี้เกมกำลังดำเนินการอยู่ กรุณารอรอบถัดไป")])
}
}
}
return res.end();
}

เมื่อเรามีการดึงเพื่อนๆ เข้ามา เราก็อยากให้ Chatbot ของเราทำการ Interactive เช่นกัน ก็เลยให้ Chatbot ของเรามีการ โต้ตอบกลับไปอย่างมีเงื่อนไข ดังนี้

const count = await firebase.getCountGameGroupStatus(groupId, false)

ผมมีการเรียกใช้ ฟังก์ชัน getCountGameGroupStatus เป็นการใช้ query ใน firestore ถึงสถานะของเกม ว่ามีเกมกำลังดำเนินการอยู่หรือไม่

  • ถ้าไม่มี Chatbot ของเราจะกล่าว “ยินดีต้อนรับสมาชิกใหม่”
  • ถ้ามีเกมกำลังดำเนินการอยู่ ให้ Chatbot ตอบถึง สถานะที่กำลังดำเนินการ

โดยในส่วนของ firestore จะขอเล่าในหัวข้อ step การสร้าง เล่น และ จบเกม

4. Event Type : leave

// https://developers.line.biz/en/reference/messaging-api/#leave-event
if (event.type === "leave") {
// Delete Game Document when LINE OA Leave Group
await firebase.deleteGameGroup(groupId)
return res.end();
}

event นี้จะเกิดขึ้นเมื่อใครสักคนเตะ Chatbot ออก เราเลยให้ทำการเคลียร์ Database ของเราเมื่อ Chatbot ออกจากกลุ่มเพื่อไม่ให้กลายเป็นขยะข้อมูลใน Database แต่ในเชิงแนวคิด หากเพื่อนๆอยากจะทำ report ก็อาจจะไม่ต้องทำวิธีนี้ก็ได้

4. Event Type : Message && Message Type : Text

// Message Type Text
// https://developers.line.biz/en/reference/messaging-api/#wh-text
if (event.type === "message" && event.message.type === "text") {
// do somthing
return res.end();

}

ถือได้ว่าเป็นพระรองของงานนี้เลยก็ว่าได้ เพราะ Event นี้เราจะใช้ text message เป็นหลัก รายละเอียดอยู่ในหัวข้อถัดไป

5. Event Type : Postback

// https://developers.line.biz/en/reference/messaging-api/#postback-event
if (event.type === "postback") {
// do somthing
return res.end();

}

มาถึงพระเอกของเราคือ postback ซึ่งเป็นส่วนในการ รับส่งค่า key value ต่างๆที่เราจำเป็นต้องใช้เช่น เป่ายิ้งฉุบ และ จบเกม นั่นเอง

เอาละเราได้ Event ของ LINE ที่คาดว่าจะใช้ละ คราวนี้มาเตรียมตัวสร้าง..เล่น..จบเกมกัน

ขั้นตอนที่ 3 เตรียมตัวเล่นเกม -> สร้างเกม -> เล่นเกม -> จบเกม

เตรียมตัวเล่นเกม

ในขั้นตอนนี้เราจะมาอธิบายส่วนที่ง่ายที่สุดกันไปก่อนนั่นคือการ Config Message Event Type Text ต่างๆ

File : src/webhook.js

if (event.type === "message" && event.message.type === "text") {
const lineProfile = await line.getProfile(userId)

let textMessage = event.message.text
if (textMessage === "กติกา") {
await line.reply(event.replyToken, [messages.ruleMessage()])
} else if (textMessage === "เข้าร่วม") {
await line.reply(event.replyToken, [messages.textMessageQuickReply(`คุณ ${lineProfile.data.displayName}ได้เข้าร่วมเกมเรียบร้อยแล้ว `)])
} else if (textMessage === "เริ่มเกม") {
await line.reply(event.replyToken, [messages.textMessageQuickReplyGame("วรยุทธใต้หล้าตัดสินแพ้ชนะวัดที่ความเร็ว \n เมื่อผู้สร้างพร้อมเกม กด 'สร้างเกม' \n\n หากสร้างเกมแล้วจะไม่สามารถสร้างทับได้")])
} else if (textMessage === "ล้างเกมของคุณ") {
// Function Clear Game all Status
await firebase.deleteGameUserId(groupId, userId)
await line.reply(event.replyToken, [messages.textMessageQuickReplyGame(`ระบบได้ลบเกมของ ${lineProfile.data.displayName} เรียบร้อย`)])
} else if (textMessage === "สร้างเกม") {

// If Game Running
// Check Duplicate for Create Game
const count = await firebase.getCountGameGroupStatus(groupId, false)
if (count === 0) {
const resultId = await firebase.createGame(userId, groupId)
await line.reply(event.replyToken, [messages.selectMessage(userId, groupId, resultId)])
} else {
await line.reply(event.replyToken, [messages.textMessage("เกมได้ถูกสร้างแล้วไม่สามารถสร้างทับกันได้")])
}
}
return res.end();

}

จาก Code ชุดบนนี้ผมมีการเรียกใช้ไฟล์ชื่อว่า /messages/messages.js เพื่อให้ง่ายต่อการปั้น JSON ของ Quick Reply และ Flex Message

  1. กติกา

เรามีการกำหนดกฏเกณฑ์การเล่นของเกม ซึ่งในที่นี้ผมกำหนดไว้กลางๆ

2. เข้าร่วม

อันนี้ผมแค่อยากให้ทุกคน interactive กันในกลุ่มเฉยๆ ว่าเข้าร่วมแล้วนะ ซึ่งในจุดนี้เอาไว้ให้ทุกคนตรวจสอบกันว่าเข้ามาแล้ว

3. เริ่มเกม

อันนี้ผมเป็นแนวเกม Confirm ว่า จะเริ่มเล่นแล้วให้ทุกคนเตรียมพร้อม ซึ่งสังเกตว่า ผมจะมีการใช้ ฟังก์ชัน : textMessageQuickReplyGame เป็นการกำหนด Action ด้วยว่า ก่อนสร้างเกม ก็สามารถ ล้างเกมได้ เพื่อความชัวร์ซึ่งหน้าตาจะประมาณนี้

File : message/message.js

const quickReplyGame = {
"items": [{
"type": "action",
"imageUrl": "https://bucket.ex10.tech/images/8d325cbc-d31f-11ee-97d4-0242ac12000b/originalContentUrl.png",
"action": {
"type": "message",
"label": "สร้างเกม",
"text": "สร้างเกม"
}
},
{
"type": "action",
"imageUrl": "https://bucket.ex10.tech/images/871f88ff-d3b0-11ee-97d4-0242ac12000b/originalContentUrl.png",
"action": {
"type": "message",
"label": "ล้างเกมของคุณ",
"text": "ล้างเกมของคุณ"
}
}
]
}

4. ล้างเกมของคุณ

ตรงนี้จะมีการเข้าไป Query Database ด้วยเงื่อนไขที่ว่าสามารถล้างเกมทั้งหมดของตัวเองได้ด้วย ฟังก์ชัน deleteGameUserId

await firebase.deleteGameUserId(groupId, userId)
//File : util/firebase.js
exports.deleteGameUserId = async (groupId, userId) => {

let gameDocument = gameDb.where("groupId", "==", groupId).where('ownerId', '==', userId)
await gameDocument.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
doc.ref.delete();
});
});

}

ซึ่งเราไม่แน่ใจว่าในนั้นจะมีข้อมูลเรายังไงบ้างแต่ก็ สามารถ ล้างเกม ให้หมดได้เลยและเมื่อลบเรียบร้อย เราค่อย Reply แจ้งกลับไปบอกในห้องว่า เราลบเรียบร้อยแล้ว

await line.reply(event.replyToken, [messages.textMessageQuickReplyGame(`ระบบได้ลบเกมของ ${lineProfile.data.displayName} เรียบร้อย`)])

จบแล้วสำหรับเตรียมตัว เอาละครับ ของจริงเริ่มหลังจากนี้เตรียมแล้วลุยกันเลย

5. สร้างเกม


// If Game Running
// Check Duplicate for Create Game
const count = await firebase.getCountGameGroupStatus(groupId, false)
if (count === 0) {
const resultId = await firebase.createGame(userId, groupId)
await line.reply(event.replyToken, [messages.selectMessage(userId, groupId, resultId)])
} else {
await line.reply(event.replyToken, [messages.textMessage("เกมได้ถูกสร้างแล้วไม่สามารถสร้างทับกันได้")])
}

ในเริ่มต้นสร้างเกมจะมีการตรวจสอบก่อนว่า มีเกม ทำงานอยู่หรือไม่

const count = await firebase.getCountGameGroupStatus(groupId, false)
exports.getCountGameGroupStatus = async (groupId, status) => {
const gameDocument = gameDb.where("groupId", "==", groupId).where('endgame', '==', status)
const gameCount = await gameDocument.count().get()
return gameCount.data().count
}

หากไม่มีก็สามารถ Create Game ได้ที่ ฟังก์ชัน createGame

File : util/firebase.js

exports.createGame = async (userId, groupId) => {

const gameDocument = gameDb.where("groupId", "==", groupId).where("userId", "==", userId)
const gameCount = await gameDocument.count().get()
if (gameCount.data().count === 0) {
const result = await gameDb.add({
ownerId: userId, // userID ผู้สร้างเกม
groupId: groupId, // line group id
ownerSelect: false, // defualt ค่าเป็น false สำหรับเลือก ค้อน กรรไกร กระดาษ
endgame: false, // สถานะจบเกม
users: [], // user ผู้เข้าร่วม
createAt: Date.now() // เวลาสร้างเกม
})
return result.id // game id
}
return false

}

และเมื่อสร้างเสร็จแล้วก็ให้ Chatbot Reply Flex Message เกมออกไป

โดย Action ของการเลือกผมจะใช้ Type Postback ตัวอย่างหน้าตาแบบนี้

// ตัวอย่าง action กรรไกร
{
"type": "postback",
"label": "กรรไกร",
"data": `{"userId": "${userId}", "item": "scissors", "gameId": "${gameId}" , "groupId": "${groupId}"}`
}

จะเห็นว่าผมมีการยัด JSON value เข้าในใน property data เลยครับ

ส่วนจบเกมเราจะปัดข้อมูลหน้าตาแบบนี้

{
"type": "postback",
"label": "จบเกม",
"displayText": "จบเกม",
"data": `{"userId":"${userId}", "item": "endgame","gameId":"${gameId}"}`
},

เพื่อให้ส่งข้อมูลไป ฟังก์ชัน endgame ครับ

แต่ถ้าหากมีอยู่แล้วก็ให้ Chatbot ทำการ Reply กลับไปว่า “เกมได้ถูกสร้างแล้วไม่สามารถสร้างทับกันได้” ซึ่งจะต้องให้ผู้สร้างจบเกมก่อน หรือลบเกมเท่านั้น

6. เล่นเกม

เมื่อผู้ร่วมเล่นเกม และ ผู้สร้างเกม เลือก กระดาษ , กรรไกร, ค้อน ให้ Chatbot แสดงแจ้งเตือน ว่าได้ทำการเลือกแล้ว (แต่ต้องไม่บอกนะว่าเลือกอะไร)

ในส่วนของ Code เราจะใช้ Postback Data

  // https://developers.line.biz/en/reference/messaging-api/#postback-event
if (event.type === "postback") {

// Data parse of Postback event
const DPB = JSON.parse(event.postback.data)

// Owner and Member Select
if (DPB.item === "rock" || DPB.item === "paper" || DPB.item === "scissors") {

await gameAction(DPB, groupId, userId, event.replyToken)
return res.end();

}
...

return res.end();

}

ซึ่งผมได้สร้าง ฟังก์ชัน gameAction เพื่อทำ Logic ต่างๆ

File : src/webhook.js

// DPB = data pare of postback event 
async function gameAction(DPB, groupId, userId, replyToken) {

// Check Status for Game is Running
const checkGameStatus = await firebase.getCheckGameGroupStatus(groupId, DPB.gameId)
if (checkGameStatus) {

// Validate isOwner
const isOwnerSelected = await firebase.updateInsertOwnerSelect(groupId, DPB.gameId, DPB.item, userId)
if (isOwnerSelected) {
let msgOwnerSelected = "ถึงแม้เป็น ผู้สร้างห้อง ก็เปลี่ยนใจไม่ได้!"
if (isOwnerSelected === "done") {
// Can't Change
msgOwnerSelected = "ตอนนี้ ผู้สร้างห้องได้ทำการเลือกแล้ว ทุกคนรีบเลือกด่วน!"
}
await line.reply(replyToken, [messages.textMessage(msgOwnerSelected)])
} else {
// Validate isMember
const isMemberSelected = await firebase.updateInsertJoinerSelect(groupId, DPB.gameId, DPB.item, userId)
const lineProfile = await line.getProfileGroup(groupId, userId)

let msgMemberSelect = `ขณะนี้ ${lineProfile.data.displayName} ได้ทำการเลือกแล้ว!`
if (!isMemberSelected) {
// Can't Change
msgMemberSelect = `คุณ ${lineProfile.data.displayName} ไม่สามารถเปลี่ยนใจได้`
}
await line.reply(replyToken, [messages.textMessage(msgMemberSelect)])
}
}
}
  • Step 1 ให้ทำการตรวจสอบก่อนว่า มีเกมนี้อยู่จริงๆ
const checkGameStatus = await firebase.getCheckGameGroupStatus(groupId, DPB.gameId)
  • Step 2 เมื่อผู้เล่นเลือก ค้อน กรรไกร หรือ กระดาษ เราจะทำการ Insert Data เข้าไปในฐานะผู้สร้างเกม
const isOwnerSelected = await firebase.updateInsertOwnerSelect(groupId, DPB.gameId, DPB.item, userId)

ซึ่งในตัว ฟังก์ชัน updateInsertOwnerSelect นั้นจะมีการตรวจสอบว่าเป็น ผู้สร้างเลือกหรือเปล่า และ เคยเลือกแล้วหรือยัง เพราะ ถ้าเคยเลือกแล้วจะไม่สามารถเลือกซ้ำได้

File : util/firebase.js

exports.updateInsertOwnerSelect = async (groupId, gameId, item, userId) => {
try {
const querySnapshot = await gameDb
.where('groupId', '==', groupId)
.where('endgame', '==', false)
.get();
for (const doc of querySnapshot.docs) {
if (userId === doc.data().ownerId) {
if (doc.id === gameId && !doc.data().ownerSelect) {
await gameDb.doc(doc.id).update({
ownerSelect: item,
});
return "done";
} else {
return "selected";
}
} else {
return false;
}

}
} catch (error) {
return false;
}

ถ้าหากว่า response เป็น done จะ reply เป็น เลือกแล้ว response เป็น selected ก็จะขึ้นว่าไม่สามารถเปลี่ยนแปลงได้

  if (isOwnerSelected) {
let msgOwnerSelected = "ถึงแม้เป็น ผู้สร้างห้อง ก็เปลี่ยนใจไม่ได้!"
if (isOwnerSelected === "done") {
// Can't Change
msgOwnerSelected = "ตอนนี้ ผู้สร้างห้องได้ทำการเลือกแล้ว ทุกคนรีบเลือกด่วน!"
}
await line.reply(replyToken, [messages.textMessage(msgOwnerSelected)])
}

แต่ในกรณีที่ ฟังก์ชัน updateInsertOwnerSelect คืนค่าเป็น false กลับมา ผมจะตีว่าเป็น ผู้ร่วมเล่นเกม ที่ไม่ใช่ ผู้สร้างห้อง

const isMemberSelected = await firebase.updateInsertJoinerSelect(groupId, DPB.gameId, DPB.item, userId)

ความวุ่นวายอยู่ตรงนี้แหละ เพื่อนๆ ฟังก์ชัน updateInsertJoinerSelect ต้องปั้นข้อมูลไป Update ใน Database ซึ่งจะได้หน้าตาแบบนี้

File : util/firebase.js

exports.updateInsertJoinerSelect = async (groupId, gameId, item, userId) => {
try {
const querySnapshot = await gameDb
.where('groupId', '==', groupId)
.where('endgame', '==', false)
.get();
for (const doc of querySnapshot.docs) {

if (doc.id === gameId && doc.data().ownerId !== userId) {
let userlistItem = doc.data().users
const dataLength = Object.keys(userlistItem).length;

let arrayData = userlistItem
if (dataLength > 0) {
let memberList = []
for (const [index, userObject] of userlistItem.entries()) {
let memberId = Object.keys(userObject)[0];
memberList.push(memberId)
}
const hasValue = memberList.some(e => e === userId);
if (!hasValue) {
const newData = {
[userId]: item,
};
arrayData.push(newData)
await gameDb.doc(doc.id).update({
users: arrayData,
});
return true;
} else {
return false;
}

} else {

const newData = {
[userId]: item,
};
arrayData.push(newData)
await gameDb.doc(doc.id).update({
users: arrayData,
});

return true;
}

}

return false;
}
} catch (error) {
return false;
}
};
  • ขั้นแรก เราต้อง ตรวจสอบ ว่า Property users ยังเป็น [] อยู่ไหม ถ้าใช้ก็ ปั้น Object userid[item]ไปตรงๆ
  • แต่ถ้าหากว่ามี user อยู่แล้วก็ตรวจสอบว่าไม่มี userId ตัวเองอยู่ใน users[]

สรุปหน้าตาจะเมื่อส่งเข้าไปใน firestore จะประมาณนี้

หมายเหตุ ซึ่ง ย้อนกลับไป Event Member Join จะพบว่า ผมมีการตรวจสอบเพิ่มด้วยว่าทางมีการเข้ามาระหว่างเกมกำลังดำเนินการ ด้วยสาเหตุที่ว่า

  • จะไม่เห็น Flex Message ก่อนหน้า
  • และต้องให้เขารับรู้ด้วยว่าเข้ามาระหว่างเล่น

7. จบเกม (ช่วงสุดท้าย)

ขั้นตอนจบเกมเป็นส่วนสำคัญมากเพราะเราต้องดึงข้อมูลมาสรุปรายงาน และในตัว postback ก็มี value endgame มาด้วย จะเข้า ฟังก์ชัน endGame

 // End Game 
if (DPB.item === "endgame") {

await endGame(DPB, groupId, userId, event.replyToken)
return res.end();

}

ฟังก์ชัน endGame

async function endGame(DPB, groupId, userId, replyToken) {

// Update Status "endgame": true
const result = await firebase.endGame(groupId, userId)

if (result) {

// Get Member List by Group ID and Game ID
const dataItem = await firebase.getUserByGame(groupId, DPB.gameId, userId);

let userWin = [];
let userEqual = [];
let userLost = [];

const ownerLineProfile = await line.getProfileGroup(groupId, userId);

// Exception Error Game Fail
if (!dataItem.ownerSelect || dataItem.users.length === 0) {
await line.reply(replyToken, [messages.textMessageQuickReplyGame("เกมได้สิ้นสุดลง แบบไม่สมบูรณ์ กรุณาเริ่มต้นเกมใหม่อีกครั้ง")]);
} else {

for (const userObject of dataItem.users) {
const memberId = Object.keys(userObject)[0];

const lineProfile = await line.getProfileGroup(groupId, memberId);

const userSelection = userObject[memberId];
const ownerSelection = dataItem.ownerSelect;

// Is Equl
if (userSelection === ownerSelection) {
userEqual.push(lineProfile.data);

} else {
const userWinsAgainst = {
'rock': 'scissors',
'paper': 'rock',
'scissors': 'paper'
};

// Is Winer
if (userWinsAgainst[userSelection] === ownerSelection) {
userWin.push(lineProfile.data);

} else {
// Is Losser
userLost.push(lineProfile.data);
}
}
}


let memberNo = 1;
let nameList = 'ผู้สร้างห้อง ' + ownerLineProfile.data.displayName + ' เลือก ' + dataItem.ownerSelect;
const appendUsers = (list, label, emoji) => {
if (list.length > 0) {
nameList += `\n-----${label}-----`;
list.forEach((memberList) => {
nameList += `\n ${memberNo}.${memberList.displayName} ${emoji}`;
memberNo++;
});
}
};

appendUsers(userWin, "ผู้ชนะได้แก่", "✅");
appendUsers(userEqual, "ผู้เสมอได้แก่", "😉");
appendUsers(userLost, "ผู้แพ้ได้แก่", "❌");

nameList += "\n------ \nแพ้เป็นพระ คนชนะคนนี้เป็นของเธอนะ";

await line.reply(replyToken, [messages.textMessageEndGame(nameList)]);
}


} else {
await line.reply(replyToken, [messages.textMessage("ไม่พบสิทธิ์การจบเกมของท่าน หรือ เกมนี้ที่ท่านเลือกอาจจบลงแล้ว")])
}
}

เมื่อเข้า ฟังก์ชันนี้ ผมจะวิ่งไป update Status ของเกมให้จบเกม

// update status endgame : true
const result = await firebase.endGame(groupId, userId)

และเมื่อเสร็จเรียบร้อยผมก็ทำการดึง ข้อมูลขึ้นมาทั้งหมด เพื่อเตรียมรอไว้สำหรับออกรายงาน

const dataItem = await firebase.getUserByGame(groupId, DPB.gameId, userId);

แอบไปตรวจสอบเพิ่มให้นิดหน่อยว่าถ้า owner ยังไม่เลือก หรือ สมาชิกไม่ได้เลือก แล้วกดจบเกม ก็จะขึ้นว่า เกมสิ้นสุด แต่ไม่สมบูรณ์

// Error Game Fail
if (!dataItem.ownerSelect || dataItem.users.length === 0) {
await line.reply(replyToken, [messages.textMessageQuickReplyGame("เกมได้สิ้นสุดลง แบบไม่สมบูรณ์ กรุณาเริ่มต้นเกมใหม่อีกครั้ง")]);
}

จุดตัดสินอยู่ตรงนี้ครับ

let userWin = [];
let userEqual = [];
let userLost = [];
for (const userObject of dataItem.users) {
const memberId = Object.keys(userObject)[0];

const lineProfile = await line.getProfileGroup(groupId, memberId);

const userSelection = userObject[memberId];
const ownerSelection = dataItem.ownerSelect;

// Is Equl
if (userSelection === ownerSelection) {
userEqual.push(lineProfile.data);

} else {
const userWinsAgainst = {
'rock': 'scissors',
'paper': 'rock',
'scissors': 'paper'
};

// Is Winer
if (userWinsAgainst[userSelection] === ownerSelection) {
userWin.push(lineProfile.data);

} else {
// Is Losser
userLost.push(lineProfile.data);
}
}
}

เอาสิ่งที่เขาเลือกมาหาว่าใครแพ้ชนะด้วยเงื่อนไข ง่ายๆ ว่า

  • ถ้า เหมือนกัน ก็ เสมอ และ push เข้า userEqual
  • ถ้าเลือกตามเงื่อนไขของ userWinsAgainst ก็ ชนะไป และ push เข้า userWin
  • แต่ถ้าไม่ ก็นับเป็นแพ้ทั้งหมด และ push เข้า userLost

เพียงเท่านี้ก็จะได้ ก้อนข้อมูลผู้แพ้ ชนะ เสมอแล้ว

ส่วนสุดท้าย รายงานผลคนในห้อง

  let memberNo = 1;
let nameList = 'ผู้สร้างห้อง ' + ownerLineProfile.data.displayName + ' เลือก ' + dataItem.ownerSelect;
const appendUsers = (list, label, emoji) => {
if (list.length > 0) {
nameList += `\n-----${label}-----`;
list.forEach((memberList) => {
nameList += `\n ${memberNo}.${memberList.displayName} ${emoji}`;
memberNo++;
});
}
};

appendUsers(userWin, "ผู้ชนะได้แก่", "✅");
appendUsers(userEqual, "ผู้เสมอได้แก่", "😉");
appendUsers(userLoss, "ผู้แพ้ได้แก่", "❌");

nameList += "\n------ \nแพ้เป็นพระ คนชนะคนนี้เป็นของเธอนะ";

await line.reply(replyToken, [messages.textMessageEndGame(nameList)]);

เรียบร้อยแล้ว คราวนี้ในห้อง LINE Group ของเราก็จะได้รายชื่อแพ้ ชนะ เอาไป ทำโทษกันสนุกได้เลย

ของแถม สุดพิเศษของเรา นั่นก็คือผมมีการ แอบใส่ Action ตัวใหม่ล่าสุดเข้าไปด้วยที่ปุ่ม “สรุปผลการแข่งขัน”

Clipboard action

ซึ่งมีน้องพร้อม LAE ของเราได้เขียนบทความตัวอย่างไว้ดีมากๆ

โดยใน Code ผมเอาไปใส่ตรง Quick Reply น่าจะเป็นแบบนี้

{
"type": "action",
"imageUrl": "https://bucket.ex10.tech/images/9f2a63dc-d84e-11ee-97d4-0242ac12000b/originalContentUrl.png",
"action": {
"type": "clipboard",
"label": "สรุปผลการแข่งขัน",
"clipboardText": text
}
}

ผมนำรายชื่อมาใส่ กันแบบนี้เลยถ้ากดแล้วจะขึ้นแบบนี้

เพื่อนก็สามารถนำไป วางที่ไหนก็ได้เลยครับ

เรียบร้อยแล้วครับสำหรับบทความทำเกมสนุกๆ เป่ายิ้งฉุบ ถ้าหากเพื่อนๆอยากลองเล่นกันดูก็ Scan เล่นกันได้เลยครับ

เพื่อนๆสามารถนำไปต่อยอด หรือ ทำอะไรได้อีกหลายอย่างแน่นอน ขอบคุณสำหรับการติดต่อตามและพบกันใหม่ในบทความหน้านะครับ ขอให้สนุกกับการพัฒนาตนเอง Bye Bye..

--

--