mcpField
C
classroom-multiplayer-host

classroom-multiplayer-host

github.com/lensetek其他011 次安装1 天前更新
代码仓库

关于 classroom-multiplayer-host

面向教师的课堂游戏托管技能,可实时运行多人测验和教育游戏。支持离线局域网或即时 HTTPS 隧道,并记录成绩以供导出。

主要功能

  1. 01离线局域网与热点游戏
  2. 02即时 HTTPS 隧道
  3. 03基于移动浏览器的学生游戏
  4. 04自动记录成绩与作答
  5. 05导出 Excel 和 JSON

适用场景

  1. 01无互联网时开展抢答测验
  2. 02让远程学生参与课堂游戏
  3. 03查看学生成绩与题目难度
  4. 04导出游戏结果用于评估

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lensetek/course-agent-skills --skill classroom-multiplayer-host

安装后在 Claude Code 中输入/classroom-multiplayer-host确认已加载。

详细介绍

课堂多人游戏主持端(classroom-multiplayer-host)

专用 Agent,用于在实体课堂或混合式学习环境中,**同步(实时)**呈现、分发并引导教育类多人游戏(棋盘游戏问答、知识竞赛、抢答比赛和团队冒险)。

该系统解决了学校/校园基础设施的常见难题:

  1. 无需购买公共域名或租用云 VPS。
  2. 无需在学校路由器上进行复杂的端口转发配置。
  3. 支持 100% 离线局域网/热点模式(节省流量,延迟 < 10 ms)。
  4. 支持零配置即时 HTTPS 隧道,适用于有学生在线学习或学校路由器启用了 AP 客户端隔离的情况。
  5. 自动记录成绩(实时评估账本):每次交互、回答和得分都会自动保存为成绩册,可导出为 Excel/JSON,以便整合到成绩单/OBE 中。

🌐 1. 双网络架构(双网络模式)

               +-------------------------------------------------+
               |             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:纯局域网/热点模式(100% 离线且快速)
  • 适用场景:在使用本地 Wi-Fi 的教室,或教师在智能手机上开启个人热点时。
  • 优势:
    • 完全不消耗互联网流量(零互联网带宽)。
    • 即时低延迟(<10 毫秒),非常适合抢答竞赛(抢答器比赛)。
    • 学生只需连接到同一 Wi-Fi 热点,并扫描投影屏幕上的二维码。
  • 自动检测 IP:服务自动检测活动网卡(192.168.x.x 或 172.x.x.x),并直接在终端和投影屏幕上打印 ASCII 二维码。
模式 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,学生只需用各自的智能手机扫描二维码即可。

📱 2. 学生客户端设计(移动端优先)

面向学生的游戏客户端遵循移动端优先的设计理念:

  1. 零安装:学生无需从 Play Store 或 App Store 下载应用。在 Android、iOS、Chromebook 和笔记本电脑上的 Chrome、Safari、Samsung Internet、Edge 中均可流畅运行。
  2. 快速入门:
    • 输入全名。
    • 输入学生/大学生学号(NIS/NIM)(对自动汇总成绩至关重要)。
    • 选择队伍/小组(团队游戏时)或彩色头像。
  3. 响应式人体工学界面:
    • 答题按钮尺寸较大(方便手指操作),尽量避免紧张时误触。
    • 抢答器按钮支持振动反馈(如果支持 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
}
成绩册导出格式:
  1. 学生成绩汇总(Rekap_Nilai_Game_[Judul]_[Tanggal].xlsx):
    • 列:排名、NIS/NIM、学生姓名、队伍、总分、正确答案数、错误答案数、准确率(%)、平均答题速度(秒)、换算成绩(百分制 / A-E 等级)。
  2. 试题分析(区分度与难度):
    • 每道题的答对学生百分比。
    • 识别最难的题目(最常答错的概念),供教师/讲师在课程结束前评估。
  3. 系统集成载荷:
    • 可直接导入 student-submission-assessor 和 obe-excel-dashboard-builder 技能的即用型 JSON 文件,用于计算 CPMK/CPL 达成情况的作品集。

💻 4. 即用型独立服务器模板

以下是轻量服务器的实现,可由教师直接在笔记本电脑上使用 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. 凭据安全与评估规范检查清单

  1. 凭据安全:此测验服务器无需云数据库凭据、机密 API 令牌或客户端密码。所有学生日志数据都保存在教师服务器的内存中。
  2. 学生数据保密:成绩数据不会上传至公共第三方;.xlsx 和 .json 文件会直接下载到教师笔记本电脑的本地存储介质。
  3. 评估完整性:答案仅存储在教师服务器端(/host),因此学生无法通过浏览器的检查元素查看答案。

🤝 7. 与 Course-Agent-Skills 生态系统集成

  • 由 board-game-quiz-maker 创建的游戏生成。
  • 成绩和回答日志会直接导出,供 student-submission-assessor 分析。
  • 累计分数汇总会通过 obe-excel-dashboard-builder 纳入 OBE 达成情况作品集。

评论

支持 Markdown