#!/usr/bin/env python3 import json, random, string, time, hashlib, base64, struct, threading from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler from pathlib import Path from urllib.parse import urlparse, parse_qs ROOT = Path(__file__).resolve().parent GAMES = {"dice":{"name":"大话骰","max":12},"poker":{"name":"德州扑克","max":9},"six":{"name":"六张牌","max":8},"catch":{"name":"抓二游","max":8}} rooms = {} BOT_NAMES = ["阿杰","Mia","老陈","可乐","Luna","大熊","小白","阿豪"] def now_ms(): return int(time.time()*1000) def new_id(prefix="p"): return prefix + "".join(random.choice(string.ascii_lowercase+string.digits) for _ in range(10)) def room_code(): while True: code="".join(random.choice(string.ascii_uppercase+string.digits) for _ in range(4)) if code not in rooms: return code def make_player(name, host=False, player_id=None, bot=False, ready=None): if ready is None: ready = True if (host or bot) else False return {"id":player_id or new_id("b" if bot else "p"),"name":(name or "玩家")[:16],"host":host,"bot":bot,"ready":ready,"cups":0,"dice":[],"stats":{"splits":0,"counters":0,"declines":0,"cupsDrunk":0}} def find_player(room, player_id): return next((p for p in room["players"] if p["id"]==player_id), None) def require_player(room, player_id): p=find_player(room, player_id) if not p: raise ValueError("当前身份不在房间内") return p def require_host(room, player_id): p=require_player(room, player_id) if not p.get("host"): raise ValueError("只有房主可以操作") return p def current_player(room): if not room["players"]: return None room["turnIndex"]%=len(room["players"]) return room["players"][room["turnIndex"]] def min_count(room): return max(1,len(room["players"])) def max_count(room): return len(room["players"])*5 def call_mode(room,count,old=None,requested=None,point=None): count = int(count) if requested in ["斋","飞"]: if requested == "飞" and int(point or 0) == 1: return "飞" # 先保留请求,后续校验会明确拒绝:叫1不能飞 return requested if int(point or 0) == 1: return "斋" # 未显式选择时,叫 1 默认只能是斋 if not old: # 仅首叫按人数阈值判断:N、N+1 为斋,N+2 起飞 return "飞" if count >= min_count(room)+2 else "斋" if old.get("mode") == "飞": # 默认继续飞;若要平斋,由前端显式传 mode=斋 return "飞" # 斋局后续:未达到上一手数量 2 倍时仍是斋;达到 2 倍才破斋飞 return "飞" if count >= int(old["count"])*2 else "斋" def point_rank(mode, point): point = int(point) if mode == "斋": return 7 if point == 1 else point # 斋局里 1 最大:2<3<4<5<6<1 return point def count_actual(room,call): point=int(call["point"]); flying=call.get("mode")=="飞" return sum(1 for p in room["players"] for d in p["dice"] if d==point or (flying and point!=1 and d==1)) def is_valid_raise(room, old, count, point, mode): if not old: return True old_mode = old.get("mode", "斋") if old_mode == "斋": if mode == "飞": return count >= old["count"] * 2 # 继续斋:数量可增加;同数量时按斋点数顺序 2<3<4<5<6<1 if count > old["count"]: return True if count == old["count"]: return point_rank("斋", point) > point_rank("斋", old["point"]) return False # 上一手已飞 if mode == "斋": # 飞后切斋/平斋:不能降数量;同数量或更高数量都可进入斋 return count >= old["count"] # 继续飞:数量增加,或同数量更大点数 if count > old["count"]: return True if count == old["count"]: return int(point) > int(old["point"]) return False def next_call(room,old): if not old: return {"count":min_count(room),"point":1} if old.get("mode") == "斋": order=[2,3,4,5,6,1] idx=order.index(old["point"]) if old["point"] in order else 0 if idx < len(order)-1: return {"count":old["count"],"point":order[idx+1]} return {"count":old["count"]+1,"point":2} if old["point"]<6: return {"count":old["count"],"point":old["point"]+1} return {"count":old["count"]+1,"point":1} def recent_call_targets(room, count): seen=set(); items=[] for entry in reversed(room.get("roundCalls", [])): pid=entry["playerId"] if pid in seen: continue seen.add(pid) items.append(entry) if len(items)>=count: break return items def add_drink(player, cups): player["cups"] += cups player.setdefault("stats",{}).setdefault("cupsDrunk",0) player["stats"]["cupsDrunk"] += cups def visible_dice(room, player, viewer_id): if room["status"]=="result" and room.get("lastResult",{}).get("revealDice", True): return player["dice"] if room["status"]=="playing" and viewer_id and player["id"]==viewer_id: return player["dice"] return [] def public_room(room, viewer_id=None): cur=current_player(room) return {"code":room["code"],"game":room["game"],"gameName":GAMES[room["game"]]["name"],"maxPlayers":GAMES[room["game"]]["max"],"status":room["status"],"locked":room["locked"],"round":room["round"],"currentCall":room["currentCall"],"turnIndex":room["turnIndex"],"turnPlayerId":cur["id"] if cur else None,"turnPlayerName":cur["name"] if cur else None,"turnPlayerBot":bool(cur and cur.get("bot")),"pendingAction":room.get("pendingAction"),"lastResult":room["lastResult"],"updatedAt":room["updatedAt"],"history":room.get("history",[])[-16:],"settings":room.get("settings",{"cupsPerLoss":1}),"diceRule":{"minCount":min_count(room),"zhaiCounts":[min_count(room),min_count(room)+1],"flyFrom":min_count(room)+2,"maxCount":max_count(room),"zhaiPointOrder":[2,3,4,5,6,1],"breakZhai":"double"},"players":[{"id":p["id"],"name":p["name"],"host":p["host"],"bot":p["bot"],"ready":p["ready"],"cups":p["cups"],"stats":p.get("stats",{}),"dice":visible_dice(room,p,viewer_id)} for p in room["players"]]} def send_json(h,data,status=200): body=json.dumps(data,ensure_ascii=False).encode(); h.send_response(status); h.send_header("Content-Type","application/json; charset=utf-8"); h.send_header("Content-Length",str(len(body))); h.send_header("Access-Control-Allow-Origin","*"); h.end_headers(); h.wfile.write(body) def read_json(h): n=int(h.headers.get("Content-Length","0") or 0) return json.loads(h.rfile.read(n).decode() or "{}") if n else {} def append_history(room, typ, text, player="系统"): room.setdefault("history",[]).append({"type":typ,"player":player,"text":text,"at":now_ms()}) def transfer_host_if_needed(room): if any(p.get("host") for p in room["players"]): return human=next((p for p in room["players"] if not p.get("bot")), None) target=human or (room["players"][0] if room["players"] else None) if target: target["host"]=True; target["ready"]=True; append_history(room,"host",f"{target['name']} 成为新房主",target["name"]) def cleanup_room(code): room=rooms.get(code) if room and not any(not p.get("bot") for p in room["players"]): rooms.pop(code,None) return True return False def create_room(game,name,player_id=None): game=game if game in GAMES else "dice"; code=room_code(); player=make_player(name,True,player_id,ready=True) rooms[code]={"code":code,"game":game,"status":"waiting","locked":False,"round":0,"turnIndex":0,"currentCall":None,"pendingAction":None,"lastResult":None,"history":[],"roundCalls":[],"settings":{"cupsPerLoss":1},"players":[player],"updatedAt":now_ms()} append_history(rooms[code],"system",f"{player['name']} 创建房间",player["name"]) return rooms[code],player def join_room(code,name,player_id=None): code=(code or "").upper().strip(); room=rooms.get(code) if not room: raise ValueError("房间不存在") existed=find_player(room,player_id) if player_id else None if existed: existed["name"]=(name or existed["name"])[:16]; room["updatedAt"]=now_ms(); return room,existed,True same_name=next((p for p in room["players"] if not p["bot"] and p["name"]==(name or "玩家")[:16]),None) if same_name: room["updatedAt"]=now_ms(); return room,same_name,True if room["locked"]: raise ValueError("房间已暂停加入") if room["status"]!="waiting": raise ValueError("游戏已开始,暂不能加入;原玩家可重连回房") if len(room["players"])>=GAMES[room["game"]]["max"]: raise ValueError("房间已满") player=make_player(name,False,player_id,ready=False); room["players"].append(player); append_history(room,"join",f"{player['name']} 加入房间",player["name"]); room["updatedAt"]=now_ms(); return room,player,False def set_ready(code, player_id, ready=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["status"]=="playing": raise ValueError("游戏中不能切换准备状态") p=require_player(room,player_id) p["ready"] = (not p["ready"]) if ready is None else bool(ready) append_history(room,"ready",f"{p['name']} {'已准备' if p['ready'] else '取消准备'}",p["name"]) room["updatedAt"]=now_ms(); return room def add_bot(code, player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if player_id: require_host(room,player_id) if room["status"]=="playing": raise ValueError("游戏中不能添加机器人") if len(room["players"])>=GAMES[room["game"]]["max"]: raise ValueError("房间已满") used={p["name"] for p in room["players"]}; name=next((n for n in BOT_NAMES if n not in used), "机器人") bot=make_player(name,bot=True,ready=True); room["players"].append(bot); append_history(room,"bot",f"机器人 {name} 加入房间",name); room["updatedAt"]=now_ms(); return room def update_settings(code, player_id, cups_per_loss=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") require_host(room,player_id) if room["status"]=="playing": raise ValueError("游戏中不能修改规则") if cups_per_loss is not None: cups=int(cups_per_loss) if cups<1 or cups>5: raise ValueError("输家杯数必须是 1-5") room.setdefault("settings",{})["cupsPerLoss"]=cups append_history(room,"settings",f"输家惩罚改为 {cups} 杯") room["updatedAt"]=now_ms(); return room def start_room(code, player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if player_id: require_host(room,player_id) if room["status"]=="playing": raise ValueError("游戏进行中,不能重新开始") if len(room["players"])<2: raise ValueError("至少需要 2 人,房主可先添加机器人或等待玩家加入") not_ready=[p["name"] for p in room["players"] if not p.get("ready")] if not_ready: raise ValueError("还有玩家未准备:"+"、".join(not_ready)) room["status"]="playing"; room["round"]+=1; start_id=(room.get("lastResult") or {}).get("loserId") room["turnIndex"]=next((i for i,p in enumerate(room["players"]) if p["id"]==start_id),0) room["currentCall"]=None; room["pendingAction"]=None; room["lastResult"]=None; room["history"]=[]; room["roundCalls"]=[] for p in room["players"]: p["dice"]=[random.randint(1,6) for _ in range(5)] append_history(room,"start",f"第 {room['round']} 局开始") room["updatedAt"]=now_ms(); return room def validate_turn(room,player_id=None): p=current_player(room) if not p: raise ValueError("房间没有玩家") if player_id and p["id"]!=player_id: raise ValueError(f"还没轮到你,当前轮到 {p['name']}") return p def call_dice(code,player_id,count,point,requested_mode=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["game"]!="dice": raise ValueError("当前不是大话骰房间") if room["status"]!="playing": raise ValueError("游戏未开始或已结算,不能叫骰") actor=validate_turn(room,player_id); count,point=int(count),int(point) if point<1 or point>6: raise ValueError("点数必须是 1-6") if countmax_count(room): raise ValueError(f"数量不能超过总骰子数 {max_count(room)}") if requested_mode == "飞" and point == 1: raise ValueError("叫 1 只能是斋,不能叫飞") if not room["currentCall"] and count == min_count(room) and point != 1: raise ValueError(f"首叫最低数量时只能叫 {min_count(room)} 个 1") if not room["currentCall"] and count <= min_count(room)+1 and requested_mode == "飞": raise ValueError(f"首轮低数量阶段只能叫斋;{len(room['players'])} 人局前两档 {min_count(room)}、{min_count(room)+1} 个都不能直接叫飞") # 新规则校验见下方:斋局 1 最大,破斋必须翻倍 mode=call_mode(room,count,room["currentCall"],requested_mode,point) if room["currentCall"] and room["currentCall"].get("mode") == "斋" and mode == "飞" and count < room["currentCall"]["count"] * 2: old = room["currentCall"] raise ValueError(f"破斋需要翻倍:当前 {old['count']} 个 {old['point']} · 斋,至少要叫 {old['count']*2} 个任意点才能飞") if not is_valid_raise(room, room["currentCall"],count,point,mode): raise ValueError("叫点必须比上一手大;斋局点数顺序为 2<3<4<5<6<1,且破斋必须翻倍") room["currentCall"]={"count":count,"point":point,"mode":mode,"by":actor["name"],"byId":actor["id"]} room.setdefault("roundCalls",[]).append({"playerId":actor["id"],"playerName":actor["name"],"count":count,"point":point,"mode":mode,"order":len(room.get("roundCalls",[]))+1}) append_history(room,"call",f"{actor['name']} 叫 {count} 个 {point} · {mode}",actor["name"]) room["turnIndex"]=(room["turnIndex"]+1)%len(room["players"]); room["updatedAt"]=now_ms(); return room def finish_no_reveal(room, loser, cups, text): loser["cups"] += cups loser.setdefault("stats",{}).setdefault("cupsDrunk",0) loser["stats"]["cupsDrunk"] += cups room["status"] = "result" room["pendingAction"] = None room["lastResult"] = {"call": room.get("currentCall"), "actual": None, "opener": None, "openerId": None, "loser": loser["name"], "loserId": loser["id"], "cups": cups, "text": text, "revealDice": False} append_history(room, "decline", text, loser["name"]) room["updatedAt"] = now_ms() return room def resolve_open(room, opener, stake_cups, reason="开骰"): call = room["currentCall"] actual = count_actual(room, call) caller = find_player(room, call.get("byId")) or room["players"][0] loser = opener if actual >= call["count"] else caller loser["cups"] += stake_cups loser.setdefault("stats",{}).setdefault("cupsDrunk",0) loser["stats"]["cupsDrunk"] += stake_cups room["status"] = "result" room["pendingAction"] = None result_text = f"{opener['name']} {reason}:{call['mode']}局实际有 {actual} 个 {call['point']},{loser['name']} 喝 {stake_cups} 杯" room["lastResult"] = {"call": call, "actual": actual, "opener": opener["name"], "openerId": opener["id"], "loser": loser["name"], "loserId": loser["id"], "cups": stake_cups, "text": result_text, "revealDice": True} append_history(room, "open", result_text, opener["name"]) room["updatedAt"] = now_ms() return room def open_dice(code,player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["game"]!="dice": raise ValueError("当前不是大话骰房间") if room["status"]!="playing": raise ValueError("游戏未开始或已结算,不能开骰") if room.get("pendingAction"): raise ValueError("当前有待处理的开骰/劈") if not room["currentCall"]: raise ValueError("还没有人叫点,不能开骰") opener=validate_turn(room,player_id) caller=find_player(room,room["currentCall"].get("byId")) if not caller: raise ValueError("上一手叫骰玩家不存在") base=int(room.get("settings",{}).get("cupsPerLoss",1)) room["pendingAction"]={"type":"open","stage":"wait_caller_counter","initiatorId":opener["id"],"initiatorName":opener["name"],"targetId":caller["id"],"targetName":caller["name"],"baseCups":base,"call":room["currentCall"]} append_history(room,"challenge",f"{opener['name']} 要开骰,等待 {caller['name']} 是否反劈",opener["name"]) room["updatedAt"]=now_ms(); return room def split_dice(code,player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["game"]!="dice": raise ValueError("当前不是大话骰房间") if room["status"]!="playing": raise ValueError("游戏未开始或已结算,不能劈") if room.get("pendingAction"): raise ValueError("当前有待处理的开骰/劈") if not room["currentCall"]: raise ValueError("还没有人叫点,不能劈") opener=validate_turn(room,player_id) caller=find_player(room,room["currentCall"].get("byId")) if not caller: raise ValueError("上一手叫骰玩家不存在") base=int(room.get("settings",{}).get("cupsPerLoss",1)) opener.setdefault("stats",{}).setdefault("splits",0) opener["stats"]["splits"] += 1 room["pendingAction"]={"type":"split","stage":"wait_caller_accept","initiatorId":opener["id"],"initiatorName":opener["name"],"targetId":caller["id"],"targetName":caller["name"],"baseCups":base,"call":room["currentCall"]} append_history(room,"split",f"{opener['name']} 劈 {caller['name']},等待受/不受/反劈",opener["name"]) room["updatedAt"]=now_ms(); return room def respond_challenge(code, player_id, response): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") pending=room.get("pendingAction") if not pending: raise ValueError("当前没有待处理的开骰/劈") base=int(pending.get("baseCups",1)) initiator=find_player(room,pending["initiatorId"]) target=find_player(room,pending["targetId"]) if not initiator or not target: raise ValueError("挑战玩家不存在") stage=pending["stage"] if stage=="wait_caller_counter": if player_id!=target["id"]: raise ValueError(f"等待 {target['name']} 是否反劈") if response=="no_counter": return resolve_open(room,initiator,base,"开骰") if response=="counter": target.setdefault("stats",{}).setdefault("counters",0) target["stats"]["counters"] += 1 pending["stage"]="wait_opener_accept"; pending["counterType"]="open"; pending["declineCups"]=base; pending["acceptCups"]=base+1 append_history(room,"counter",f"{target['name']} 反劈,等待 {initiator['name']} 受/不受",target["name"]) room["updatedAt"]=now_ms(); return room if stage=="wait_caller_accept": if player_id!=target["id"]: raise ValueError(f"等待 {target['name']} 受/不受/反劈") if response=="decline": target.setdefault("stats",{}).setdefault("declines",0) target["stats"]["declines"] += 1 return finish_no_reveal(room,target,base,f"{target['name']} 不受,喝 {base} 杯,本局结束") if response=="accept": return resolve_open(room,initiator,base+1,"劈后开骰") if response=="counter": target.setdefault("stats",{}).setdefault("counters",0) target["stats"]["counters"] += 1 pending["stage"]="wait_opener_accept"; pending["counterType"]="split"; pending["declineCups"]=base+1; pending["acceptCups"]=base+2 append_history(room,"counter",f"{target['name']} 反劈,等待 {initiator['name']} 受/不受",target["name"]) room["updatedAt"]=now_ms(); return room if stage=="wait_opener_accept": if player_id!=initiator["id"]: raise ValueError(f"等待 {initiator['name']} 受/不受") if response=="decline": initiator.setdefault("stats",{}).setdefault("declines",0) initiator["stats"]["declines"] += 1 return finish_no_reveal(room,initiator,int(pending["declineCups"]),f"{initiator['name']} 不受,喝 {pending['declineCups']} 杯,本局结束") if response=="accept": return resolve_open(room,initiator,int(pending["acceptCups"]),"受反劈后开骰") raise ValueError("无效应答") def steal_split(code, player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["game"]!="dice": raise ValueError("当前不是大话骰房间") if room["status"]!="playing": raise ValueError("游戏未开始或已结算,不能抢开") if room.get("pendingAction"): raise ValueError("当前有待处理的开骰/劈") if not room["currentCall"]: raise ValueError("还没有人叫点,不能抢开") actor=require_player(room,player_id); caller=find_player(room,room["currentCall"].get("byId")); nxt=current_player(room) if not caller: raise ValueError("上一手叫骰玩家不存在") if actor["id"]==caller["id"]: raise ValueError("叫骰本人不能抢开自己") if nxt and actor["id"]==nxt["id"]: raise ValueError("下家可直接操作,不能算抢开") base=int(room.get("settings",{}).get("cupsPerLoss",1)) actor.setdefault("stats",{}).setdefault("splits",0) actor["stats"]["splits"] += 1 room["pendingAction"]={"type":"steal_split","stage":"wait_caller_accept","initiatorId":actor["id"],"initiatorName":actor["name"],"targetId":caller["id"],"targetName":caller["name"],"baseCups":base,"call":room["currentCall"]} append_history(room,"split",f"{actor['name']} 抢开 {caller['name']},等待受/不受/反劈",actor["name"]) room["updatedAt"]=now_ms(); return room def multi_outcome(initiator, target_names, drink_map): """统一多家结算展示数据:winners=未喝酒的参与者,drinks=各人喝杯明细。""" participants=[initiator['name']]+list(target_names) winners=[n for n in participants if drink_map.get(n,0)<=0] drinks=[{"name":n,"cups":c} for n,c in drink_map.items() if c>0] return winners, drinks def multi_calls(room, targets): """两家结算各参与者的叫点明细:已开骰的带实际数与是否成立,不受的实际数留空。""" out=[] for t in targets: call=t.get('call',{}) or {} resolved=t.get('status')=='resolve' actual=count_actual(room,call) if resolved and call else None win=(actual>=int(call['count'])) if resolved and call else None out.append({"name":t['playerName'],"count":call.get('count'),"point":call.get('point'),"mode":call.get('mode'),"actual":actual,"win":win}) return out def finalize_multi_direct(room, initiator, targets, base, label): outcomes=[] for t in targets: actual=count_actual(room,t) win = actual >= t["count"] outcomes.append({"target":t,"actual":actual,"win":win}) winners=[o for o in outcomes if o["win"]] losers=[o for o in outcomes if not o["win"]] drink_map={} def _drink(player, cups): add_drink(player, cups); drink_map[player['name']]=drink_map.get(player['name'],0)+cups if losers and not winners: per=base*2 for o in losers: _drink(find_player(room,o["target"]["playerId"]), per) top=losers[0]["target"] text=f"{initiator['name']} {label}:三家全输,三家各喝 {per} 杯" room["lastResult"]={"multi":True,"targets":[{"name":o['target']['playerName'],"actual":o['actual'],"win":o['win']} for o in outcomes],"loser":"三家","loserId":top['playerId'],"cups":per,"text":text,"revealDice":True} else: for o in losers: _drink(find_player(room,o["target"]["playerId"]), base) d_cups=base*len(winners) if d_cups: _drink(initiator,d_cups) loser_id = initiator['id'] if d_cups >= base and d_cups >= (base if losers else 0) else (losers[0]['target']['playerId'] if losers else initiator['id']) loser_name = initiator['name'] if loser_id==initiator['id'] else next(o['target']['playerName'] for o in losers if o['target']['playerId']==loser_id) text=f"{initiator['name']} {label}:三家中 {len(winners)} 家赢,{initiator['name']} 喝 {d_cups} 杯;{len(losers)} 家输,各喝 {base} 杯" room["lastResult"]={"multi":True,"targets":[{"name":o['target']['playerName'],"actual":o['actual'],"win":o['win']} for o in outcomes],"loser":loser_name,"loserId":loser_id,"cups":max(d_cups,base if losers else 0),"text":text,"revealDice":True} wl,dl=multi_outcome(initiator,[o['target']['playerName'] for o in outcomes],drink_map) room['lastResult']['winners']=wl; room['lastResult']['drinks']=dl room['lastResult']['calls']=[{"name":o['target']['playerName'],"count":o['target']['count'],"point":o['target']['point'],"mode":o['target'].get('mode','斋'),"actual":o['actual'],"win":o['win']} for o in outcomes] room["status"]='result'; room["pendingAction"]=None; append_history(room,'open',room['lastResult']['text'],initiator['name']); room['updatedAt']=now_ms(); return room def start_multi_direct(code, player_id, kind='open3'): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room['status']!='playing': raise ValueError('游戏未开始或已结算') if room.get('pendingAction'): raise ValueError('当前有待处理的开骰/劈') initiator=validate_turn(room,player_id) need=3 if kind.endswith('3') else 2 if need==3 and len(room['players'])<4: raise ValueError('开/劈三家仅限4人局或以上') if need==2 and len(room['players'])<3: raise ValueError('开/劈两家仅限3人局或以上') targets=recent_call_targets(room, need) if len(targets)= len(p['targets']): # 统一结算 reveal=False; summary=[]; top_loser=None; top_cups=-1; drink_map={} def _drink(player, cups): add_drink(player, cups); drink_map[player['name']]=drink_map.get(player['name'],0)+cups for t in p['targets']: if t['status']=='decline': player=find_player(room,t['playerId']); _drink(player,int(t['cups'])); summary.append(f"{player['name']} 不受,喝 {t['cups']} 杯") if t['cups']>top_cups: top_loser, top_cups = player, t['cups'] elif t['status']=='resolve': call=t['call']; actual=count_actual(room,call); caller=find_player(room,t['playerId']); loser=initiator if actual>=call['count'] else caller; _drink(loser,int(t['stake'])); reveal=True; summary.append(f"{caller['name']}:实际 {actual} 个 {call['point']},{loser['name']} 喝 {t['stake']} 杯") if int(t['stake'])>top_cups: top_loser, top_cups = loser, int(t['stake']) room['status']='result'; room['pendingAction']=None; room['lastResult']={"multi":True,"targets":[{"name":t['playerName'],"status":t['status']} for t in p['targets']],"loser":top_loser['name'] if top_loser else initiator['name'],"loserId":top_loser['id'] if top_loser else initiator['id'],"cups":top_cups if top_cups>0 else 0,"text":""} wl,dl=multi_outcome(initiator,[t['playerName'] for t in p['targets']],drink_map); room['lastResult']['winners']=wl; room['lastResult']['drinks']=dl room['lastResult']['calls']=multi_calls(room, p['targets']) room['lastResult']['text']=('开两家' if p['type']=='open2' else '劈两家')+':'+';'.join(summary) room['lastResult']['revealDice']=reveal append_history(room,'open',room['lastResult']['text'],initiator['name']); room['updatedAt']=now_ms(); return room p['currentIndex']=idx; room['updatedAt']=now_ms(); return room if p['stage']=='wait_initiator': if player_id!=initiator['id']: raise ValueError(f"等待 {initiator['name']} 受/不受") if response=='decline': initiator.setdefault('stats',{}).setdefault('declines',0); initiator['stats']['declines']+=1; target_info['status']='decline_initiator'; target_info['cups']=int(target_info['declineCups']) elif response=='accept': target_info['status']='resolve'; target_info['stake']=int(target_info['acceptStake']) else: raise ValueError('只能选择受/不受') p['stage']='wait_target'; p['currentIndex']=idx+1 if p['currentIndex'] >= len(p['targets']): summary=[]; reveal=False; top_loser=None; top_cups=-1; drink_map={} def _drink(player, cups): add_drink(player, cups); drink_map[player['name']]=drink_map.get(player['name'],0)+cups for t in p['targets']: if t['status'] in ['decline','decline_initiator']: loser=find_player(room,initiator['id'] if t['status']=='decline_initiator' else t['playerId']); _drink(loser,int(t['cups'])); summary.append(f"{loser['name']} 不受,喝 {t['cups']} 杯") if int(t['cups'])>top_cups: top_loser, top_cups = loser, int(t['cups']) elif t['status']=='resolve': call=t['call']; actual=count_actual(room,call); caller=find_player(room,t['playerId']); loser=initiator if actual>=call['count'] else caller; _drink(loser,int(t['stake'])); reveal=True; summary.append(f"{caller['name']}:实际 {actual} 个 {call['point']},{loser['name']} 喝 {t['stake']} 杯") if int(t['stake'])>top_cups: top_loser, top_cups = loser, int(t['stake']) room['status']='result'; room['pendingAction']=None; room['lastResult']={"multi":True,"targets":[{"name":t['playerName'],"status":t['status']} for t in p['targets']],"loser":top_loser['name'] if top_loser else initiator['name'],"loserId":top_loser['id'] if top_loser else initiator['id'],"cups":top_cups if top_cups>0 else 0,"text":""} wl,dl=multi_outcome(initiator,[t['playerName'] for t in p['targets']],drink_map); room['lastResult']['winners']=wl; room['lastResult']['drinks']=dl room['lastResult']['calls']=multi_calls(room, p['targets']) room['lastResult']['text']=('开两家' if p['type']=='open2' else '劈两家')+':'+';'.join(summary) room['lastResult']['revealDice']=reveal append_history(room,'open',room['lastResult']['text'],initiator['name']); room['updatedAt']=now_ms(); return room room['updatedAt']=now_ms(); return room raise ValueError('无效两家应答') def bot_turn(code): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if room["status"]!="playing": return room pending=room.get("pendingAction") if pending: if pending.get("type") in ["open2","split2"]: idx=int(pending.get("currentIndex",0)); targets=pending.get("targets",[]) if idx < len(targets): actor_id = targets[idx]["playerId"] if pending["stage"]=="wait_target" else pending["initiatorId"] actor=find_player(room,actor_id) if actor and actor.get("bot"): if pending["stage"]=="wait_target": if pending['type']=='open2': return respond_multi(code,actor_id,'no_counter' if random.random()<0.72 else 'counter') return respond_multi(code,actor_id,'accept' if random.random()<0.55 else ('decline' if random.random()<0.78 else 'counter')) if pending["stage"]=="wait_initiator": return respond_multi(code,actor_id,'accept' if random.random()<0.62 else 'decline') return room actor_id = pending["targetId"] if pending["stage"] in ["wait_caller_counter","wait_caller_accept"] else pending["initiatorId"] actor=find_player(room,actor_id) if actor and actor.get("bot"): if pending["stage"]=="wait_caller_counter": return respond_challenge(code,actor_id,"no_counter" if random.random()<0.75 else "counter") if pending["stage"]=="wait_caller_accept": return respond_challenge(code,actor_id,"accept" if random.random()<0.55 else ("decline" if random.random()<0.75 else "counter")) if pending["stage"]=="wait_opener_accept": return respond_challenge(code,actor_id,"accept" if random.random()<0.65 else "decline") return room bot=current_player(room) if not bot or not bot.get("bot"): return room if room["currentCall"]: actual=count_actual(room,room["currentCall"]); pressure=room["currentCall"]["count"]-actual if room["currentCall"]["count"]>=min_count(room)+2: if pressure>0 and random.random()<0.55: return open_dice(code,bot["id"]) if random.random()<0.16: return open_dice(code,bot["id"]) if room["currentCall"]["count"]>=max_count(room): return open_dice(code,bot["id"]) n=next_call(room,room["currentCall"]) if n["count"]>max_count(room): return open_dice(code,bot["id"]) return call_dice(code,bot["id"],n["count"],n["point"]) def toggle_lock(code,player_id=None): room=rooms.get((code or "").upper()) if not room: raise ValueError("房间不存在") if player_id: require_host(room,player_id) room["locked"]=not room["locked"]; append_history(room,"lock","房间已暂停加入" if room["locked"] else "房间已开放加入"); room["updatedAt"]=now_ms(); return room def leave_room(code, player_id): code=(code or "").upper(); room=rooms.get(code) if not room: raise ValueError("房间不存在") if room["status"]=="playing": raise ValueError("游戏中不能退出,请等待本局结算") p=require_player(room,player_id); room["players"]=[x for x in room["players"] if x["id"]!=player_id] append_history(room,"leave",f"{p['name']} 离开房间",p["name"]); transfer_host_if_needed(room); room["turnIndex"]=0; room["updatedAt"]=now_ms(); deleted=cleanup_room(code); return None if deleted else room def kick_player(code, host_id, target_id): code=(code or "").upper(); room=rooms.get(code) if not room: raise ValueError("房间不存在") require_host(room,host_id) if room["status"]=="playing": raise ValueError("游戏中不能踢人") if host_id==target_id: raise ValueError("不能踢自己,请使用退出房间") target=find_player(room,target_id) if not target: raise ValueError("目标玩家不在房间") room["players"]=[p for p in room["players"] if p["id"]!=target_id] append_history(room,"kick",f"{target['name']} 被房主移出房间",target["name"]); transfer_host_if_needed(room); room["turnIndex"]=0; room["updatedAt"]=now_ms(); return room # ---------------- WebSocket(纯标准库实现 RFC6455)---------------- WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" state_lock = threading.RLock() # 保护 rooms / ws_clients 的并发修改 ws_clients = {} # cid -> {"sock","code","player_id","send_lock"} def recv_exact(sock, n): buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: return None buf += chunk return buf def ws_read_frame(sock): """读取一帧,返回 (opcode, payload);连接关闭或异常返回 (None, None)。客户端帧带掩码,需解掩。""" head = recv_exact(sock, 2) if not head: return None, None opcode = head[0] & 0x0f masked = head[1] & 0x80 length = head[1] & 0x7f if length == 126: ext = recv_exact(sock, 2) if not ext: return None, None length = struct.unpack(">H", ext)[0] elif length == 127: ext = recv_exact(sock, 8) if not ext: return None, None length = struct.unpack(">Q", ext)[0] mask = recv_exact(sock, 4) if masked else b"\x00\x00\x00\x00" if mask is None: return None, None payload = recv_exact(sock, length) if length else b"" if payload is None: return None, None if masked: payload = bytes(payload[i] ^ mask[i % 4] for i in range(len(payload))) return opcode, payload def ws_build_frame(payload, opcode=0x1): data = payload if isinstance(payload, bytes) else payload.encode() n = len(data) header = bytes([0x80 | opcode]) if n < 126: header += bytes([n]) elif n < 65536: header += bytes([126]) + struct.pack(">H", n) else: header += bytes([127]) + struct.pack(">Q", n) return header + data def ws_send(client, obj): frame = ws_build_frame(json.dumps(obj, ensure_ascii=False)) try: with client["send_lock"]: client["sock"].sendall(frame) return True except Exception: return False def ws_handshake(handler): key = handler.headers.get("Sec-WebSocket-Key") if not key: return False accept = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode() resp = ("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" "Connection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n") try: handler.connection.sendall(resp.encode()); return True except Exception: return False def room_clients(code): code = (code or "").upper() with state_lock: return [c for c in ws_clients.values() if c.get("code") == code] def lobby_clients(): with state_lock: return [c for c in ws_clients.values() if not c.get("code")] def broadcast_room(code): """向房内所有连接推送各自视角的房间状态;房间已删除则推 closed。""" code = (code or "").upper(); room = rooms.get(code) for c in room_clients(code): if room: ws_send(c, {"type": "room", "room": public_room(room, c.get("player_id"))}) else: ws_send(c, {"type": "closed"}) def broadcast_rooms(): payload = {"type": "rooms", "rooms": [public_room(r) for r in rooms.values()]} for c in lobby_clients(): ws_send(c, payload) def schedule_bots(code): threading.Timer(0.9, lambda: run_bots(code)).start() def run_bots(code): """服务端驱动机器人:推进一步,若有变化则广播并继续排程,直到轮到真人或本局结束。""" code = (code or "").upper(); changed = False with state_lock: room = rooms.get(code) if room and room["status"] == "playing": before = room["updatedAt"] try: bot_turn(code) except Exception as e: print("bot error:", e) changed = rooms.get(code, {}).get("updatedAt") != before if changed: broadcast_room(code); schedule_bots(code) # 推进游戏进程、需触发机器人接续的动作 BOT_TRIGGER = {"start","call","open","split","steal","multi","respond","respondMulti"} def handle_ws(client, msg): t = msg.get("type") try: acted = None; room = None with state_lock: if t == "rooms": ws_send(client, {"type": "rooms", "rooms": [public_room(r) for r in rooms.values()]}); return elif t == "create": room, player = create_room(msg.get("game", "dice"), msg.get("name", "玩家"), msg.get("playerId")) client["code"] = room["code"]; client["player_id"] = player["id"]; acted = room["code"] ws_send(client, {"type": "joined", "room": public_room(room, player["id"]), "player": player}) elif t == "join": room, player, reused = join_room(msg.get("code", ""), msg.get("name", "玩家"), msg.get("playerId")) client["code"] = room["code"]; client["player_id"] = player["id"]; acted = room["code"] ws_send(client, {"type": "joined", "room": public_room(room, player["id"]), "player": player, "reused": reused}) else: code = client.get("code"); pid = client.get("player_id") if not code: ws_send(client, {"type": "error", "error": "尚未加入房间"}); return acted = code if t == "ready": room = set_ready(code, pid, msg.get("ready")) elif t == "start": room = start_room(code, pid) elif t == "call": room = call_dice(code, pid, msg.get("count", 1), msg.get("point", 1), msg.get("mode")) elif t == "open": room = open_dice(code, pid) elif t == "split": room = split_dice(code, pid) elif t == "steal": room = steal_split(code, pid) elif t == "multi": room = start_multi_direct(code, pid, msg.get("kind", "open2")) elif t == "respond": room = respond_challenge(code, pid, msg.get("response", "")) elif t == "respondMulti": room = respond_multi(code, pid, msg.get("response", "")) elif t == "settings": room = update_settings(code, pid, msg.get("cupsPerLoss")) elif t == "lock": room = toggle_lock(code, pid) elif t == "kick": room = kick_player(code, pid, msg.get("targetId")) elif t == "addBot": room = add_bot(code, pid) elif t == "leave": room = leave_room(code, pid); client["code"] = None; client["player_id"] = None ws_send(client, {"type": "left"}) else: ws_send(client, {"type": "error", "error": "未知消息"}); return if acted: broadcast_room(acted); broadcast_rooms() if t in BOT_TRIGGER: schedule_bots(acted) except Exception as e: ws_send(client, {"type": "error", "error": str(e)}) def ws_serve(handler): sock = handler.connection cid = new_id("w") client = {"sock": sock, "code": None, "player_id": None, "send_lock": threading.Lock()} with state_lock: ws_clients[cid] = client ws_send(client, {"type": "rooms", "rooms": [public_room(r) for r in rooms.values()]}) try: while True: opcode, payload = ws_read_frame(sock) if opcode is None or opcode == 0x8: break # 关闭 if opcode == 0x9: # ping -> pong try: with client["send_lock"]: sock.sendall(ws_build_frame(payload, 0xA)) except Exception: break continue if opcode == 0xA: continue # pong if opcode != 0x1: continue # 仅处理文本帧 try: data = json.loads(payload.decode() or "{}") except Exception: continue handle_ws(client, data) finally: with state_lock: ws_clients.pop(cid, None) class Handler(SimpleHTTPRequestHandler): def __init__(self,*a,**kw): super().__init__(*a,directory=str(ROOT),**kw) def log_message(self,fmt,*a): print("[%s] %s"%(self.log_date_time_string(),fmt%a)) def end_headers(self): # 禁用静态文件缓存,避免浏览器用旧版 live.html/JS/CSS(WS 握手走 sendall,不经此处) self.send_header("Cache-Control","no-cache, no-store, must-revalidate") super().end_headers() def do_OPTIONS(self): self.send_response(204); self.send_header("Access-Control-Allow-Origin","*"); self.send_header("Access-Control-Allow-Methods","GET, POST, OPTIONS"); self.send_header("Access-Control-Allow-Headers","Content-Type"); self.end_headers() def do_GET(self): p=urlparse(self.path); qs=parse_qs(p.query) if p.path=="/ws" and self.headers.get("Upgrade","").lower()=="websocket": self.close_connection=True if ws_handshake(self): ws_serve(self) return if p.path=="/api/rooms": send_json(self,{"ok":True,"rooms":[public_room(r) for r in rooms.values()]}); return if p.path=="/api/room": code=qs.get("code",[""])[0].upper(); viewer=qs.get("playerId",[None])[0]; room=rooms.get(code) send_json(self,{"ok":bool(room),"room":public_room(room,viewer) if room else None,"error":None if room else "房间不存在"},200 if room else 404); return return super().do_GET() def do_POST(self): try: p=urlparse(self.path); d=read_json(self); viewer=d.get("playerId") if p.path=="/api/create": room,player=create_room(d.get("game","dice"),d.get("name","玩家"),viewer); send_json(self,{"ok":True,"room":public_room(room,player["id"]),"player":player}); return if p.path=="/api/join": room,player,reused=join_room(d.get("code",""),d.get("name","玩家"),viewer); send_json(self,{"ok":True,"room":public_room(room,player["id"]),"player":player,"reused":reused}); return if p.path=="/api/ready": send_json(self,{"ok":True,"room":public_room(set_ready(d.get("code",""),viewer,d.get("ready",None)),viewer)}); return if p.path=="/api/add-bot": send_json(self,{"ok":True,"room":public_room(add_bot(d.get("code",""),viewer),viewer)}); return if p.path=="/api/settings": send_json(self,{"ok":True,"room":public_room(update_settings(d.get("code",""),viewer,d.get("cupsPerLoss",None)),viewer)}); return if p.path=="/api/leave": room=leave_room(d.get("code",""),viewer); send_json(self,{"ok":True,"room":public_room(room,viewer) if room else None,"deleted":room is None}); return if p.path=="/api/kick": send_json(self,{"ok":True,"room":public_room(kick_player(d.get("code",""),viewer,d.get("targetId")),viewer)}); return if p.path=="/api/start": send_json(self,{"ok":True,"room":public_room(start_room(d.get("code",""),viewer),viewer)}); return if p.path=="/api/call": send_json(self,{"ok":True,"room":public_room(call_dice(d.get("code",""),viewer,d.get("count",1),d.get("point",1),d.get("mode",None)),viewer)}); return if p.path=="/api/open": send_json(self,{"ok":True,"room":public_room(open_dice(d.get("code",""),viewer),viewer)}); return if p.path=="/api/split": send_json(self,{"ok":True,"room":public_room(split_dice(d.get("code",""),viewer),viewer)}); return if p.path=="/api/steal-split": send_json(self,{"ok":True,"room":public_room(steal_split(d.get("code",""),viewer),viewer)}); return if p.path=="/api/multi-open2": send_json(self,{"ok":True,"room":public_room(start_multi_direct(d.get("code",""),viewer,'open2'),viewer)}); return if p.path=="/api/multi-split2": send_json(self,{"ok":True,"room":public_room(start_multi_direct(d.get("code",""),viewer,'split2'),viewer)}); return if p.path=="/api/multi-open3": send_json(self,{"ok":True,"room":public_room(start_multi_direct(d.get("code",""),viewer,'open3'),viewer)}); return if p.path=="/api/multi-split3": send_json(self,{"ok":True,"room":public_room(start_multi_direct(d.get("code",""),viewer,'split3'),viewer)}); return if p.path=="/api/respond-challenge": send_json(self,{"ok":True,"room":public_room(respond_challenge(d.get("code",""),viewer,d.get("response","")),viewer)}); return if p.path=="/api/respond-multi": send_json(self,{"ok":True,"room":public_room(respond_multi(d.get("code",""),viewer,d.get("response","")),viewer)}); return if p.path=="/api/bot-turn": send_json(self,{"ok":True,"room":public_room(bot_turn(d.get("code","")),viewer)}); return if p.path=="/api/toggle-lock": send_json(self,{"ok":True,"room":public_room(toggle_lock(d.get("code",""),viewer),viewer)}); return send_json(self,{"ok":False,"error":"未知接口"},404) except Exception as e: send_json(self,{"ok":False,"error":str(e)},400) if __name__=="__main__": create_room("dice","演示房主") port=8765 print(f"酒桌派对开发服务器:http://127.0.0.1:{port}/live.html (WebSocket: /ws)") server=ThreadingHTTPServer(("0.0.0.0",port),Handler) server.daemon_threads=True server.serve_forever()