/// <summary> /// 辅助子过程。检查异步的网络请求是否成功、对服务器的操作请求是否实现,并返回实现了的操作的结果,或错误信息。 /// </summary> /// <typeparam name="T">代表结果数据的泛型参数。</typeparam> /// <param name="respTask">代表异步的网络请求。</param> /// <param name="deserializer">对网络请求结果的反序列化器。</param> /// <returns>操作的结果。</returns> private static async Task <GameOperation <T> > OperationFromResponseAsync <T>(Task <HttpResponseMessage> respTask, Func <string, T> deserializer) { string msg = ""; try { var resp = await respTask.ConfigureAwait(false); // 如果是 404 BadRequest,我们最好看看服务器提供了哪些错误信息。 if (resp.IsSuccessStatusCode || resp.StatusCode == HttpStatusCode.BadRequest) { var respStr = await resp.Content.ReadAsStringAsync(); if (resp.IsSuccessStatusCode) { return(GameOperation <T> .Succ(deserializer(respStr))); } else { // 404。服务器会将错误信息放在 JSON 数据的 Message 字段里。 var deserialized = JsonConvert.DeserializeAnonymousType(respStr, new { Message = "" }); msg = deserialized.Message; } } } catch (Exception ex) { msg = ex.Message; } return(GameOperation <T> .Fail(msg)); }
/// <summary> /// 开启一个新游戏。 /// </summary> /// <param name="roomIdToStart">这个游戏实例要连接到的房间号。若为null或空,则为默认的0号房间。</param> /// <param name="userId">参与这个游戏的玩家ID。若为null或空,会生成新ID。</param> /// <returns>创建游戏的操作结果。</returns> public static async Task <GameOperation <CreateGameResult> > OpenRoomAsync(string roomIdToStart, string userId) { // ConfigureAwait(false) 可以避免对特定的线程进行调度,比如只能序列化访问的UI线程。 // 该函数内的操作都不需要 UI 线程。 var stateOp = await QueryStateAsync(userId, roomIdToStart).ConfigureAwait(false); if (!stateOp.Succeeded) { // 如果查询状态失败了,创建游戏也就失败了。 return(GameOperation <CreateGameResult> .Fail(stateOp.ErrorMessage)); } var state = stateOp.OperationResult; if (string.IsNullOrEmpty(state.UserId)) { // User ID 是必要的。 return(GameOperation <CreateGameResult> .Fail("No valid User ID.")); } var mode = ConvertNumberMode(state.Numbers); if (mode == RoomNumberMode.Unknown) { // 不支持的游戏模式。 return(GameOperation <CreateGameResult> .Fail($"Unsupported number mode {mode}.")); } var game = new Game { UserId = state.UserId, RoomId = state.RoomId, Nickname = state.NickName, NumberMode = mode, }; // 启动游戏主循环,用于倒计时和推进游戏轮数。 // 由于我们用 async/await的形式包装了主循环,故其返回 Task。不过我们不需要用这个 Task。 var ignored = game.StartAsync(state.RoundId, state.LeftTime); return(GameOperation <CreateGameResult> .Succ(new CreateGameResult(game, TimeSpan.FromSeconds(state.LeftTime), ConvertStateToNewRound(state)))); }
/// <summary> /// 创建一个新的游戏房间,并在其中开启新游戏。 /// </summary> /// <param name="mode"></param> /// <param name="userId">可选。要继承的玩家ID。</param> /// <returns>创建游戏的操作结果。</returns> public static async Task <GameOperation <CreateGameResult> > StartInNewRoomAsync(RoomNumberMode mode, string userId = null) { if (!SupportedNumberModes.ContainsKey(mode)) { throw new ArgumentOutOfRangeException(nameof(mode)); } // 先创建新房间,再在这个房间里,按正常流程启动游戏。 var url = string.Format(NewRoomEndpointTemplate, SupportedNumberModes[mode]); var newRoomOp = await OperationFromResponseAsync(s_httpClient.GetAsync(url), JsonConvert.DeserializeObject <NewRoom>); if (!newRoomOp.Succeeded) { return(GameOperation <CreateGameResult> .Fail(newRoomOp.ErrorMessage)); } var newRoom = newRoomOp.OperationResult; return(await OpenRoomAsync(roomIdToStart : newRoom.RoomId, userId : userId)); }
/// <summary> /// 检查并提交这一轮的黄金点。 /// </summary> /// <param name="candidate">要提交的数。</param> /// <param name="candidate2">要提交的第二个数。在不支持两个数的游戏里,必须是null。</param> /// <returns>提交黄金点的操作结果。</returns> public async Task <GameOperation <bool> > SubmitAsync(double candidate, double?candidate2) { EnsureGameNotClosed(); // _roundId 可能由于多线程而变动。我们先复制它到局部变量。 var roundId = _roundId; if (!(0 < candidate && candidate < 100)) { return(GameOperation <bool> .Fail("Input must be in (0, 100)")); } if (candidate2.HasValue) { if (NumberMode != RoomNumberMode.Two) { return(GameOperation <bool> .Fail("Not 2-number room.")); } if (!(0 < candidate2 && candidate2 < 100)) { return(GameOperation <bool> .Fail("Secondary input must be in (0, 100)")); } } string submitUrl = string.Format( SubmitEndpointTemplate, Uri.EscapeDataString(UserId), Uri.EscapeDataString(roundId), Uri.EscapeDataString(candidate.ToString()), Uri.EscapeDataString(candidate2?.ToString() ?? "")); // 如果游戏模式不是提交两个数,就将第二个数对应的参数设为空。 var dummyBody = new StringContent(""); return(await OperationFromResponseAsync( s_httpClient.PostAsync(submitUrl, dummyBody), unused => true)); }