跳到主要内容

使用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));
}
}
}