RFIDを使ったくじ引きをつくり、イベントで遊んでもらいました
私が運営している「工作室もくもくはりねずみ」で、11月19日に「もくはり文化祭」というイベントを開催しました。
お箸作り・木の玩具づくりなど様々なものづくりワークショップや
展示、体験ができるものづくりのイベントです。
入り口のエントランスで客引きになるライトなゲームがあるといいよねーという話になり、
手元にM5StackのRFIDモジュールがあったため、くじびきを作ってみました。
くじびきはこんなふうに遊べます。
モンスターそれぞれにRFIDが仕込まれており、リーダーで読み込むとくじびきができます。
3等でお茶、2等でミニワークショップ、1等ですごいワークショップが当たるハズれなしのくじびきです。モンスターくじびき!
— はりねずみ麺 きさいち@工作室もくはりオーナー (@hedgehog_noodl) November 13, 2023
完成です!
三等: お茶
二等: ミニワークショップ
一等: すごいワークショップ
グラフィックはAdobe fireflyとDALL-E#もくはり pic.twitter.com/nawp04jtQr
使ったもの
- RFID
- M5Stack用WS1850S搭載 RFID 2ユニット
- M5Stack Core2
- Google Sheets
- GAS
在庫の管理とくじびきの確率について
- 3等と1等は、在庫が限られるので在庫管理をする必要がある。2等はいくつあたりが出てもOK。
- 1等は2%、2等は36%、3等は62%の確率とする
- RFIDの数は20個、毎回どれが何の賞かはリセットされる
- くじ引きが行われる度にGoogle Sheetsの在庫を一つ減らす。
くじ引きが始まると以下の動きをします。
- くじびきが始まると1〜100の中からランダムで数字を20個選び、RFIDのUIDそれぞれに割り当てる
- ユーザーがRFIDを読み込むと、そのRFIDに割り当てられた数字によって賞を表示します。1〜62の場合は3等、63〜98の場合は2等、99〜100の場合は1等とします。
...あれ??? 確率、間違ってない!!?
こうして記事として書いていて気が付きましたが、
これでは「1等は2%、2等は36%、3等は62%の確率」という条件になっていません!
とりあえず、1等が一番確率低くなってることは間違いないのでいいとしましょう。
確率の計算が得意な方、この方法による実際の確率を計算してくれたら嬉しいです。
コード
コード全文はこちら
#include <M5Core2.h>
#include "MFRC522_I2C.h"
#include <Wire.h>
#include <HTTPClient.h>
#include <SD.h>
#include <FS.h>
const char* ssid = "SSID"; // WiFiのSSID
const char* password = "PassWord"; // WiFiのパスワード
const char* serverName = "GASのエンドポイント"; // GASのエンドポイント
String result = "a";
// UIDの配列
const char* uids[] = {
"04730653110189",
"04267253110189",
"04a62953110189",
"042d7252110189",
"04887053110189",
"04277653110189",
"04226d53110189",
"04904853110189",
"045c4a53110189",
"04e0b253110189",
"04fd8453110189",
"04a8f652110189",
"043b3553110189",
"04d27b53110189",
"0473ed52110189",
"047e0f53110189",
"04e3a553110189",
"04ab9153110189",
"040f2d53110189",
"04e6ba52110189"
};
// 製品名の配列
const char* prd[] = {"tea", "mini", "ws"};
// MFRC522のインスタンス作成
MFRC522 mfrc522(0x28);
enum Mode {
Initialization,
Lottery,
Finished,
OutOfStock
};
Mode currentMode = Initialization;
void setup() {
//M5.begin();
M5.begin(true, true, true, true, mbus_mode_t::kMBusModeOutput,
true);
//SD.begin(); // SDカードの初期化
//M5.Lcd.setTextSize(2);
Wire.begin();
mfrc522.PCD_Init();
randomSeed(analogRead(0));
// WiFi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
M5.Lcd.setCursor(0, 0);
M5.Lcd.drawJpgFile(SD, "/initializing.jpg",0,0);
//M5.Lcd.println("Connecting to WiFi...");
}
//M5.Lcd.println("Connected to WiFi");
// 在庫数の取得
int stockCount = getStockCount();
if (stockCount > 0) {
currentMode = Lottery;
} else {
currentMode = OutOfStock;
}
}
void loop() {
switch (currentMode) {
case Initialization:
// 何もしない(既に初期化は完了)
break;
case Lottery:
performLottery();
break;
case Finished:
//M5.Lcd.println(result);
if(result == "ws"){
M5.Lcd.drawJpgFile(SD, "/finish3.jpg",0,0);
}else if(result == "mini"){
M5.Lcd.drawJpgFile(SD, "/finish2.jpg",0,0);
}else{
M5.Lcd.drawJpgFile(SD, "/finish1.jpg",0,0);
}
break;
case OutOfStock:
M5.Lcd.println("Out of Stock");
break;
}
delay(1000);
}
void performLottery() {
// ランダムな20個の整数を選択
//M5.Lcd.println("Start Lottery");
M5.Lcd.setCursor(0, 0);
M5.Lcd.drawJpgFile(SD, "/lottery.jpg",0,0);
int numbers[20];
for (int i = 0; i < 20; i++) {
numbers[i] = random(1, 101); // 1〜100のランダムな数
}
// UIDが読み取られた場合の処理
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
currentMode = Finished; // 終了モードへの切り替え
String readUID = "";
for (byte i = 0; i < mfrc522.uid.size; i++) {
readUID += String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
readUID += String(mfrc522.uid.uidByte[i], HEX);
}
Serial.println(readUID);
M5.Axp.SetLDOEnable(3, true); // Open the vibration. 开启震动马达
delay(200);
M5.Axp.SetLDOEnable(3, false);
delay(200);
// UIDと整数の割り当てを確認
for (int i = 0; i < 20; i++) {
if (readUID.equals(uids[i])) {
int assignedNumber = numbers[i];
// 範囲に応じた製品名を表示
if (assignedNumber <= 62) {
result = prd[0];
sendToGoogleSheets(2);
//M5.Lcd.println(prd[0]);
} else if (assignedNumber <= 98) {
result = prd[1];
sendToGoogleSheets(3);
//M5.Lcd.println(prd[1]);
} else {
result = prd[2];
sendToGoogleSheets(4);
//M5.Lcd.println(prd[2]);
}
break;
}
}
}
}
int getStockCount() {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(serverName); // GASのURL
int httpCode = http.GET();
if (httpCode > 0) {
// リダイレクトのチェック
if (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
String newLocation = http.getLocation(); // 新しいURLを取得
http.end();
http.begin(newLocation); // 新しいURLで再度HTTPリクエスト
httpCode = http.GET(); // 再度リクエスト
if (httpCode <= 0) {
return 0; // 二回目のリクエストが失敗した場合
}
}
// リダイレクト後のデータ処理
String payload = http.getString();
Serial.println(payload);
http.end();
return payload.toInt();
} else {
http.end();
return 0;
}
} else {
return 0;
}
}
void sendToGoogleSheets(int row) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(serverName);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
String httpRequestData = "row=" + String(row);
int httpResponseCode = http.POST(httpRequestData);
if (httpResponseCode > 0) {
String payload = http.getString();
Serial.println(payload);
}
else {
Serial.print("Error on sending POST: ");
Serial.println(httpResponseCode);
}
http.end();
}
else {
Serial.println("WiFi Disconnected");
}
}
効果音を入れたかったのですが、どうしてもうまくいきませんでした。
サンプルコードはうまくいったのになぜだろう。未だに解明できておりません。
くじびきボードの作成
キャラクターと背景イラストはAI生成
イラスト生成は、AIで生成しています。
背景とモンスターはFireflyを、M5Stack上で表示する画面はDALL-Eを使用しました。
ボードの作成
3mm厚のMDFをカットし、A3普通紙に背景を印刷してスプレーのり77で張りつけました。
モンスターは、写真紙に印刷してハサミでカットし、RFIDのタグを重ねて両面テープで貼付けしています。
タイトル部分はLaser Pecker2というレーザー加工機で刻印しました。
25人の方に体験いただきました!
イベント当日は、25人もの方にくじびきを体験いただきました。(一人が複数回行ったケースも有り)
以外にも、スマホゲームのガチャほどではないですが中毒性があるようで、
当たるまで何度もくじをひく方も......!
そして、意外と1等が出る。ということがわかりました。
冒頭に述べた通り、確率の計算が間違っていたので正しい確率が計算できておりません。
当日は1等が2回、そこそこ早い段階で出ました。
現在、1等が出る確率は何%なんだろう......
そして、何%くらいにすると妥当なんだろう......
うまくいかなかったところ・解決できていないところも残っておりますが、
お客さんを呼び込みつつ楽しんでいただけてよかったです。