目录
一、游戏规则
我们对规则也进行了一些改动,因为我们认为负分在游戏中体现给用户是不良好的体验,因此我们设定加分初始值为0分,只有最后一名玩家不能加分,而第一名玩家能够加N+2分,其他玩家每轮加2分,这样就可以在不改变规则的情况下避免负分的情况。
N 个玩家,每人写 2 个 0~100 之间的有理数 (不包括 0 或 100 ) ,提交给服务器,服务器在当前回合结束时算出所有数字的 平均值 ,然后乘以 0.618(所谓黄金分割常数),得到 G 值。提交的数字最靠近 G(取绝对值)的玩家得到 N+2 分,离 G 最远的玩家得到 0 分,其他玩家得 2 分。
二、实现方法
由于需要实现以下两个功能,因此我们考虑利用CSS和JS制作成能够登录的BS交互设计。
后端:基于websocket的PHP
前端:bootstrap框架,自适应
为了实现多用户参与,我们的想法是类似一般的桌面游戏中房间的功能。
用户可以自定义自己的用户名,以及创建房间号。其他用户可以在大厅中看到这个房间,或者利用房间号进入相应的房间。
待所有用户进入后,房主选择开始游戏。
每个用户有10秒钟的时间输入自己的数字,大家也可以看到上一轮的黄金点数字,方便选择下一轮的数字。
每一轮结束后系统计算每个玩家的得分,并且显示在房间中。
前端被访问时会自动调用server.php,如果已经在运行则会调用运行的实例,否则新建实例
三、HTML代码
以下是对主要网页的设定。
HTML称为超文本标记语言,是一种标识性的语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的Internet资源连接为一个逻辑整体。HTML文本是由HTML命令组成的描述性文本,HTML命令可以说明文字,图形、动画、声音、表格、链接等。 [1]
超文本是一种组织信息的方式,它通过超级链接方法将文本中的文字、图表与其他信息媒体相关联。这些相互关联的信息媒体可能在同一文本中,也可能是其他文件,或是地理位置相距遥远的某台计算机上的文件。这种组织信息方式将分布在不同位置的信息资源用随机方式进行连接,为人们查找,检索信息提供方便。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>GamePoint</title>
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/swiper.min.css">
<script language='javascript' src="js/jquery-3.3.1.min.js"></script>
<script language='javascript' src="js/bootstrap.min.js"></script>
<script language='javascript' src="js/gamemain.js"></script>
<style>
.wdheig{
width:100%;
height:100%;
}
</style>
</head>
<body class="backgroundColor3" style="background: url(static/1.jpg) no-repeat;background-size: cover;">
<nav class="navbar navbar-expand-lg navbar-light">
<!--button的位置设计-->
<a class="navbar-brand text-center w-100 color5 " href="#" style="color: #0037ff;font-family: 'Trebuchet MS', Helvetica, sans-serif;font-size: 4em;" ;="">GoldenPoint-Game
<button class="navbar-toggler backgroundColor4" style="float:right;" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</a>
</nav>
<div class="container-fluid main">
<div id='rooms' class="container h-100">
<div class="input-group col-md-6 m-auto pt-2">
<span style="color: #121111;margin-top: 5px;">请修改玩家名:</span>
<input id='input_name' type="text" class="form-control" placeholder="请输入您的名称">
<div class="input-group-append">
<button style="margin-left: 8px;" id='confirm_name' class="btn btn-outline-secondary color5" type="button" >确定</button>
</div>
</div>
<div class="input-group col-md-6 m-auto pt-2 container" style="border-radius: 10px;">
<span style="color: #000;margin-top: 5px;">请输入加入ID:</span>
<input id='input_room' type="number" class="form-control col-ms-6" placeholder="请输入要加入的房间ID">
<button id='join' class="btn btn-outline-secondary color5" type="button" style="margin-left: 8px;" >加入</button>
</div>
<div id="buttonBox" class="input-group col-md-6 m-auto pt-2 container" style="display: flex;">
<button id='create' class="btn btn-outline-secondary color5" type="button" >开始游戏</button>
<button id="refresh" class="btn btn-outline-secondary color5" type="button">游戏列表</button>
</div>
<div class="collapse" id="collapseExample">
<div class="card card-body">
<p>这是一个多人游戏</p>
<p>N个同学,每人写一个0~100之间的有理数,交给裁判,裁判算出所有数字的平均值,然后乘以0.618(所谓黄金分割常数),得到G值。提交的数字最靠近G(取绝对值)的同学得到N+2分,离G最远的同学得到0分,其他同学得2分。
</p>
</div>
</div>
<div id='roomCards' class="rooms col-md-10 m-auto pt-2 row">
</div>
</div>
<!--前端插件swiper-->
<div id='game' class="swiper-container" hidden="true" style="width:100%;height:100%; margin: 0; padding: 0;">
<div class=" swiper-wrapper" style="height:100%;">
<div id="gameleft" style="border-right-style: none;" class="col-md-2 col-ms-2 pl-left my-redius backgroundColor2 color5 border4 swiper-slide" >
<p class="p-1">游戏状态</p>
<h5 id='state'>未开始</h5>
<hr class="bordertop4"/>
<h5 id='round' class="p-1">局数</h5>
<hr class="bordertop4"/>
<div id='players'>
</div>
</div>
<div id="gameright" style="border-left-style:dashed;" class="col-md-2 col-ms-2 pl-right my-redius backgroundColor2 color4 border4 swiper-slide">
<div class="h-75 my-redius" >
<h4 class="p-1">这一轮剩下的时间</h4>
<h1 id='time' class="p-1">10</h1>
<img style="width: 100px;" src="static/3.gif" alt="">
<hr class="bordertop4"/>
<h4 class="p-1">上一轮的黄金点是</h4>
<img style="width: 100px;" src="static/4.gif" alt="">
<h1 id='point' class="p-1">0</h1>
</div>
<div class="h-25 pl-right-down backgroundColor2 color4">
<button id='start' class="btn btn-success m-1" type="button" style="margin-left:0;">开始游戏</button>
<button id='quit' class="btn btn-warning m-1" type="button">退出游戏</button>
</div>
</div>
<div id="gamecenter" class="col-md-8 col-ms-8 pl-center my-redius backgroundColor3 color4 border4 swiper-slide" >
<div class="pl-center-up my-redius color2 backgroundColore" style=" border:1px solid black;">
<div id="msg_list" class="msg h-100 my-redius"></div>
</div>
<div class="pl-center-down my-redius backgroundColor3">
<div class="input-group mb-3">
<input id='input_number' type="number" class="form-control" placeholder="请输入0~100的数字(有理数)">
<div class="input-group-append">
<button id='confirm' class="btn btn-outline-secondary color4" type="button">确定</button>
</div>
</div>
</div>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</div>
<img id="winImg" src="static/">
<script src="js/swiper.min.js"></script>
<!-- Swiper插件安装 -->
<script>
var swiper = new Swiper('.swiper-container', {
pagination: {
el: '.swiper-pagination',
},
});
</script>
</body>
</html>
利用bootstrap自动生成的前端框架(仅展示一部分)
--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;
四、JS代码
已在代码中加入注释
// 存储用户名到全局变量,握手成功后发送给服务器
var uname = '玩家' + uuid(8, 16);
var maxScoreMan = null
var time = 0;
var pasdTime = 0;
$.ajax({
'type':'GET',
'url':'/server/server.php',
'timeout':1000
});
var ws = new WebSocket("ws://192.168.1.1:8585");
ws.onopen = function() {
};
ws.onerror = function() {
alert("系统消息 : 出错了,不能连接到服务器,请退出重试.");
};
//服务器返回信息
ws.onmessage = function(e) {
var msg = JSON.parse(e.data);
console.log("Receive:" + e.data);
switch(msg.type) {
case 'handshake':
refresh();
break;
case 'create':
if(msg.success) {
join(msg.room);
} else {
alert(msg.reason);
}
break;
case 'join':
if(msg.success) {
listMsg("您加入的游戏 id:" + msg.content.id);
$('#rooms').attr('hidden', 'true');
$('#game').removeAttr('hidden');
} else {
alert(msg.reason);
}
break;
case 'info':
listMsg(msg.content);
break;
case 'room':
var room = msg.room;
var players = msg.players;
$('#round').text('目前进行到第 ' + room.round + ' 轮' );
$('#point').text(room.point);
time = room.time_left;
pasdTime = 0;
$('#players').html('');
players.forEach(function(x) {
if(x.uname == uname){
$('#players').append('<p>' + x.uname + ' [这是你]目前的分数:' + x.score + '</p>');
}
else {
$('#players').append('<p>' + x.uname + ' 目前的分数:' + x.score + '</p>');
}
if(x.uname == uname) {
$('#score').text('你的分数:' + x.score);
}
});
var state = '';
if(room.ended)
state = '游戏已经结束了';
else if(room.started)
state = '游戏正在进行';
else state = '游戏尚未开始';
$('#state').text(state);
break;
case 'lobby':
var rooms = msg.content;
$('#roomCards').html('');
rooms.forEach(function(x) {
var state = '';
if(x.ended){
state = '游戏已经结束';
if(maxScoreMan && maxScoreMan.uname == uname) {
document.querySelector('#winImg').style.display='block'
setTimeout(()=>{
document.querySelector('#winImg').style.display='none'
maxScoreMan = null
},2000)
}
}else if(x.started)
state = '游戏已经开始';
else state = '游戏未开始';
$('#roomCards').append('<div class="col-md-5 style="height:50%;margin-left:80px;border-radius:80px;margin-top:80px;background-color:#ffee23;"><div class="card" style="background-color:#ffee23;border:1px solid #ffee23;"><div class="card-body"style="backgroun-color:#ffee23;"><div class="card"><div class="card-body"><h5 class="card-title">游戏 id:' + x.id +' [' +state+']</h5>' +
'<p class="card-text">游戏人数:' + x.person + '/' + 10 + '</p>' +
'<p class="card-text">当前轮数:' + x.round + '/' + x.max_round + '</p>' +
'<a href="javascript:join(' + x.id + ')" class="btn btn-primary" style="background:#ffee23;border:1px solid #ffee23;color:black;">加入</a></div></div></div>');
});
break;
}
};
//bind
$(document).ready(function() {
$('#input_name').val(uname);
$("#confirm").on('click', function() {
var num = $('#input_number').val();
if(!isNaN(num) && num != '') {
sendMsg({
'type': 'num',
'content': num
});
} else {
alert("请输入数字!");
}
$('#input_number').val('');
});
$('#input_number').keyup(function(event) {
if(event.keyCode == 13) {
$("#confirm").trigger("click");
}
});
$("#start").on('click', function() {
sendMsg({
'type': 'start',
'content': ""
});
});
$("#refresh").on('click', function() {
refresh();
});
$("#confirm_name").on('click', function() {
var name = $('#input_name').val();
if(name == '') {
alert('名称不能为空!');
} else {
uname = name;
}
});
$("#create").on('click', function() {
var create = {
'type': 'create',
'content': ''
};
sendMsg(create);
});
$("#quit").on('click', function() {
var leave = {
'type': 'leave',
'content': ''
};
sendMsg(leave);
$('#msg_list').html('');
$('#game').attr('hidden', 'true');
$('#rooms').removeAttr('hidden');
});
$("#join").on('click', function() {
var num = $('#input_room').val();
if(!isNaN(num) && num != '') {
join(Number(num));
} else {
alert("请输入数字!");
}
});
setInterval("timeRefresh()", 100);
setInterval("refresh()", 3000);
});
function timeRefresh() {
pasdTime += 0.1;
if(time - pasdTime <= 0) {
$('#time').text('0');
} else {
$('#time').text(Math.floor(time - pasdTime));
}
}
function join(id) {
sendMsg({
'type': 'join',
'content': {
'room': id,
'uname': uname
}
});
}
function refresh(id) {
sendMsg({
'type': 'lobby',
'content': ''
});
}
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p");
msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
}
//随机ID生成
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [],
i;
radix = radix || chars.length;
if(len) {
for(i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for(i = 0; i < 36; i++) {
if(!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
//数据发送
function sendMsg(msg) {
var data = JSON.stringify(msg);
console.log("send:" + data);
ws.send(data);
}
/*监听代码的改动 */
$(window).resize(function() {
var width = $(this).width();
var height = $(this).height();
if(width<400){
$("#gameleft").removeClass("col-md-2");
$("#gamecenter").removeClass("col-md-8");
$("#gameright").removeClass("col-md-2");
$("#gameleft").addClass("wdheig");
$("#gamecenter").addClass("wdheig");
$("#gameright").addClass("wdheig");
}else{
$("#gameleft").removeClass("wdheig");
$("#gamecenter").removeClass("wdheig");
$("#gameright").removeClass("wdheig");
$("#gameleft").addClass("col-md-2");
$("#gamecenter").addClass("col-md-8");
$("#gameright").addClass("col-md-2");
}
});
五、运行截图
游戏主界面
玩家1建立了房间,并且一个人进行游戏
可以建立多个房间,显示房间内人数及轮次
可以查看游戏说明
六、压力测试
因为还没有放上服务器,因此采用本地多开网页进行测试。
测试截图如下:
可以在大厅中看到其他房间,用户可以选择加入房间。
可以看到其他用户加入了房间
一轮游戏以后,各用户的得分显示
目前由于没有在互联网中测试,暂时不知道最大用户并发量是多少。
七、代码结构展示
后文
PHP服务器设置
<?php
error_reporting(E_ALL);
set_time_limit(0);// 设置超时时间为无限,防止超时
date_default_timezone_set('Asia/shanghai');
class WebSocket {
const LOG_PATH = '';
const LISTEN_SOCKET_NUM = 9;
private $rooms=[];
private $sockets = [];
private $master;
//当前最大的房间id
private $max_room_id=0;
//过去的时间
private $deltaTime=0;
//上次的时间戳
private $lastTime=0;
private $oneSecond=0;
public function __construct($host, $port) {
if(!$this->checkPortBindable($host,$port))
die('create error');
try {
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 设置IP和端口重用,在重启服务器后能重新使用此端口;
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1);
// 将IP和端口绑定在服务器socket上;
socket_bind($this->master, $host, $port);
// listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
socket_listen($this->master, self::LISTEN_SOCKET_NUM);
//设置异步非阻塞
socket_set_nonblock($this->master);
} catch (Exception $e) {
$err_code = socket_last_error();
$err_msg = socket_strerror($err_code);
$this->error([
'error_init_server',
$err_code,
$err_msg
]);
die('create error');
}
$this->sockets[0] = ['resource' => $this->master];
$pid = function_exists('posix_getpid')?posix_getpid():get_current_user();
$this->debug(["server: {$this->master} started ,pid: {$pid}"]);//
while (true) {
try {
$this->doServer();
$this->doGame();
} catch (Exception $e) {
echo 'error';
$this->error([
'error_do_server',
$e->getCode(),
$e->getMessage()
]);
}
}
}
private function checkPortBindable($host, $port, &$errno=null, &$errstr=null)
{
$socket = stream_socket_server("tcp://$host:$port", $errno, $errstr);
if (!$socket) {
return false;
}
fclose($socket);
unset($socket);
return true;
}
private function doServer() {
$write = $except = NULL;
$sockets = array_column($this->sockets, 'resource');
$read_num = socket_select($sockets, $write, $except, 1);
// select作为监视函数,参数分别是(监视可读,可写,异常,超时时间),返回可操作数目,出错时返回false;
if (false === $read_num) {
$this->error([
'error_select',
$err_code = socket_last_error(),
socket_strerror($err_code)
]);
return;
}
foreach ($sockets as $socket) {
// 如果可读的是服务器socket,则处理连接逻辑
if ($socket == $this->master) {
$client = socket_accept($this->master);
// 创建,绑定,监听后accept函数将会接受socket要来的连接,一旦有一个连接成功,将会返回一个新的socket资源用以交互,如果是一个多个连接的队列,只会处理第一个,如果没有连接的话,进程将会被阻塞,直到连接上.如果用set_socket_blocking或socket_set_noblock()设置了阻塞,会返回false;返回资源后,将会持续等待连接。
if (false === $client) {
$this->error([
'err_accept',
$err_code = socket_last_error(),
socket_strerror($err_code)
]);
continue;
} else {
self::connect($client);
continue;
}
} else {
// 如果可读的是其他已连接socket,则读取其数据,并处理应答逻辑
$bytes = @socket_recv($socket, $buffer, 2048, 0);
if ($bytes < 9) {
$this->disconnect($socket);
} else {
if (!$this->sockets[(int)$socket]['handshake']) {
self::handShake($socket, $buffer);
continue;
} else {
$recv_msg = self::parse($buffer);
}
array_unshift($recv_msg, 'receive_msg');
self::dealMsg($socket, $recv_msg);
}
}
}
}
public function doGame(){
//刷新delta时间
if($this->lastTime==0)
$this->lastTime=floatval(microtime(true));
else{
$time=floatval(microtime(true));
$this->deltaTime=$time-$this->lastTime;
$this->lastTime=$time;
$this->oneSecond+=$this->deltaTime;
if($this->oneSecond<1)
{
return;
}
$this->deltaTime=$this->oneSecond;
$this->oneSecond=0;
}
foreach ($this->rooms as &$game)
{
#$this->debug($game);
if($game['started']!==true||$game['ended']!==false)
continue;
$game['time_left']-=$this->deltaTime;
if($game['time_left']<0)
{
//----------游戏逻辑
$sumValue=0;
$sum=0;
$room_players=&$this->gerRoomPlayers($game['id']);
foreach ($room_players as $p)
{
$this->debug($p);
if($p['num']!=-1)
{
$sum++;
$sumValue+=$p['num'];
}else
{
$this->sendRoomInfo($game['id'],$p['uname'].' 未在时间内做出决定,将在此局不算入');
}
}
if($sum>0)
{
$game['point']=$sumValue/$sum*0.618;
reset($room_players);
$closet_player=current($room_players);
$farst_player=current($room_players);
foreach ($room_players as $p)
{
if(abs($p['num']-$game['point'])<abs($closet_player['num']-$game['point']))
$closet_player=$p;
else if(abs($p['num']-$game['point'])>abs($farst_player['num']-$game['point']))
$farst_player=$p;
}
foreach ($room_players as &$p)
{
$p['num']=-1;
if($p['uname']==$closet_player['uname'])
$p['score']+=($sum+2);
else if($p['uname']==$farst_player['uname'])
$p['score']+=0;
else
$p['score']+=2;
}
unset($p);
$this->sendRoomInfo($game['id'],$closet_player['uname'].' 是本局的胜利玩家,加'.($sum+2).'分');
if($sum>1)
$this->sendRoomInfo($game['id'],$farst_player['uname'].' 是本局离黄金点最远的玩家,不加分');
if($sum>2)
$this->sendRoomInfo($game['id'],'其他玩家各加2分!');
}else
{
$game['point']=0;
}
//----------游戏逻辑结束
$game['time_left']=$game['time_one'];
//增加round
$game['round']++;
if($game['round']>$game['max_round'])
{
$game['round']--;
$game['ended']=true;
$this->sendRoomInfo($game['id'],'所有'.$game['max_round'].'局已经结束!');
}else{
$this->sendRoomInfo($game['id'],'第'.$game['round'].'局开始');
}
$this->debug($game);
$this->refreshRoom($game['id']);
}
}
//清除一个很容易犯的bug
unset($game);
}
/**
* 将socket添加到已连接列表,但握手状态留空;
*
* @param $socket
*/
public function connect($socket) {
socket_getpeername($socket, $ip, $port);
$socket_info = [
'resource' => $socket,
'uname' => '',
'handshake' => false,
'ip' => $ip,
'port' => $port,
'score'=>-1,
'room'=>-1,
'admin'=> false,
'num'=>-1
];
$this->sockets[(int)$socket] = $socket_info;
//$this->debug(array_merge(['socket_connect'], $socket_info));
//$this->debug(['id:'.(string)(int)$socket]);
}
private function disconnect($socket) {
$this-> leaveRoom($this->sockets[(int)$socket]);
unset($this->sockets[(int)$socket]);
}
/**
* 用公共握手算法握手
*
* @param $socket
* @param $buffer
*
* @return bool
*/
public function handShake($socket, $buffer) {
// 获取到客户端的升级密匙
$line_with_key = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
$key = trim(substr($line_with_key, 0, strpos($line_with_key, "\r\n")));
// 生成升级密匙,并拼接websocket升级头
$upgrade_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));// 升级key的算法
$upgrade_message = "HTTP/1.1 101 Switching Protocols\r\n";
$upgrade_message .= "Upgrade: websocket\r\n";
$upgrade_message .= "Sec-WebSocket-Version: 13\r\n";
$upgrade_message .= "Connection: Upgrade\r\n";
$upgrade_message .= "Sec-WebSocket-Accept:" . $upgrade_key . "\r\n\r\n";
socket_write($socket, $upgrade_message, strlen($upgrade_message));// 向socket里写入升级信息
$this->sockets[(int)$socket]['handshake'] = true;
socket_getpeername($socket, $ip, $port);
$this->debug([
'hand_shake',
$socket,
$ip,
$port
]);
// 向客户端发送握手成功消息,以触发客户端发送用户名动作;
$msg = [
'type' => 'handshake',
'content' => 'done',
];
$msg = $this->build(json_encode($msg));
socket_write($socket, $msg, strlen($msg));
return true;
}
/**
* 解析数据
*
* @param $buffer
*
* @return bool|string
*/
private function parse($buffer) {
$decoded = '';
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return json_decode($decoded, true);
}
/**
* 将普通信息组装成websocket数据帧
*
* @param $msg
*
* @return string
*/
private function build($msg) {
$frame = [];
$frame[0] = '81';
$len = strlen($msg);
if ($len < 126) {
$frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
} else if ($len < 65025) {
$s = dechex($len);
$frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
} else {
$s = dechex($len);
$frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
}
$data = '';
$l = strlen($msg);
for ($i = 0; $i < $l; $i++) {
$data .= dechex(ord($msg{$i}));
}
$frame[2] = $data;
$data = implode('', $frame);
return pack("H*", $data);
}
private function &gerRoomPlayers($id)
{
$playerlist=[];
foreach ($this->sockets as &$socket) {
if ($socket['resource'] == $this->master) {
continue;
}
if($socket['room'] ==$id)
{
$playerlist[]=&$socket;
}
}
return $playerlist;
}
private function refreshRoom($id)
{
if(!isset($this->rooms[$id]))
return;
$playerlist=$this-> gerRoomPlayers($id);
$playerInfos=[];
foreach ($playerlist as $player) {
unset($player['num']);
unset($player['port']);
unset($player['ip']);
unset($player['resource']);
unset($player['handshake']);
$playerInfos[]=$player;
}
foreach ($playerlist as $player) {
$this->sendPack($player,['type' => 'room',"room"=>$this->rooms[$id],"players"=>$playerInfos]);
}
}
private function sendPack($socket,$response)
{
$msg=$this->build(json_encode($response));
socket_write($socket['resource'], $msg, strlen($msg));
//$this->debug(array_merge(['send:'],$response));
}
private function sendRoomInfo($roomId,$info)
{
if(isset($this->rooms[$roomId]))
{
$playerlist=$this->gerRoomPlayers($roomId);
foreach ($playerlist as $player) {
$this->sendInfo($player,$info);
}
}
}
private function sendInfo($player,$info)
{
$response=[];
$response['type'] = 'info';
$response['content'] = $info;
$this->sendPack($player,$response);
}
private function sendLobby($player)
{
$response=[];
$response['type'] = 'lobby';
$response['content'] = [];
foreach ($this->rooms as $r) {
unset($r['point']);
unset($r['time_one']);
unset($r['time_left']);
$response['content'][]=$r;
}
$this->sendPack($player,$response);
}
private function leaveRoom(&$player)
{
if($player['room']!=-1)
{
$player['admin']=false;
$room=&$this->rooms[$player['room']];
$player['room']=-1;
$room['person']--;
$ps=$this->gerRoomPlayers($room['id']);
if($room['person']>0)
{
$hasAdmin=false;
foreach ($ps as $p)
{
if($p['admin'])
{
$hasAdmin=true;
break;
}
}
$this->sendRoomInfo($room['id'],$player['uname']." 离开了房间");
if(!$hasAdmin)
{
$ps[0]['admin']=true;
$this->sendRoomInfo($room['id'],'现在 '.$player['uname']." 是房主了");
}
$this->refreshRoom($room['id']);
$this->debug(array_merge(['playerLeft'],$player));
}else{
unset($this->rooms[$room['id']]);
}
}
}
private function dealMsg($player, $recv_msg) {
$msg_type = $recv_msg['type'];
$content = $recv_msg['content'];
$response = [];
//将int转为结构体
$player=&$this->sockets[(int)$player];
switch ($msg_type) {
//创建房间
case 'create':
$response['type'] = 'create';
$response['success'] = false;
//如果玩家已经加入了其他房间则不能创建房间
if($player['room']!=-1)
{
$response['reason'] = '您已经加入了其他房间不能再创建房间了!';
$this->sendPack($player,$response);
return;
}
$response['success'] = true;
//创建房间
$room_info = [
'id' => ++$this->max_room_id,
'point'=>0,
'round'=>1,
'max_round'=>10,
'time_left'=>10,
'time_one'=>15,
'started'=>false,
'ended'=>false,
'person'=>0
];
$this->rooms[$room_info['id']]=$room_info;
$player['room']=$room_info['id'];
$player['admin']=true;
$response['room'] = $room_info['id'];
$this->sendPack($player,$response);
$this->debug(array_merge(['roomCreated'],$room_info));
return;
case 'join':
$response['type'] = 'join';
$response['success'] = false;
$content['room']=intval($content['room']);
if(!isset($this->rooms[$content['room']]))
{
$response['reason'] = '该房间不存在!';
$this->sendPack($player,$response);
return;
}
$room=&$this->rooms[$content['room']];
if($room['person']>=10)
{
$response['reason'] = '人数已满!';
$this->sendPack($player,$response);
return;
}
if($room['started']===true)
{
$response['reason'] = '房间已经开始游戏!';
$this->sendPack($player,$response);
return;
}
if($room['ended']===true)
{
$response['reason'] = '房间游戏已经结束!';
$this->sendPack($player,$response);
return;
}
#防止刷管理员
if($player['room']!=$room['id'])
$player['admin']=false;
if($player['admin']==false)
foreach ($this->gerRoomPlayers($room['id']) as $p)
{
if($p['uname']==$content['uname'])
{
$response['reason'] = $p['uname'].' 你的名字在该房间已经存在!';
$this->sendPack($player,$response);
return;
}
}
$response['success'] = true;
$response['content']=$room;
$room['person']++;
$room['time_left']=0;
$player['room']=$content['room'];
$player['uname']=$content['uname'];
$player['score']=0;
$this->sendPack($player,$response);
$this->sendRoomInfo($player['room'],$player['uname']." 加入游戏");
$this->refreshRoom($player['room']);
break;
case 'leave':
$this-> leaveRoom($player);
break;
case 'start':
if($player['admin']!==true)
return;
$room=&$this->rooms[$player['room']];
if($room['started']&&!$room['ended'])
return;
$room['round']=1;
$room['point']=0;
$room['time_left']=10;
$room['started']=true;
$this->sendRoomInfo($player['room'],"游戏开始!现在是第一局");
$this->refreshRoom($room['id']);
break;
case 'num':
$room=&$this->rooms[$player['room']];
if($room['started']===false)
{
$this->sendInfo($player,'你现在不能选择数字,游戏未开始!');
return;
}
$origin=$player['num'];
$player['num']=floatval($content);
if($player['num']>100||$player['num']<0)
{
$this->sendInfo($player,'你只能选择0~100之间的数字!');
$player['num']=-1;
}else if($origin==-1)
{
$this->sendInfo($player,'你选择了:'.$player['num']);
}else if($origin!=$player['num']){
$this->sendInfo($player,'你改变了自己的选择:'.$origin.' -> '.$player['num']);
}else
{
$this->sendInfo($player,'你刚刚选择的就是:'.$player['num']);
}
break;
case 'lobby':
$this->sendLobby($player);
break;
}
//$msg=$this->build(json_encode($response));
//$this->broadcast($msg);
}
/**
* 广播消息
*
* @param $data
*/
private function broadcast($data) {
foreach ($this->sockets as $socket) {
if ($socket['resource'] == $this->master) {
continue;
}
socket_write($socket['resource'], $data, strlen($data));
}
}
/**
* 记录debug信息
*
* @param array $info
*/
public function debug(array $info) {
$time = date('Y-m-d H:i:s');
array_unshift($info, $time);
$info = array_map('json_encode', $info);
file_put_contents(self::LOG_PATH . 'websocket_debug.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
}
/**
* 记录错误信息
*
* @param array $info
*/
public function error(array $info) {
$time = date('Y-m-d H:i:s');
array_unshift($info, $time);
$info = array_map('json_encode', $info);
file_put_contents(self::LOG_PATH . 'websocket_error.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
}
}
global $ws;
$ws = new WebSocket("192.168.1.1", "8585");
#fputs('',"GET timer.php");