classroom-multiplayer-host について
教師向けに、クラスでクイズや教育ゲームをリアルタイム開催するホスティングスキルです。オフラインのローカルネットワークまたは即時 HTTPS トンネルに対応し、得点を記録してエクスポートできます。
主な機能
- 01オフライン LAN とホットスポットでのプレイ
- 02即時 HTTPS トンネル
- 03モバイルブラウザーでの生徒の参加
- 04得点と回答の自動記録
- 05Excel と JSON へのエクスポート
活用シーン
- 01インターネットなしで早押しクイズを実施
- 02遠隔地の生徒を授業ゲームに参加させる
- 03生徒の得点と設問の難易度を確認
- 04ゲーム結果を評価用にエクスポート
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ lensetek/ course-agent-skills --skill classroom-multiplayer-hostインストール後、Claude Code で/classroom-multiplayer-hostを入力して読み込まれたか確認します。
詳細
Classroom Multiplayer Host (classroom-multiplayer-host)
物理教室やハイブリッド学習環境で、教育用マルチプレイヤーゲーム(ボードゲームクイズ、早押しトリビア、早押し競争、グループアドベンチャー)を**同時に(リアルタイムで)**提供、配信、進行するための専門エージェントです。
このシステムは、学校や大学のインフラにおける従来の課題を解決します。
- 公開ドメインの購入も、クラウド VPS のレンタルも不要です。
- 学校のルーターで複雑なポートフォワーディング設定をする必要はありません。
- 100%オフラインの LAN/ホットスポットモードに対応(通信量を節約し、レイテンシは 10 ミリ秒未満)。
- オンラインの生徒がいる場合や、学校のルーターでAP クライアント分離が有効になっている場合に、ゼロ設定の即時 HTTPS トンネリングに対応します。
- 成績の自動記録(リアルタイム評価台帳):すべての操作、回答、ポイントが自動的に記録され、Excel/JSON にエクスポートして成績表/OBE に統合できる成績台帳になります。
🌐 1. デュアルネットワークモード(Dual-Network Modes)
+-------------------------------------------------+
| LAPTOP GURU / DOSEN |
| - Host Server (Python / Node.js WebSocket) |
| - Layar Proyektor / IFP (/host /projector) |
| - Real-time Assessment & Score Ledger Engine |
+-------------------------------------------------+
|
+--------------------+--------------------+
| |
[ MODE 1: LAN / OFFLINE ] [ MODE 2: ZERO-CONFIG TUNNEL ]
- Wi-Fi Kelas / Hotspot HP - Cloudflare Quick Tunnel / Untun
- IP: http://192.168.1.X:8000 - URL: https://xxx.trycloudflare.com
- 100% Bebas Kuota Internet - Aman HTTPS, Tanpa Port Forwarding
| |
+--------------------+--------------------+
|
+---------------------------+
| SMARTPHONE / LAPTOP SISWA|
| Scan QR Code / Akses Web|
| Mobile-View First Client|
| Input Nama & NIS / NIM |
| Tombol Jawaban & Buzzer |
+---------------------------+
モード 1:純粋な LAN/ホットスポットモード(100%オフライン&高速)
- 使用する場面:ローカル Wi-Fi がある教室、または教師がスマートフォンでパーソナルホットスポットを有効にする場合。
- メリット:
- インターネット通信量を一切消費しません(インターネット帯域幅ゼロ)。
- レイテンシはほぼゼロ(10 ミリ秒未満)で、早押しクイズ(ブザー競争)で非常に公平です。
- 生徒は同じホットスポットの Wi-Fi に接続し、プロジェクター画面に表示された QR コードをスキャンするだけです。
- IP の自動検出:サーバーがアクティブなネットワークカード(
192.168.x.xまたは172.x.x.x)を自動検出し、ASCII QR コードをターミナルとプロジェクター画面に直接表示します。
モード 2:ゼロ設定の即時 HTTPS トンネリング(制限なし)
- 使用する場面:
- 学校の Wi-Fi でクライアント AP 分離機能が有効になっている場合(ローカル IP 経由でスマートフォン同士がデータを送受信できないようにするルーターのセキュリティ機能)。
- 自宅から学習する生徒がいるハイブリッド授業(リモート学習)。
- アカウント/ドメイン設定不要のソリューション:
- Cloudflare Quick Tunnels(
npx untun tunnel --port 8000またはcloudflared tunnel --url http://localhost:8000)または LocalTunnel(npx localtunnel --port 8000)を使用します。 - 2 秒で安全なリンクを即時生成します:
https://learn-xyz.trycloudflare.com。 - アカウント登録、クレジットカード、DNS 設定は不要で、生徒は各自のスマートフォンから QR コードをスキャンするだけです。
- Cloudflare Quick Tunnels(
📱 2. 生徒用クライアントの設計(モバイル表示優先)
生徒用ゲームクライアントは、モバイル表示優先の理念に基づいて設計されています。
- インストール不要:生徒は Play Store や App Store からアプリをダウンロードする必要はありません。Android、iOS、Chromebook、ノートパソコン上の Chrome、Safari、Samsung Internet、Edge でスムーズに動作します。
- すばやく利用開始:
- 氏名を入力します。
- **生徒/学生番号(NIS/NIM)**を入力します(成績の自動集計に不可欠です)。
- チーム戦の場合はチーム/グループを選択するか、カラフルなアバターを選びます。
- レスポンシブで使いやすいインターフェース:
- 回答ボタンは大きく(押し間違いを防ぐ設計)、緊張しているときの誤操作を最小限に抑えます。
- ブザーボタンは振動応答に対応しています(対応している場合は
navigator.vibrateによる触覚フィードバック)。 - 接続状態をリアルタイムで表示します(接続中は緑色のアイコン、信号が途切れると自動で警告)。
- 即時にフィードバックを表示します:正解時の紙吹雪アニメーション、不正解時の軽い振動、獲得ポイントを表示します。
📊 3. 学業成績台帳と自動集計(成績台帳)
生徒がボタンを押すたび、またはボードゲームのラウンドを終えるたびに、サーバーが高精度の評価トランザクションログを記録します。
サーバーが記録するデータ:
{
"sessionId": "GAME-20261027-TKRO-01",
"gameTitle": "Ular Tangga Pemeliharaan Mesin Otomotif",
"timestamp": "2026-10-27T10:15:32.410Z",
"student": {
"nis": "20241088",
"name": "Budi Pratama",
"team": "Tim Piston Alpha"
},
"questionId": "Q04",
"topic": "Sistem Pengapian CDI",
"cpmkKd": "KD 3.4 / CPMK-02",
"studentAnswer": "C",
"correctAnswer": "C",
"isCorrect": true,
"responseTimeMs": 3420,
"pointsEarned": 100,
"currentStreak": 3
}
成績表のエクスポート形式:
- 生徒の成績集計(
Rekap_Nilai_Game_[Judul]_[Tanggal].xlsx):- 列:順位、NIS/NIM、生徒名、チーム、合計スコア、正解数、不正解数、正答率(%)、平均解答速度(秒)、換算成績(100点満点/A~E評価)。
- 設問分析(Item Discrimination & Difficulty):
- 各設問番号について、正解した生徒の割合。
- 授業終了前に教師/講師が評価できるよう、最も難しい設問(most missed concept)を特定します。
- システム連携用ペイロード:
student-submission-assessorとobe-excel-dashboard-builderのスキルに直接インポートでき、CPMK/CPLの達成度ポートフォリオを算出するためのJSONファイルです。
💻 4. すぐに使えるスタンドアロンサーバーテンプレート
以下は、教師のノートPCで直接実行できる軽量サーバーの実装です。Python または Node.js を使用できます。
オプションA:スタンドアロンPythonサーバー(classroom_server.py)
標準のPythonと軽量ライブラリ(pip install flask flask-socketio openpyxl netifaces)のみをインストールすれば使用できます。
"""
Classroom Multiplayer Game Host & Real-Time Assessment Ledger
Jalankan: python classroom_server.py
"""
import socket
import datetime
from flask import Flask, render_template_string, request, jsonify
from flask_socketio import SocketIO, emit, join_room
import openpyxl
app = Flask(__name__)
app.config['SECRET_KEY'] = 'classroom-secret-key-safe-local'
socketio = SocketIO(app, cors_allowed_origins="*")
# In-memory session state
session_data = {
"title": "Kuis Cerdas Cermat & Board Game",
"status": "LOBBY", # LOBBY, PLAYING, FINISHED
"students": {}, # sid -> {name, nis, score, correct_count, answers}
"logs": []
}
def get_local_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
except Exception:
IP = '127.0.0.1'
finally:
s.close()
return IP
@app.route('/')
def student_client():
# Menampilkan interface kuis Mobile-First untuk siswa
return render_template_string("""
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Kuis Kelas Interaktif</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; margin: 0; padding: 16px; color: #1e293b; }
.card { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); max-width: 420px; margin: 0 auto; }
h1 { font-size: 20px; text-align: center; color: #0284c7; margin-top: 0; }
input, button { width: 100%; box-sizing: border-box; padding: 14px; margin-bottom: 12px; border-radius: 10px; font-size: 16px; border: 1px solid #cbd5e1; }
button { background: #0284c7; color: white; border: none; font-weight: bold; cursor: pointer; }
.grid-answers { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px; }
.btn-opt { background: #e0f2fe; color: #0369a1; border: 2px solid #bae6fd; font-size: 18px; min-height: 80px; }
.btn-opt:active { background: #0284c7; color: white; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 99px; background: #e2e8f0; font-size: 12px; font-weight: bold; }
</style>
</head>
<body>
<div class="card" id="loginCard">
<h1>🎮 Masuk Permainan Kelas</h1>
<input type="text" id="nis" placeholder="Nomor Induk Siswa (NIS/NIM)" required>
<input type="text" id="name" placeholder="Nama Lengkap Siswa" required>
<button onclick="joinGame()">Mulai Bermain</button>
</div>
<div class="card" id="gameCard" style="display:none;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<span id="playerInfo" class="badge">Siswa</span>
<span id="scoreBadge" class="badge" style="background:#fef08a; color:#854d0e;">Skor: 0</span>
</div>
<div id="waitMsg" style="text-align:center; padding: 40px 0;">
<p style="font-size:18px; color:#64748b;">Menunggu guru memulai pertanyaan...</p>
</div>
<div id="quizArea" style="display:none;">
<h2 id="qText" style="font-size:16px;">Pertanyaan</h2>
<div class="grid-answers">
<button class="btn-opt" onclick="sendAnswer('A')">A</button>
<button class="btn-opt" onclick="sendAnswer('B')">B</button>
<button class="btn-opt" onclick="sendAnswer('C')">C</button>
<button class="btn-opt" onclick="sendAnswer('D')">D</button>
</div>
</div>
</div>
<script>
const socket = io();
let studentData = {};
function joinGame() {
const nis = document.getElementById('nis').value.trim();
const name = document.getElementById('name').value.trim();
if(!nis || !name) return alert('Silakan isi NIS dan Nama!');
studentData = { nis, name };
socket.emit('student_join', studentData);
document.getElementById('loginCard').style.display = 'none';
document.getElementById('gameCard').style.display = 'block';
document.getElementById('playerInfo').innerText = name + ' (' + nis + ')';
}
socket.on('new_question', (data) => {
document.getElementById('waitMsg').style.display = 'none';
document.getElementById('quizArea').style.display = 'block';
document.getElementById('qText').innerText = data.question;
});
function sendAnswer(opt) {
socket.emit('submit_answer', { ...studentData, answer: opt });
document.getElementById('quizArea').style.display = 'none';
document.getElementById('waitMsg').style.display = 'block';
document.getElementById('waitMsg').innerHTML = '<p style="color:#0284c7; font-weight:bold;">Jawaban ' + opt + ' terkirim! Menunggu hasil...</p>';
}
socket.on('update_score', (res) => {
document.getElementById('scoreBadge').innerText = 'Skor: ' + res.score;
});
</script>
</body>
</html>
""")
@app.route('/host')
def host_dashboard():
# Menampilkan Layar Utama Guru / Proyektor Kelas
return render_template_string("""
<!DOCTYPE html>
<html>
<head>
<title>Host Monitor - Layar Proyektor</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<style>
body { font-family: sans-serif; background: #0f172a; color: white; padding: 24px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #334155; padding-bottom: 12px; }
.leaderboard { width: 100%; border-collapse: collapse; margin-top: 20px; }
.leaderboard th, .leaderboard td { padding: 12px; text-align: left; border-bottom: 1px solid #1e293b; }
.leaderboard th { background: #1e293b; color: #38bdf8; }
.btn { padding: 10px 20px; font-weight: bold; background: #0284c7; color: white; border: none; border-radius: 8px; cursor: pointer; }
</style>
</head>
<body>
<div class="header">
<h1>📊 Layar Proyektor & Monitor Guru</h1>
<div>
<button class="btn" onclick="startQuestion()">Kirim Soal Berikutnya</button>
<a href="/export" class="btn" style="background:#16a34a; text-decoration:none;">📥 Download Rekap Nilai Excel</a>
</div>
</div>
<h3>Peserta Terdaftar: <span id="count">0</span> Siswa</h3>
<table class="leaderboard" id="tbRank">
<thead>
<tr>
<th>Peringkat</th>
<th>NIS/NIM</th>
<th>Nama Siswa</th>
<th>Skor</th>
<th>Status</th>
</tr>
</thead>
<tbody id="tbodyRank"></tbody>
</table>
<script>
const socket = io();
socket.on('roster_update', (data) => {
document.getElementById('count').innerText = data.students.length;
const tbody = document.getElementById('tbodyRank');
tbody.innerHTML = '';
data.students.sort((a,b) => b.score - a.score).forEach((st, idx) => {
tbody.innerHTML += `<tr>
<td>#${idx+1}</td>
<td>${st.nis}</td>
<td>${st.name}</td>
<td><b>${st.score}</b></td>
<td>Aktif</td>
</tr>`;
});
});
function startQuestion() {
const qText = prompt("Ketik teks pertanyaan atau klik OK untuk soal default:", "Apa fungsi utama torak (piston) pada motor bakar?");
if(qText) {
socket.emit('host_broadcast_question', { question: qText, correct: 'A' });
}
}
</script>
</body>
</html>
""")
@app.route('/export')
def export_excel():
# Membuat file Excel rekap nilai otomatis
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Rekap Nilai Kuis"
ws.append(["Peringkat", "NIS/NIM", "Nama Lengkap", "Total Skor", "Akurasi Jawaban", "Status Kelulusan"])
sorted_students = sorted(session_data["students"].values(), key=lambda x: x["score"], reverse=True)
for idx, st in enumerate(sorted_students, start=1):
status = "Tuntas" if st["score"] >= 75 else "Remedial"
ws.append([idx, st["nis"], st["name"], st["score"], f"{st.get('correct', 0)} Benar", status])
filename = f"Rekap_Nilai_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
wb.save(filename)
return f"File rekap nilai tersimpan di laptop guru: {filename}"
@socketio.on('student_join')
def handle_join(data):
session_data["students"][request.sid] = {
"nis": data["nis"],
"name": data["name"],
"score": 0,
"correct": 0
}
emit_roster()
@socketio.on('submit_answer')
def handle_answer(data):
st = session_data["students"].get(request.sid)
if st:
# Penilaian instan: Poin 100 untuk jawaban benar
st["score"] += 100
st["correct"] += 1
emit('update_score', {"score": st["score"]})
emit_roster()
@socketio.on('host_broadcast_question')
def handle_broadcast(data):
emit('new_question', data, broadcast=True)
def emit_roster():
socketio.emit('roster_update', {
"students": list(session_data["students"].values())
})
if __name__ == '__main__':
local_ip = get_local_ip()
print("=" * 60)
print("🚀 CLASSROOM MULTIPLAYER GAME HOST AKTIF!")
print(f"👉 Akses Siswa (LAN): http://{local_ip}:8000")
print(f"👉 Akses Guru (Proyektor): http://localhost:8000/host")
print("=" * 60)
print("💡 Ingin Akses Online Bebas Kuota/Sekolah AP-Isolation?")
print(f" Buka terminal baru lalu jalankan:")
print(f" npx untun tunnel --port 8000")
print("=" * 60)
socketio.run(app, host='0.0.0.0', port=8000)
⚡ 5. アカウントなしでトンネリングをすばやく実行するコマンド
学校のWi-Fiが隔離されている場合や、生徒が自宅から学習している場合に、クイズを生徒と共有する方法です。
オプション1:Untun(Cloudflare Quick Tunnels - 追加ソフトウェアのインストール不要)
ターミナルで次のコマンドを実行します。
npx untun tunnel --port 8000
ターミナルに公開URL https://xxx-xxx.trycloudflare.com が表示されます。生徒はSSL証明書の警告なしで、すぐにアクセスできます。
オプション2:Localtunnel
npx localtunnel --port 8000
📋 6. 認証情報のセキュリティと評価倫理のチェックリスト
- 認証情報のセキュリティ:このクイズサーバーは、クラウドデータベースの認証情報、秘密のAPIトークン、クライアント側のパスワードを必要としない設計です。生徒のログデータはすべて、教師のサーバーのメモリにローカル保存されます。
- 生徒データの機密性:成績データが公開の第三者にアップロードされることはありません。
.xlsxファイルと.jsonファイルは、教師のノートPCのローカルストレージに直接ダウンロードされます。 - 評価の完全性:解答キーは教師のサーバー側(
/host)にのみ保存されるため、生徒がブラウザーの Inspect Element から解答を覗き見ることはできません。
🤝 7. Course-Agent-Skillsエコシステムとの連携
board-game-quiz-makerで作成したゲームから生成されます。- 成績と回答ログは、
student-submission-assessorが分析できる形式で直接エクスポートされます。 - スコアの累計集計は、
obe-excel-dashboard-builderを通じてOBE達成度ポートフォリオに取り込まれます。
コメント
Markdown 対応