.Net Core——用SignalR擼個遊戲
- 2021 年 12 月 4 日
- 筆記
- .Net/.Net Core, NET Core
之前開內部培訓,說到實時web應用這一塊講到了SignalR,我說找時間用它做個遊戲玩玩,後面時間緊張就一直沒安排。這兩天閑了又想起這個事,考慮後決定用2天時間寫個斗D主,安排了前端同學寫客戶端,我寫遊戲邏輯和服務。
這個項目難度並不高,但是遊戲邏輯還是挺繞的,聯調過程中也發現解決了很多小問題。來園子里整理一篇文章,記錄一下。
基礎的介紹就免了,畢竟官網跟著走兩圈啥都懂了。沒基礎的可以戳這裡,是我之前寫的一篇SignalR基礎介紹,帶有一個極簡聊天室。
tips:文章結尾有開源地址,遊戲數據都是本地的,改下IP運行起來就可以玩了。
直接上乾貨,首先是數據模型:
/// <summary> /// 用戶資訊 /// </summary> public class Customer { /// <summary> /// 唯一ID /// </summary> public string? ID { get; set; } /// <summary> /// 昵稱 /// </summary> public string? NickName { get; set; } /// <summary> /// 卡片 /// </summary> public List<string> Card { get; set; } } /// <summary> /// 房間 /// </summary> public class Room { /// <summary> /// 房間名 /// </summary> public string Name { get; set; } /// <summary> /// 房主id /// </summary> public string Masterid { get; set; } /// <summary> /// 當前出牌人 /// </summary> public int Curr { get; set; } /// <summary> /// 當前卡片 /// </summary> public List<string> CurrCard { get; set; } = new List<string>(); /// <summary> /// 當前卡片打出人 /// </summary> public string ExistingCardClient { get; set; } /// <summary> /// 房間成員列表 /// </summary> public List<Customer> Customers { get; set; } = new List<Customer>(); }
tips:只是單純為了斗D主設計的,商用版肯定不能這麼搞,參考請慎用。
有了數據模型,自然少不了CRUD:
/// <summary> /// 用戶操作 /// </summary> public static class CustomerAction { /// <summary> /// 用戶列表 /// </summary> private static List<Customer> cusList = new List<Customer>(); /// <summary> /// 不存在則新增,存在則修改昵稱 /// </summary> /// <param name="customer"></param> public static void Create(Customer customer) { Customer curr = null; if (cusList.Count > 0) curr = cusList.Where(x => x.ID == customer.ID).FirstOrDefault(); if (curr is null) cusList.Add(customer); else { curr.NickName = customer.NickName; Up4ID(curr); } } /// <summary> /// 用戶列表 /// </summary> /// <returns></returns> public static List<Customer> GetList() { return cusList; } /// <summary> /// 獲取單個 /// </summary> /// <param name="id"></param> /// <returns></returns> public static Customer GetOne(string id) { return cusList.Where(x => x.ID == id).FirstOrDefault(); } /// <summary> /// 刪除用戶 /// </summary> /// <param name="id"></param> public static void Delete(string id) { cusList.RemoveAll(x => x.ID == id); } /// <summary> /// 增加卡片 /// </summary> /// <param name="id"></param> /// <param name="cards"></param> public static void InCard(string id, List<string> cards) { Customer customer = cusList.Where(x => x.ID == id).FirstOrDefault(); if (customer.Card is null) customer.Card = cards; else customer.Card.AddRange(cards); Up4ID(customer); } /// <summary> /// 扣除卡片 /// </summary> /// <param name="id"></param> /// <param name="cards"></param> /// <param name="group"></param> /// <returns></returns> public static bool OutCard(string id, List<string> cards, Room group) { Customer client = cusList.Where(x => x.ID == id).FirstOrDefault(); if (client is null) return false; //卡片不匹配直接失敗 if (client.Card.Where(x => cards.Contains(x)).ToList().Count != cards.Count) return false; //不符合出牌規則直接失敗 if (!new Game.WithCard().Rule(group.CurrCard, cards, group.ExistingCardClient is null || group.ExistingCardClient == id)) return false; foreach (var item in cards) { client.Card.Remove(item); } group.CurrCard = cards; group.ExistingCardClient = id; Up4ID(client); RoomAction.Up4Name(group); return true; } /// <summary> /// 更新(根據ID) /// </summary> /// <param name="customer"></param> /// <returns></returns> public static bool Up4ID(Customer customer) { if (cusList.Count == 0) return false; cusList.RemoveAll(x => x.ID == customer.ID); cusList.Add(customer); return true; } } /// <summary> /// 房間操作 /// </summary> public static class RoomAction { /// <summary> /// 房間列表 /// </summary> private static List<Room> roomList = new List<Room>(); /// <summary> /// 新增房間 /// 如果房間已存在則不新增 /// </summary> /// <param name="group"></param> public static void Create(Room group) { if (!roomList.Where(x => x.Name == group.Name).Any()) roomList.Add(group); } /// <summary> /// 獲取列表 /// </summary> /// <returns></returns> public static List<Room> GetList() { return roomList; } /// <summary> /// 獲取單個 /// </summary> /// <param name="masterid">房主id</param> /// <param name="roomName">房間名稱</param> /// <returns></returns> public static Room GetOne(string masterid = null, string roomName = null) { if (roomList.Count == 0) return null; if (masterid != null) return roomList.Where(x => x.Masterid == masterid).FirstOrDefault(); if (roomName != null) return roomList.Where(x => x.Name == roomName).FirstOrDefault(); return null; } /// <summary> /// 加入房間 /// </summary> /// <param name="client"></param> /// <param name="roomName"></param> public static bool Join(Customer client, string roomName) { if (roomList.Count == 0) return false; var room = roomList.Where(x => x.Name == roomName).FirstOrDefault(); if (room is null) return false; if (room.Customers.Count == 3) return false; room.Customers.Add(client); Up4Name(room); return true; } /// <summary> /// 刪除房間 /// </summary> /// <param name="masterid">房主id</param> public static bool Delete(string masterid) { if (roomList.Count == 0) return false; var room = roomList.Where(x => x.Masterid == masterid).FirstOrDefault(); if (room == null) return false; roomList.Remove(room); return true; } /// <summary> /// 更新(根據房名) /// </summary> /// <param name="room"></param> /// <returns></returns> public static bool Up4Name(Room room) { if (roomList.Count == 0) return false; roomList.RemoveAll(x => x.Name == room.Name); roomList.Add(room); return true; } /// <summary> /// 更新當前出牌人 /// </summary> /// <param name="roomName"></param> /// <param name="index">傳入則強制修改,不傳按規則走</param> public static Customer ChangeCurr(string roomName, int index = -1) { var room = roomList.Where(x => x.Name == roomName).FirstOrDefault(); if (index != -1) room.Curr = index; else room.Curr = (room.Curr + 1) % 3; Up4Name(room); return room.Customers[room.Curr]; } }
因為所有數據都是通過靜態屬性保存的,所以大部分都是linq操作(原諒我linq水平有限)。
接下來是遊戲邏輯:
/// <summary> /// 卡片相關 /// </summary> public class WithCard { /// <summary> /// 黑桃-S、紅桃-H、梅花-C、方塊-D /// BG大王,SG小王,14-A,15-2 /// </summary> readonly List<string> Cards = new List<string>() { "S-14","S-15","S-3","S-4","S-5","S-6","S-7","S-8","S-9","S-10","S-11","S-12","S-13", "H-14","H-15","H-3","H-4","H-5","H-6","H-7","H-8","H-9","H-10","H-11","H-12","H-13", "C-14","C-15","C-3","C-4","C-5","C-6","C-7","C-8","C-9","C-10","C-11","C-12","C-13", "D-14","D-15","D-3","D-4","D-5","D-6","D-7","D-8","D-9","D-10","D-11","D-12","D-13", "BG-99","SG-88" }; /// <summary> /// 發牌 /// </summary> public List<List<string>> DrawCard() { List<string> a = new List<string>(); List<string> b = new List<string>(); List<string> c = new List<string>(); Random ran = new Random(); //剩3張底牌 for (int i = 0; i < 51; i++) { //隨機抽取一張牌 string item = Cards[ran.Next(Cards.Count)]; switch (i % 3) { case 0: a.Add(item); break; case 1: b.Add(item); break; case 2: c.Add(item); break; } Cards.Remove(item); } return new List<List<string>>() { a,b,c,Cards }; } /// <summary> /// 規則 /// </summary> /// <param name="existingCard"></param> /// <param name="newCard"></param> /// <param name="isSelf"></param> /// <returns></returns> public bool Rule(List<string> existingCard, List<string> newCard, bool isSelf) { //現有牌號 List<int> existingCardNo = existingCard.Select(x => Convert.ToInt32(x.Split('-')[1])).ToList().OrderBy(x => x).ToList(); //新出牌號 List<int> newCardNo = newCard.Select(x => Convert.ToInt32(x.Split('-')[1])).ToList().OrderBy(x => x).ToList(); //上一手是王炸,禁止其他人出牌 if (existingCardNo.All(x => x > 50) && existingCardNo.Count == 2) { if (isSelf) return true; else return false; } //王炸最大 if (newCardNo.All(x => x > 50) && newCard.Count == 2) return true; //單張 if (newCardNo.Count == 1) { if (existingCardNo.Count == 0) return true; if ((existingCardNo.Count == 1 && newCardNo[0] > existingCardNo[0]) || isSelf) return true; } //對子/三隻 if (newCardNo.Count == 2 || newCardNo.Count == 3) { if (existingCardNo.Count == 0 && newCardNo.All(x => x == newCardNo[0])) return true; if (newCardNo.All(x => x == newCardNo[0]) && (isSelf || newCardNo.Count == existingCardNo.Count && newCardNo[0] > existingCardNo[0])) return true; } if (newCard.Count == 4) { //炸 if (newCardNo.All(x => x == newCardNo[0])) { if (existingCardNo.Count == 0 || isSelf) return true; if (existingCardNo.All(x => x == existingCardNo[0]) && existingCardNo.Count == 4) { if (newCardNo[0] > existingCardNo[0]) return true; } return true; } //三帶一 { List<int> flagA = newCardNo.Distinct().ToList(); //超過2種牌直接失敗 if (flagA.Count > 2) return false; //沒有上一手牌,或者上一手是自己出的牌 if (existingCardNo.Count == 0 || isSelf) return true; int newCardFlag = 0; if (newCardNo.Where(x => x == flagA[0]).ToList().Count() > 1) { newCardFlag = flagA[0]; } else newCardFlag = flagA[1]; List<int> flagB = existingCardNo.Distinct().ToList(); //上一手牌不是三帶一 if (flagB.Count > 2) return false; int existingCardFlag = 0; if (existingCardNo.Where(x => x == flagB[0]).ToList().Count() > 1) { existingCardFlag = flagB[0]; } else existingCardFlag = flagB[1]; if (newCardFlag > existingCardFlag) return true; } } if (newCard.Count >= 5) { bool flag = true; for (int i = 0; i < newCardNo.Count - 1; i++) { if (newCardNo[i] + 1 != newCardNo[i + 1]) { flag = false; break; } } //順子 if (flag) { if (existingCardNo.Count == 0 || (newCardNo[0] > existingCardNo[0] && newCardNo.Count == existingCardNo.Count) || isSelf) return true; } } return false; } }
單張規則和普通斗D主一樣(除了王以外2最大,其次是A),多張規則目前支援:王炸、對子、三隻、順子、三帶一。目前只做到這裡,各位同學可以拿回去自行擴展。
上一些運行圖。房主建房並加入:
新玩家加入:
房間人滿以後房主開始遊戲,隨機分配地主:
出牌特效:
遊戲結算:
最後附上開源地址(客戶端在web分支)://gitee.com/muchengqingxin/card-game
tips:前端同學在沒有UI配合的情況下做到現在這樣,必須給個贊。最後提醒大家,不要拿去商用。