家の中を小さなカジノにプロジェクト
マカオで遊んださいころカジノSIC BOを自宅で再現してみた。
Raspberry Piでさいころを撮影し、出目を判別するようにしました。
●使用したマイコン
①Arduino Uno: サイコロ振動モーター制御用
②Raspberry Pi Model B: Wifiサーバ構築用
●流れ
Arduino Uno
↓End信号送信(UART)
Raspberry Pi
・さいころ撮影,出目検出
・出目結果をArduinoとWifiで送信
↓出目結果送信(UART)
Arduino Uno
・LCDに結果表示
・20秒待ってサイコロをシャッフル
・End信号を送信
↓End信号送信(UART)
Raspberry Pi
●(参考)コード
★Arduino Uno
#include <LiquidCrystal.h>
// LCD
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// ピン
const int motorPin = 4;
const int buttonPin = 2;
//const int ledPin = 13;
// 状態管理
char input[32];
byte idx = 0;
unsigned long displayTime = 0;
bool waitingEnd = false;
bool rolling = false;
unsigned long rollTime = 0;
void setup() {
Serial.begin(9600);
pinMode(motorPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
//pinMode(ledPin, OUTPUT);
lcd.begin(16, 2);
lcd.clear();
lcd.print("Dice Ready");
}
void loop() {
// ===== ボタン =====
if (digitalRead(buttonPin) == LOW) {
Serial.println("END");
delay(300); // チャタリング対策(OK)
}
// ===== シリアル受信 =====
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
input[idx] = '\0';
processDice(input);
idx = 0;
} else if (idx < sizeof(input) - 1) {
input[idx++] = c;
}
}
// ===== 5秒後にサイコロ回転 =====
if (waitingEnd && millis() - displayTime >= 20000) {
startRoll();
waitingEnd = false;
}
}
// ==============================
void processDice(const char* data) {
if (strlen(data) == 0) return;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Dice Result");
lcd.setCursor(0, 1);
lcd.print(" "); // 行クリア
lcd.setCursor(0, 1);
lcd.print(data);
displayTime = millis();
waitingEnd = true;
}
void startRoll() {
//digitalWrite(ledPin, HIGH);
digitalWrite(motorPin, HIGH);
delay(500);
digitalWrite(motorPin, LOW);
delay(1000);
//digitalWrite(ledPin, LOW);
Serial.println("END");
delay(300); // チャタリング対策(OK)
lcd.clear();
lcd.print("Ready");
}
★Raspberry Pi
import cv2
import numpy as np
import time
import serial
import threading
import os
from flask import Flask, jsonify, render_template
from picamera2 import Picamera2
# =========================
# Serial(Arduino)
# =========================
SERIAL_PORT = "/dev/ttyACM0"
BAUDRATE = 9600
try:
ser = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=0.1)
time.sleep(2)
print("Arduino connected")
except Exception as e:
print("Arduino connection failed:", e)
ser = None
# =========================
# Flask
# =========================
app = Flask(__name__)
latest_dice = ""
status = "WAIT_END"
@app.route("/")
def index():
return render_template("sicbo.html")
@app.route("/dice")
def dice():
return jsonify({
"dice": latest_dice,
"status": status
})
def run_flask():
app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
# =========================
# サイコロ検出(枠描画付き)
# =========================
def detect_dice(image):
blur = cv2.GaussianBlur(image, (9, 9), 0)
hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV)
kernel = np.ones((5, 5), np.uint8)
out = image.copy()
# --- 出目検出 ---
lower_blue = np.array([105, 100, 160])
upper_blue = np.array([150, 255, 255])
mask_blue = cv2.inRange(hsv, lower_blue, upper_blue)
lower_red1 = np.array([0, 57, 220])
upper_red1 = np.array([40, 80, 255])
lower_red2 = np.array([140, 57, 220])
upper_red2 = np.array([180, 80, 255])
mask_red = cv2.bitwise_or(
cv2.inRange(hsv, lower_red1, upper_red1),
cv2.inRange(hsv, lower_red2, upper_red2)
)
mask_pip = cv2.bitwise_or(mask_blue, mask_red)
mask_pip = cv2.morphologyEx(mask_pip, cv2.MORPH_CLOSE, kernel)
contours_pip, _ = cv2.findContours(
mask_pip, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
pip_centers = []
for cnt in contours_pip:
area = cv2.contourArea(cnt)
if area < 50:
continue
x, y, w, h = cv2.boundingRect(cnt)
# 赤枠(出目)
cv2.rectangle(out, (x, y), (x + w, y + h), (0, 0, 255), 3)
cx = x + w // 2
cy = y + h // 2
pip_centers.append((cx, cy))
# --- サイコロ本体 ---
lower_gray = np.array([0, 10, 230])
upper_gray = np.array([180, 100, 255])
mask_dice = cv2.inRange(hsv, lower_gray, upper_gray)
mask_dice = cv2.morphologyEx(mask_dice, cv2.MORPH_CLOSE, kernel)
contours_dice, _ = cv2.findContours(
mask_dice, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
dice_results = []
for cnt in contours_dice:
area = cv2.contourArea(cnt)
if area < 600:
continue
x, y, w, h = cv2.boundingRect(cnt)
# 緑枠(サイコロ)
cv2.rectangle(out, (x, y), (x + w, y + h), (0, 255, 0), 4)
count = 0
for cx, cy in pip_centers:
if x < cx < x + w and y < cy < y + h:
count += 1
if count > 0:
dice_results.append(count)
# 出目表示
cv2.putText(
out,
str(count),
(x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(0, 255, 0),
2
)
return dice_results, out
# =========================
# END信号待ち
# =========================
def wait_for_end():
if ser and ser.in_waiting:
line = ser.readline().decode("utf-8").strip()
if line == "END":
print("END signal received")
return True
return False
# =========================
# メイン処理
# =========================
def main_loop():
global latest_dice, status
picam2 = Picamera2()
config = picam2.create_still_configuration(
main={"size": (1920, 1080), "format": "BGR888"}
)
picam2.configure(config)
picam2.start()
time.sleep(1)
picture_folder = os.path.join(os.path.dirname(__file__), "picture")
if not os.path.exists(picture_folder):
os.makedirs(picture_folder)
print("System ready")
while True:
if not wait_for_end():
time.sleep(0.05)
continue
status = "BUSY"
latest_dice = ""
# 揺れ対策
time.sleep(0.3)
# 1秒待って撮影
time.sleep(1)
image = picam2.capture_array()
dice, result_image = detect_dice(image)
# 画像保存
filename = os.path.join(
picture_folder,
"dice_" + str(int(time.time())) + ".jpg"
)
cv2.imwrite(filename, result_image)
if len(dice) == 3 and all(1 <= d <= 6 for d in dice):
latest_dice = ",".join(map(str, dice))
status = "DONE"
print("Detected:", latest_dice)
if ser:
ser.write((latest_dice + "\n").encode())
else:
latest_dice = "Nocount"
status = "NG"
print("Nocount")
if ser:
ser.write(b"Nocount\n")
# =========================
# 起動
# =========================
if __name__ == "__main__":
threading.Thread(target=run_flask, daemon=True).start()
main_loop()
★HTML(ファイル名を"sicbo.html")
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.85">
<title>骰寶 Sic Bo</title>
<style>
body{
margin:0;
background:#000;
color:#d4af37;
font-family:"Times New Roman",serif;
transform: scale(0.55);
transform-origin: top center;
}
h1{
text-align:center;
padding:8px;
margin:0;
font-size:22px;
}
.section-title{
margin:10px 8px 6px;
font-size:16px;
border-bottom:1px solid #d4af37;
}
.grid-2,.grid-3,.grid-4,.grid-6{
display:grid;
gap:6px;
padding:0 8px;
}
.grid-2{grid-template-columns:repeat(2,1fr);}
.grid-3{grid-template-columns:repeat(3,1fr);}
.grid-4{grid-template-columns:repeat(4,1fr);}
.grid-6{grid-template-columns:repeat(6,1fr);}
.bet{
background:#111;
border:2px solid #d4af37;
border-radius:8px;
padding:6px 2px 4px;
text-align:center;
font-size:14px;
cursor:pointer;
color:#d4af37;
line-height:1.2;
}
.bet small{
display:block;
font-size:10px;
margin-top:3px;
color:#aaa;
}
.bet.selected{
background:#d4af37;
border-color:#ffd700;
color:#000;
}
.bet.win{
background:#006400;
border-color:#00ff66;
color:#fff;
}
.amount{
font-size:11px;
color:#00ffff;
}
#result{
margin:10px;
padding:8px;
font-size:28px;
text-align:center;
border:2px solid #d4af37;
border-radius:10px;
background:#111;
}
#timerBarWrap{
margin:6px 10px;
height:10px;
background:#222;
border-radius:6px;
overflow:hidden;
border:1px solid #d4af37;
}
#timerBar{
height:100%;
width:100%;
background:#00ff66;
transition:width 1s linear;
}
</style>
</head>
<body>
<h1>🎲 骰寶 SIC BO</h1>
<div style="text-align:center;margin:6px;font-size:18px">
餘額:$<span id="balance">1000</span>
總下注:$<span id="totalBet">0</span>
</div>
<div style="text-align:center;margin:6px">
Chip:
<button onclick="setChip(10)">10</button>
<button onclick="setChip(50)">50</button>
<button onclick="setChip(100)">100</button>
<button onclick="setChip(500)">500</button>
<button onclick="undoBet()">取消下注</button>
<button onclick="clearBets()">清空下注</button>
</div>
<div style="margin:10px;padding:8px;border:1px solid
#d4af37;border-radius:10px;text-align:center">
出目入力(例:1,1,3)<br>
<input id="manualDice" style="width:110px;font-size:16px;text-align:center">
<button onclick="applyDice()">反映</button>
</div>
<div id="result">🎲 ---</div>
<div id="timerBarWrap">
<div id="timerBar"></div>
</div>
<div class="section-title">大小</div>
<div class="grid-2">
<div class="bet" data-type="big">大<small>1:1</small></div>
<div class="bet" data-type="small">小<small>1:1</small></div>
</div>
<div class="section-title">單雙</div>
<div class="grid-2">
<div class="bet" data-type="odd">單<small>1:1</small></div>
<div class="bet" data-type="even">雙<small>1:1</small></div>
</div>
<div class="section-title">單骰</div>
<div class="grid-6">
<div class="bet" data-type="single-1">1<small>1:1</small></div>
<div class="bet" data-type="single-2">2<small>1:1</small></div>
<div class="bet" data-type="single-3">3<small>1:1</small></div>
<div class="bet" data-type="single-4">4<small>1:1</small></div>
<div class="bet" data-type="single-5">5<small>1:1</small></div>
<div class="bet" data-type="single-6">6<small>1:1</small></div>
</div>
<div class="section-title">點數</div>
<div class="grid-4">
<script>
const sumOdds={4:"50:1",5:"18:1",6:"14:1",7:"12:1",8:"8:1",
9:"6:1",10:"6:1",11:"6:1",12:"6:1",
13:"8:1",14:"12:1",15:"14:1",16:"18:1",17:"50:1"};
for(let i=4;i<=17;i++){
document.write(`<div class="bet"
data-type="sum-${i}">${i}<small>${sumOdds[i]}</small></div>`);
}
</script>
</div>
<div class="section-title">對子</div>
<div class="grid-3">
<script>
for(let i=1;i<=6;i++){
document.write(`<div class="bet"
data-type="pair-${i}">${i}-${i}<small>8:1</small></div>`);
}
</script>
</div>
<div class="section-title">豹子</div>
<div class="grid-4">
<div class="bet" data-type="any-triple">任意<small>24:1</small></div>
<script>
for(let i=1;i<=6;i++){
document.write(`<div class="bet"
data-type="triple-${i}">${i}-${i}-${i}<small>150:1</small></div>`);
}
</script>
</div>
<div class="section-title">組合</div>
<div class="grid-6">
<script>
for(let a=1;a<=6;a++){
for(let b=a+1;b<=6;b++){
document.write(`<div class="bet"
data-type="combo-${a}-${b}">${a}-${b}<small>5:1</small></div>`);
}}
</script>
</div>
<script>
let balance=1000
let chip=10
let betHistory=[]
let isBettingOpen = true
let countdown = 20
let timer = null
let lastStatus = ""
const payout={
big:1,small:1,odd:1,even:1,
"single-1":1,"single-2":1,"single-3":1,
"single-4":1,"single-5":1,"single-6":1,
"pair-1":8,"pair-2":8,"pair-3":8,
"pair-4":8,"pair-5":8,"pair-6":8,
"any-triple":24,
"triple-1":150,"triple-2":150,"triple-3":150,
"triple-4":150,"triple-5":150,"triple-6":150
}
const sumPay={
4:50,5:18,6:14,7:12,8:8,
9:6,10:6,11:6,12:6,
13:8,14:12,15:14,16:18,17:50
}
function setChip(v){chip=v}
document.addEventListener("click",e=>{
if(e.target.classList.contains("bet")){
if(!isBettingOpen)return
if(balance<chip)return
let bet=parseInt(e.target.dataset.bet||0)
bet+=chip
balance-=chip
betHistory.push({el:e.target,amount:chip})
e.target.dataset.bet=bet
e.target.classList.add("selected")
let amt=e.target.querySelector(".amount")
if(!amt){
amt=document.createElement("div")
amt.className="amount"
e.target.appendChild(amt)
}
amt.innerText="$"+bet
updateTotal()
}
})
function undoBet(){
if(betHistory.length===0)return
let last=betHistory.pop()
let el=last.el
let amount=last.amount
let bet=parseInt(el.dataset.bet||0)
bet-=amount
balance+=amount
if(bet<=0){
bet=0
el.classList.remove("selected")
}
el.dataset.bet=bet
let amt=el.querySelector(".amount")
if(amt) amt.innerText=bet?("$"+bet):""
updateTotal()
}
function updateTotal(){
let total=0
document.querySelectorAll(".bet").forEach(b=>{
total+=parseInt(b.dataset.bet||0)
})
document.getElementById("totalBet").innerText=total
document.getElementById("balance").innerText=balance
}
function clearBets(){
document.querySelectorAll(".bet").forEach(b=>{
b.dataset.bet=0
b.classList.remove("selected")
let a=b.querySelector(".amount")
if(a)a.innerText=""
})
betHistory=[]
updateTotal()
}
function startCountdown(){
isBettingOpen = true
countdown = 20
const maxTime = 20
document.getElementById("result").innerText = "⏳ 下注時間: " + countdown
document.getElementById("timerBar").style.width = "100%"
timer = setInterval(()=>{
countdown--
let percent = (countdown / maxTime) * 100
document.getElementById("timerBar").style.width = percent + "%"
if(countdown > 0){
document.getElementById("result").innerText = "⏳ 下注時間: " + countdown
}else{
clearInterval(timer)
isBettingOpen = false
document.getElementById("result").innerText = "⛔ 下注停止"
}
},1000)
}
function applyDice(){
document.querySelectorAll(".bet").forEach(b=>b.classList.remove("win"))
const dice=document.getElementById("manualDice").value
.split(",").map(n=>+n.trim()).filter(n=>n>=1&&n<=6)
if(dice.length!==3)return
document.getElementById("result").innerText="🎲 "+dice.join(",")
const sum=dice.reduce((a,b)=>a+b,0)
const cnt={}
dice.forEach(d=>cnt[d]=(cnt[d]||0)+1)
const isTriple=Object.values(cnt).includes(3)
document.querySelectorAll(".bet").forEach(b=>{
const t=b.dataset.type
if(t==="big"&&sum>=11&&sum<=17&&!isTriple)b.classList.add("win")
if(t==="small"&&sum>=4&&sum<=10&&!isTriple)b.classList.add("win")
if(t==="odd"&&sum%2===1)b.classList.add("win")
if(t==="even"&&sum%2===0)b.classList.add("win")
if(t.startsWith("single-")&&dice.includes(+t.split("-")[1]))b.classList.add("win")
if(t.startsWith("sum-")&&sum===+t.split("-")[1])b.classList.add("win")
if(t.startsWith("pair-")&&cnt[+t.split("-")[1]]>=2)b.classList.add("win")
if(t==="any-triple"&&isTriple)b.classList.add("win")
if(t.startsWith("triple-")&&cnt[+t.split("-")[1]]===3)b.classList.add("win")
if(t.startsWith("combo-")){
const [a,c]=t.split("-").slice(1).map(Number)
if(dice.includes(a)&&dice.includes(c))b.classList.add("win")
}
})
let winMoney=0
document.querySelectorAll(".bet.win").forEach(b=>{
let bet=parseInt(b.dataset.bet||0)
if(!bet)return
let type=b.dataset.type
let pay=0
if(payout[type]) pay=payout[type]
if(type.startsWith("sum-")){
let n=parseInt(type.split("-")[1])
pay=sumPay[n]
}
if(type.startsWith("combo-")) pay=5
let win=bet*(pay+1)
winMoney+=win
})
balance+=winMoney
updateTotal()
setTimeout(clearBets,1500)
}
async function updateDice(){
try{
const res=await fetch("/dice")
const data=await res.json()
if (data.status === "BUSY" && lastStatus !== "BUSY") {
isBettingOpen = false
}
if (data.status === "DONE" && lastStatus !== "DONE") {
document.getElementById("manualDice").value = data.dice
applyDice()
setTimeout(()=>{
startCountdown()
},1500)
}
lastStatus = data.status
}catch(e){
console.log("fetch error",e)
}
}
setInterval(updateDice,1000)
startCountdown()
</script>
</body>
</html>