跳到主要内容

使用blazor server和signalr实现在线五子棋

· 阅读需 10 分钟

前些天女朋友邀请我一起玩微信小程序里的某个五子棋游戏,经常进不去房间且广告极多,令人生厌.于是就有了这个练手小项目

github仓库地址

alt text

借鉴及引用内容

阿星plus博客

五子棋单机部分代码实现仓库地址

坑点

  1. 剪贴板函数必须是https或者本地才可以调用

  2. text.json无法解析二维数组,需切换json.net

  3. signalr更新数据后无法及时刷新页面 解决方法为 StateHasChanged() 改为 InvokeAsync(StateHasChanged);有点winform中this.invoke那味了

  4. appsettings.json中或appsettings.Development.json中需添加"DetailedErrors": true以显示详细的错误信息,剪切板报错时提示我做的

主要代码

Program.cs

using Gobang.GameHub;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor.Services;

namespace Gobang
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddSignalR()
.AddNewtonsoftJsonProtocol();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

builder.Services.AddMudServices();
builder.WebHost.UseUrls("http://*:5005");

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapHub<BlazorChatSampleHub>(BlazorChatSampleHub.HubUrl);
app.MapHub<GoBangHub>(GoBangHub.HubUrl);

app.Run();
}
}
}

GoBangHub.cs(signalr集线器)

using Gobang.Model;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;

namespace Gobang.GameHub
{
public class GoBangHub : Hub
{
public const string HubUrl = "/gobang";

private static readonly List<GoBangRoom> goBangRooms = new();

public override Task OnConnectedAsync()
{
Console.WriteLine($"{Context.ConnectionId} connected");
return base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception? exception)
{
Console.WriteLine($"Disconnected {exception?.Message} {Context.ConnectionId}");
await base.OnDisconnectedAsync(exception);
}

/// <summary>
/// 创建房间(即创建群组)
/// </summary>
/// <param name="roomName">房间名(群组名)</param>
/// <param name="password">密码(可选)</param>
/// <returns></returns>
public async Task CreateRoom(string roomName, string? password = null)
{
goBangRooms.Add(new GoBangRoom() { Guid = Guid.NewGuid(), RoomName = roomName, Password = password });

await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
}

/// <summary>
/// 加入房间(群组)
/// </summary>
/// <param name="roomName">房间名(群组名)</param>
/// <param name="password">密码(可选)</param>
/// <returns></returns>
public async Task GetIntoRoom(string roomName, string? password = null)
{
var room = goBangRooms.FirstOrDefault(m => m.RoomName == roomName);

if (room == null)
{
await Clients.Caller.SendAsync("Alert", "未找到该房间!");
return;
}
else
{
if (!string.IsNullOrEmpty(room.Password) && room.Password != password)
{
await Clients.Caller.SendAsync("Alert", "房间密码错误!");
return;
}
}

await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
}

/// <summary>
/// 落子
/// </summary>
/// <param name="room">房间</param>
/// <param name="Chess">棋盘</param>
/// <returns></returns>
public async Task Playing(GoBangRoom room, int[,] Chess)
{
goBangRooms.First(m => m.RoomName == room.RoomName).Chess = Chess;
await Clients.OthersInGroup(room.RoomName).SendAsync("SynchronizeCheckerboard", Chess);
}

public async Task Win(GoBangRoom room)
{
await Clients.OthersInGroup(room.RoomName).SendAsync("Alert", "\n你个渣渣👎");
}
}
}

Gobang.razor.cs


@page "/"
@page "/{RoomName}"

@inject IJSRuntime JS

<div class="gobang-box">
<Gobang.Shared.Chessboard Chess="Chess" OnPlaying="Playing"></Gobang.Shared.Chessboard>
</div>
<div class="chess-info">
<h1>五子棋⚫⚪</h1>
@if (!IsInRoom)
{
<p><MudButton Variant="Variant.Outlined" Color="Color.Primary" @onclick="CreateRoom">创建房间</MudButton></p>
<p><MudButton Variant="Variant.Outlined" Color="Color.Primary" @onclick="GetIntoRoom">加入房间</MudButton></p>
}
else
{
<p><MudButton Variant="Variant.Outlined" Color="Color.Primary" @onclick="StartGame">@(IsInGame ? "重置游戏" : "开始游戏")</MudButton></p>

<p><MudButton Variant="Variant.Outlined" Color="Color.Primary" @onclick="Invite">邀请朋友</MudButton></p>
}

<div class="chess-msg">
<p><b>@msgs</b></p>
<span>第一步,创建房间</span>
<span>第二步,点击邀请朋友</span>
<span>第三步,等朋友进入网页后点击开始游戏</span>
<span>第四步,落子</span>
<p>游戏规则:</p>
<span>(1)房主始终黑棋先手。</span>
<span>(2)点击开始游戏按钮开始对局。</span>
<span>(4)对局双方各执一色棋子。</span>
<span>(5)空棋盘开局。</span>
<span>(6)黑先、白后,交替下子,每次只能下一子。</span>
<span>(7)棋子下在棋盘的空白点上,棋子下定后,不得向其它点移动,不得从棋盘上拿掉或拿起另落别处。</span>
<span>(8)黑方的第一枚棋子可下在棋盘任意交叉点上。</span>
<span>(9)轮流下子是双方的权利,<del>但允许任何一方放弃下子权(即:PASS权)</del></span>
<span>(10)<del>五子棋对局,执行黑方指定开局、三手可交换、五手两打的规定。整个对局过程中黑方有禁手,白方无禁手。黑方禁手有三三禁手、四四禁手和长连禁手三种。</del></span>
</div>
</div>

Gobang.razor.cs

using Gobang.GameHub;
using Gobang.Model;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.JSInterop;
using MudBlazor;

namespace Gobang.Pages
{
public partial class Gobang : IDisposable
{
[Inject] private NavigationManager? NavigationManager { get; set; }

[Inject]
private ISnackbar Snackbar { get; set; }

[Parameter]
public string RoomName { get; set; }

private int[,] Chess { get; set; } = new int[19, 19];

private string first = "He";

private bool IsInGame = false;

private bool IsInRoom = false;

private GoBangRoom? Room { get; set; }

private string msgs;

private int MineChess = 1;

private string? _hubUrl;
private HubConnection? _hubConnection;

protected override async Task OnInitializedAsync()
{
if (_hubConnection == null)
{
string baseUrl = NavigationManager!.BaseUri;

_hubUrl = baseUrl.TrimEnd('/') + GoBangHub.HubUrl;

_hubConnection = new HubConnectionBuilder()
.WithUrl(_hubUrl)
.ConfigureLogging(logging => logging.AddConsole())
.AddNewtonsoftJsonProtocol()
.Build();

_hubConnection.On<int[,]>("SynchronizeCheckerboard", SynchronizeCheckerboard);
_hubConnection.On<string>("Alert", Alert);
await _hubConnection.StartAsync();
}

await base.OnInitializedAsync();
}

protected override async Task OnParametersSetAsync()
{
if (!string.IsNullOrEmpty(RoomName))
{
IsInRoom = true;
IsInGame = true;
MineChess = 2;
Room = new GoBangRoom() { RoomName = RoomName };
await _hubConnection!.SendAsync("GetIntoRoom", RoomName, "");
}
await base.OnParametersSetAsync();
}

private async Task Alert(string msg)
{
await JS.InvokeAsync<string>("alert", msg);

if (msg == "\n你个渣渣👎")
{
IsInGame = false;
Chess = new int[19, 19];
}
}

private async Task CreateRoom()
{
var roomname = await JS.InvokeAsync<string>("prompt", "请输入房间名称!", "房间" + Guid.NewGuid());

if (string.IsNullOrEmpty(roomname)) return;

IsInRoom = true;

MineChess = 1;
if (string.IsNullOrEmpty(roomname)) return;
await _hubConnection!.SendAsync("CreateRoom", roomname, "");
Room = new GoBangRoom() { RoomName = roomname };
}

private async Task GetIntoRoom()
{
var roomname = await JS.InvokeAsync<string>("prompt", "请输入房间名称!");
if (string.IsNullOrEmpty(roomname)) return;

IsInRoom = true;
IsInGame = true;
await _hubConnection!.SendAsync("GetIntoRoom", roomname, "");
MineChess = 2;
Room = new GoBangRoom() { RoomName = roomname };
}

private async Task Invite()
{
Snackbar.Add("复制链接成功,快去邀请你的朋友吧!🚀");
await JS.InvokeVoidAsync("copyToClipboard", NavigationManager.BaseUri + Room!.RoomName);
}

private async Task StartGame()
{
// 初始化棋盘
Chess = new int[19, 19];

IsInGame = true;
await _hubConnection!.SendAsync("Playing", Room, Chess);
}

private async Task Playing((int, int) value)
{
(int row, int cell) = value;

var numEqual = Chess.OfType<int>().Count(x => x == 1) == Chess.OfType<int>().Count(x => x == 2);

if (MineChess == 1)
{
if (!numEqual)
{
Snackbar.Add("对方落子时间!🚀");
return;
}
}
else
{
if (numEqual)
{
Snackbar.Add("对方落子时间!🚀");
return;
}
}

//是否开始游戏,当前判断没开始给出提示
if (!IsInGame)
{
Snackbar.Add("\n💪点击开始游戏按钮开启对局,请阅读游戏规则💪");

return;
}

// 已落子直接返回,不做任何操作
if (Chess[row, cell] != 0)
return;

// 根据传进来的坐标进行我方落子
Chess[row, cell] = MineChess;

if (IsWin(MineChess, row, cell))
{
await JS.InvokeAsync<Task>("alert", "\n恭喜,你赢了👍");

IsInGame = !IsInGame;
await _hubConnection!.SendAsync("Win", Room);
}

// 我方落子之后通知对方落子
await _hubConnection!.SendAsync("Playing", Room, Chess);
StateHasChanged();
}

private void SynchronizeCheckerboard(int[,] chess)
{
Chess = chess;
InvokeAsync(StateHasChanged);
}

private bool IsWin(int chess, int row, int cell)
{
#region 横方向 ➡⬅

{
var i = 1;
var score = 1;
var rightValid = true;
var leftValid = true;

while (i <= 5)
{
var right = cell + i;
if (rightValid && right < 19)
{
if (Chess[row, right] == chess)
{
score++;
if (score >= 5)
return true;
}
else
rightValid = false;
}

var left = cell - i;
if (leftValid && left >= 0)
{
if (Chess[row, left] == chess)
{
score++;
if (score >= 5)
return true;
}
else
leftValid = false;
}

i++;
}
}

#endregion 横方向 ➡⬅

#region 竖方向 ⬇⬆

{
var i = 1;
var score = 1;
var topValid = true;
var bottomValid = true;

while (i < 5)
{
var top = row - i;
if (topValid && top >= 0)
{
if (Chess[top, cell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
topValid = false;
}

var bottom = row + i;
if (bottomValid && bottom < 19)
{
if (Chess[bottom, cell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
{
bottomValid = false;
}
}

i++;
}
}

#endregion 竖方向 ⬇⬆

#region 撇方向 ↙↗

{
var i = 1;
var score = 1;
var topValid = true;
var bottomValid = true;

while (i < 5)
{
var rightTopRow = row - i;
var rightTopCell = cell + i;
if (topValid && rightTopRow >= 0 && rightTopCell < 19)
{
if (Chess[rightTopRow, rightTopCell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
topValid = false;
}

var leftBottomRow = row + i;
var leftBottomCell = cell - i;
if (bottomValid && leftBottomRow < 19 && leftBottomCell >= 0)
{
if (Chess[leftBottomRow, leftBottomCell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
bottomValid = false;
}

i++;
}
}

#endregion 撇方向 ↙↗

#region 捺方向 ↘↖

{
var i = 1;
var score = 1;
var topValid = true;
var bottomValid = true;

while (i < 5)
{
var leftTopRow = row - i;
var leftTopCell = cell - i;
if (topValid && leftTopRow >= 0 && leftTopCell >= 0)
{
if (Chess[leftTopRow, leftTopCell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
topValid = false;
}

var rightBottomRow = row + i;
var rightBottomCell = cell + i;
if (bottomValid && rightBottomRow < 19 && rightBottomCell < 19)
{
if (Chess[rightBottomRow, rightBottomCell] == chess)
{
score++;
if (score >= 5)
return true;
}
else
bottomValid = false;
}

i++;
}
}

#endregion 捺方向 ↘↖

return false;
}

public async void Dispose()
{
if (_hubConnection != null)
{
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
_hubConnection = null;
}
}
}
}

Gobang.razor.cs


<div class="chess">
@for (var i = 0; i < 19; i++)
{
@for (var j = 0; j < 19; j++)
{
var _i = i;
var _j = j;
<div class="cell" @onclick="@(async () => await Playing(_i, _j))">
<span class="chess@(Chess[i, j])"></span>
</div>
}
}
</div>

@code {
[Parameter] public int[,] Chess { get; set; }
[Parameter]
public EventCallback<(int, int)> OnPlaying { get; set; }

private async Task Playing(int row, int cell)
{
if (OnPlaying.HasDelegate)
{
await OnPlaying.InvokeAsync((row, cell));
}
}
}

江南与师姐

· 阅读需 10 分钟

江南依旧是我喜欢的作家,如果这段文字触动了你,你可以去看看他的作品

以下是江南所写

我跟Celina的关系绝说不上亲近,基本是个闲人,但是某个下午Celina出现在我的实验室里,那时候我正跟某种极臭的气体战斗,像只戴着眼镜的臭鼬—-我们做实验都得戴防护限镜。

Celina问我说你晚上有空么?我说有啊师姐有饭局么?我少年时代基本就这点出息....

Celina说有,但你得穿像样点儿,我说我有新买的牛仔裤,Celina说穿牛仔裤去吃法国菜会被人赶出来,我说呦呵吃法国菜?Celina说走我带你去买身西装。

西装倒是不贵,几百美金就拿下了,但是皮鞋贵,皮鞋也几百美金,我有点胆怯了,心说师姐这是要泡我?要泡我直说啊!不用那么委婉?我能上校网发帖求点贤么....开玩笑的,那时候还没点赞这个功能,我就是心惊胆战。

Celina拍拍我肩膀上的皱褶,说一会儿坐我旁边少说话,多吃东西,今晚上有牡砺。

我于是坐在一问题为豪华的餐厅里,吃着牡蛎,听Celina和她的哈佛男友谈分手_...我的耳朵竖得很直,务必把这场惊天大八卦的每个细节都听进去。

哈佛男友真是金光闪闪,牛逼大了,在我土狗的岁月里我看到这等男人真是心生白卑,导致于我后来非常舍得在衣服上花钱,所有西装都是定制,还强行要投资我家裁缝楼下的酒廊.以便在

心里农奴翻身做主人扬眉吐气….

俩人谈分手的同时也在谈结婚,电视割的感觉,男友的主张是他在国内找到了一份非常好的工作,Celina应该立刻跟他回国,如果Celina不愿意回国也不要紧,他们结个婚就好,他觉得自己回了国,女朋友在美国很不安全。

这倒也非常霸道总裁,我喜欢...可是霸道总不是要娶我而是要娶Celina, Celina不喜欢,

Celina说我嫁给你当家庭主妇?我也是辛辛苦苦考到美国来的,我不读书了跟你回国?你当我是你的保姆?

精英说那结婚!我们在一起那么久了我们结婚不就好了?Celina说你那只眼睛看我觉得我非你不嫁?

男友说你们建筑那行就是画图画到死,女人怎么做?我跟你说国内有很多机会,你听我的没错,

Celina.说....

然后他们就决定要分手,然后就开始谈钱的问题,我这才知道Celina可以读我们学校那个很贵很贵且没什么奖学金的建筑系是因为男朋友慷慨出资,而男朋友也不是自己有线,而是家里觉得Celina早晚是自家的儿媳妇所以慷慨出资.....

最后Celina说没问题我把学费还给你,但我现在没有那笔钱,给我一年时间我分期给你

男朋友当时说了句把我惊呆了的话说,年利息按照定期算我也不多要你的。

Celina说可以!男朋友终于失去平静大声说你知道我为什么问你要利息么?他指着我说因为你今带了这个小子来跟我讲道!你不带他来我一分利息不要你的还暂缓你一年还那笔钱!

我说大哥你不要乱枪扫射老子他妈的就是来帮忙开车的!

男朋友不屑地了我一眼,大概也丝毫没觉得我有资格成为他的竞争者,说,我知道你们这种人有很多。

总之就是这么不欢而数了,Celina开着她的尼桑跑车送我回我的公寓,

非常礼貌地说今天辛苦你了,他那个人有时候发起脾气来不受控制我不敢单独见他了,我说你是很难过吧,难过了别强撑,我们可以组局吃饭一起骂那个傻逼。

Celina笑笑说其实我以前.很喜欢他的。

那之后Celina的生活一下子就变得拮据了,她打着好几份工包括黑工来支付自己的学费,好在最终男朋友还是坚持把那辆尼桑跑车送给了她,说那是礼物而不是需要偿还的东西,Celina卖了跑车,买了辆二手小丰田继续开着。

她仍旧是那么闪闪发亮,只有很少数的人知道她的辛苦,她渐渐地形容憔悴但依然微笑,用化妆来掩盖自己的黑眼圈,更多地出现在运动场而不是练习跳舞,她说她得保持运动否则就会倒下。

最辛苦的时候她问我借过钱说一个月就还,我尽我的能力给了她一张3000美元的支票,到期的时候她还了我一张3100美元的纸片说能否过一周再去兑?我就知道那时候她的户头里没有钱,还要一周

我把那张支票拿了个图钉钉在我办公桌对面的墙上,过了三个月才去兑的。

我说谢谢你送我西装和皮鞋

Celina说那是你的武装,就像女孩子用化妆来武装一样,你以后会有更好的武装。

再后来Celina毕业回国当了设计师,跟前男友结婚了,精英男一直等她,那应该是个蛮好的人,

也是配得上Celina的人,只是太自我。想必如今他再也不敢跟Celina那样说话了,

因为Celina无求于他,只是爱他而已。

读到这你大概了解了江南小说里的男女主角

学姐说精英有时候会发疯,她一个人不敢去,所以带上了江南。

如果当时提出要分手,要还钱的时候江南猛的拍案而起直面精英男会怎样呢?

那个晚上是江南最后悔的夜晚了吧?

大概现在江南也会在漫长的黑夜里幻想当时的场景

江南拍桌而起,破口大骂

“要分手就分,你滚了还有我杨志!“

“我也爱师姐,我能照顾的比你好!”

“不就是还钱吗?老子帮师姐还!谁稀罕你的臭钱!”

精英男气的浑身发抖,直勾勾的看着江南,大概是被突如其来的进攻打乱阵脚

“看什么看?不服咱俩出去solo!”

江南脸上青筋暴起,戴着眼镜也打挡不住他怒目圆睁的双眼,他抬起头和高他半扎的精英男对视

眼里有火要喷出来

这时候的师姐跟龙族四看见路明非为自己挡住昆古尼尔一样震惊和呆滞

江南一脚踹的精英男飞出三米远

右手环抱着师姐的肩膀带她坐上藏在黑暗里的尼桑跑车

油门到底,汽车咆哮

江南驾驶着跑车在黑夜里穿梭

师姐Celina看着眼前突然发疯的男孩

耳朵里只剩下自己的心跳和引擎的轰鸣……

楚子航的悔恨就来自于此吧?

可惜吗?可惜现在已经功成名就的江南,可惜现在已经手握刀剑的杨志

再也没有那样的机会,

因为世界上最快的跑车也跑不赢时光


没有如果,只有自卑,怯懦的江南,路明非,亦或是我

Welcome

· 阅读需 1 分钟
Sébastien Lorber
Docusaurus maintainer
Yangshun Tay
Front End Engineer @ Facebook

Docusaurus blogging features are powered by the blog plugin.

Simply add Markdown files (or folders) to the blog directory.

Regular blog authors can be added to authors.yml.

The blog post date can be extracted from filenames, such as:

  • 2019-05-30-welcome.md
  • 2019-05-30-welcome/index.md

A blog post folder can be convenient to co-locate blog post images:

Docusaurus Plushie

The blog supports tags as well!

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

First Blog Post

· 阅读需 1 分钟
Gao Wei
Docusaurus Core Team

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet