Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,326 @@
using RPS.Core;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace RPS.Client
{
public sealed class RpsGameClient : IGameClient
{
private const ushort SnapshotOpcode = 1;
private const ushort ChoiceOpcode = 1;
private const string UiPrefabPath =
"Assets/MiniGames/RockPaperScissors/res/UI/Prefab/UI_RockPaperScissors.prefab";
private readonly RpsLogic _logic = new RpsLogic();
private IGameClientCtx _ctx;
private RpsView _view;
private RpsState _state;
private Choice _localChoice = Choice.None;
private string _status = "waiting for match...";
private float _elapsed;
public void OnEnter(IGameClientCtx ctx)
{
_ctx = ctx;
EnsureEventSystem();
_ctx.Assets.Load(UiPrefabPath, OnPrefabLoaded);
_ctx.Logger.Info("rps client entered");
}
public void OnNetMessage(NetMessage msg)
{
if (msg.Opcode != SnapshotOpcode) return;
RpsState previous = _state;
_state = _logic.Decode(msg.Payload);
if (_state.Phase == RpsPhase.Choosing && previous != null && previous.Round != _state.Round)
_localChoice = Choice.None;
_status = _state.Phase == RpsPhase.Finished
? (_state.Winner == 2 ? "finished: draw" : "finished: seat " + _state.Winner + " wins")
: "round " + _state.Round + " / " + _state.Phase;
Render();
}
public void OnUpdate(float dt)
{
_elapsed += dt;
_view?.SetElapsed(_elapsed);
_view?.SetProgress(_state);
}
public void OnExit()
{
_ctx?.Logger.Info("rps client exited");
_view?.Destroy();
_view = null;
_ctx = null;
_state = null;
_localChoice = Choice.None;
_status = "waiting for match...";
_elapsed = 0f;
}
public void SubmitChoice(Choice choice)
{
if (_ctx == null || _state == null) return;
if (_state.Phase != RpsPhase.Choosing) return;
if (_localChoice != Choice.None) return;
_localChoice = choice;
var w = new PacketWriter();
w.WriteByte((byte)choice);
_ctx.Send(new NetMessage(ChoiceOpcode, w.ToArray()));
_status = "locked: " + choice;
Render();
}
private void OnPrefabLoaded(object loaded)
{
if (_ctx == null) return;
var prefab = loaded as GameObject;
if (prefab == null)
{
_ctx.Logger.Error("rps ui prefab failed to load: " + UiPrefabPath);
return;
}
GameObject instance = Object.Instantiate(prefab);
instance.name = "UI_RockPaperScissors";
Object.DontDestroyOnLoad(instance);
_view = new RpsView(instance, SubmitChoice);
Render();
}
private void Render()
{
_view?.Render(_state, _localChoice, _status, _elapsed);
}
private static void EnsureEventSystem()
{
if (EventSystem.current != null) return;
var eventSystem = new GameObject("EventSystem");
Object.DontDestroyOnLoad(eventSystem);
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
}
internal sealed class RpsView
{
private const float ChooseSeconds = 5f;
private const float RevealSeconds = 5f;
private const float TotalSeconds = 60f;
private readonly GameObject _root;
private readonly Button _rockButton;
private readonly Button _paperButton;
private readonly Button _scissorsButton;
private readonly Button _confirmButton;
private readonly TextMeshProUGUI _phaseText;
private readonly TextMeshProUGUI _messageText;
private readonly TextMeshProUGUI _roundText;
private readonly TextMeshProUGUI _lockedText;
private readonly TextMeshProUGUI _leftScoreText;
private readonly TextMeshProUGUI _rightScoreText;
private readonly TextMeshProUGUI _leftRevealText;
private readonly TextMeshProUGUI _rightRevealText;
private readonly TextMeshProUGUI _resultText;
private readonly TextMeshProUGUI _totalTimeText;
private readonly TextMeshProUGUI _finalRowsText;
private readonly TextMeshProUGUI _leftNameText;
private readonly TextMeshProUGUI _rightNameText;
private readonly Image _totalProgressFill;
private readonly Image _phaseProgressFill;
private readonly Image _leftChoiceIcon;
private readonly Image _rightChoiceIcon;
private readonly Sprite _rockSprite;
private readonly Sprite _paperSprite;
private readonly Sprite _scissorsSprite;
public RpsView(GameObject root, System.Action<Choice> choose)
{
_root = root;
_rockButton = Find<Button>("Btn_Rock");
_paperButton = Find<Button>("Btn_Paper");
_scissorsButton = Find<Button>("Btn_Scissors");
_confirmButton = Find<Button>("Btn_Confirm");
_phaseText = Find<TextMeshProUGUI>("Txt_Phase");
_messageText = Find<TextMeshProUGUI>("Txt_Message");
_roundText = Find<TextMeshProUGUI>("Txt_Round");
_lockedText = Find<TextMeshProUGUI>("Txt_Locked");
_leftScoreText = Find<TextMeshProUGUI>("Txt_LeftScore");
_rightScoreText = Find<TextMeshProUGUI>("Txt_RightScore");
_leftRevealText = Find<TextMeshProUGUI>("Txt_LeftReveal");
_rightRevealText = Find<TextMeshProUGUI>("Txt_RightReveal");
_resultText = Find<TextMeshProUGUI>("Txt_Result");
_totalTimeText = Find<TextMeshProUGUI>("Txt_TotalTime");
_finalRowsText = Find<TextMeshProUGUI>("Txt_FinalRows");
_leftNameText = Find<TextMeshProUGUI>("Txt_LeftName");
_rightNameText = Find<TextMeshProUGUI>("Txt_RightName");
_totalProgressFill = Find<Image>("Img_TotalProgressFill");
_phaseProgressFill = Find<Image>("Img_PhaseProgressFill");
_leftChoiceIcon = Find<Image>("Img_LeftChoiceIcon");
_rightChoiceIcon = Find<Image>("Img_RightChoiceIcon");
_rockSprite = Find<Image>("Img_RockIcon")?.sprite;
_paperSprite = Find<Image>("Img_PaperIcon")?.sprite;
_scissorsSprite = Find<Image>("Img_ScissorsIcon")?.sprite;
if (_rockButton != null) _rockButton.onClick.AddListener(() => choose(Choice.Rock));
if (_paperButton != null) _paperButton.onClick.AddListener(() => choose(Choice.Paper));
if (_scissorsButton != null) _scissorsButton.onClick.AddListener(() => choose(Choice.Scissors));
if (_confirmButton != null) _confirmButton.gameObject.SetActive(false);
SetText(_leftNameText, "You");
SetText(_rightNameText, "Opponent");
}
public void Render(RpsState state, Choice localChoice, string status, float elapsed)
{
SetText(_messageText, status);
SetElapsed(elapsed);
if (state == null)
{
SetText(_phaseText, "Waiting");
SetText(_roundText, "Round -");
SetText(_lockedText, "Locked 0/2");
SetText(_leftScoreText, "0");
SetText(_rightScoreText, "0");
SetText(_leftRevealText, "Hidden");
SetText(_rightRevealText, "Hidden");
SetText(_resultText, "");
SetText(_finalRowsText, "");
SetIcon(_leftChoiceIcon, Choice.None);
SetIcon(_rightChoiceIcon, Choice.None);
SetButtons(false);
SetProgress(null);
return;
}
SetText(_phaseText, PhaseName(state.Phase));
SetText(_roundText, "Round " + state.Round);
SetText(_leftScoreText, state.Scores[0].ToString());
SetText(_rightScoreText, state.Scores[1].ToString());
SetText(_lockedText, "Locked " + LockedCount(state) + "/2");
SetText(_leftRevealText, ChoiceDisplay(state.Choices[0], state.Phase));
SetText(_rightRevealText, ChoiceDisplay(state.Choices[1], state.Phase));
SetText(_resultText, ResultText(state));
SetText(_finalRowsText, FinalRows(state));
SetText(_rightNameText, state.IsAi[1] ? "AI" : "Opponent");
SetIcon(_leftChoiceIcon, state.Choices[0]);
SetIcon(_rightChoiceIcon, state.Choices[1]);
SetButtons(state.Phase == RpsPhase.Choosing && localChoice == Choice.None);
SetProgress(state);
}
public void SetElapsed(float elapsed)
{
SetText(_totalTimeText, elapsed.ToString("0.0") + "s / 60s");
}
public void SetProgress(RpsState state)
{
if (_totalProgressFill != null)
_totalProgressFill.fillAmount = state == null ? 0f : Mathf.Clamp01(state.TotalElapsed / TotalSeconds);
if (_phaseProgressFill != null)
{
if (state == null)
{
_phaseProgressFill.fillAmount = 0f;
}
else
{
float phaseSeconds = state.Phase == RpsPhase.Revealing ? RevealSeconds : ChooseSeconds;
_phaseProgressFill.fillAmount = Mathf.Clamp01(state.PhaseElapsed / phaseSeconds);
}
}
}
public void Destroy()
{
if (_root != null) Object.Destroy(_root);
}
private T Find<T>(string name) where T : Component
{
var transforms = _root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < transforms.Length; i++)
{
if (transforms[i].name == name)
return transforms[i].GetComponent<T>();
}
Debug.LogWarning("[RpsView] Missing UI binding: " + name);
return null;
}
private void SetButtons(bool interactable)
{
if (_rockButton != null) _rockButton.interactable = interactable;
if (_paperButton != null) _paperButton.interactable = interactable;
if (_scissorsButton != null) _scissorsButton.interactable = interactable;
}
private void SetIcon(Image image, Choice choice)
{
if (image == null) return;
Sprite sprite = choice == Choice.Rock ? _rockSprite
: choice == Choice.Paper ? _paperSprite
: choice == Choice.Scissors ? _scissorsSprite
: null;
image.sprite = sprite;
image.enabled = sprite != null;
}
private static void SetText(TextMeshProUGUI text, string value)
{
if (text != null) text.text = value ?? "";
}
private static int LockedCount(RpsState state)
{
int count = 0;
if (state.Choices[0] != Choice.None) count++;
if (state.Choices[1] != Choice.None) count++;
return count;
}
private static string ChoiceDisplay(Choice choice, RpsPhase phase)
{
if (choice == Choice.None) return "Hidden";
return phase == RpsPhase.Choosing ? "Locked" : ChoiceName(choice);
}
private static string ChoiceName(Choice c)
=> c == Choice.Rock ? "Rock"
: c == Choice.Paper ? "Paper"
: c == Choice.Scissors ? "Scissors" : "-";
private static string PhaseName(RpsPhase phase)
=> phase == RpsPhase.Choosing ? "Choose"
: phase == RpsPhase.Revealing ? "Reveal"
: "Finished";
private static string ResultText(RpsState state)
{
if (state.Phase != RpsPhase.Finished) return "";
return state.Winner == 2 ? "Draw" : "Seat " + state.Winner + " wins";
}
private static string FinalRows(RpsState state)
{
if (state.Phase != RpsPhase.Finished) return "";
return "Final score " + state.Scores[0] + " : " + state.Scores[1];
}
}
}
@@ -0,0 +1,151 @@
using System.Collections.Generic;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace RPS.Core
{
public sealed class RpsLogic : IGameLogic<RpsState, RpsInput, RpsEvent>
{
public const float ChooseSeconds = 5f;
public const float RevealSeconds = 5f;
public const float TotalSeconds = 60f;
private static readonly Choice[] AllChoices =
{
Choice.Rock,
Choice.Paper,
Choice.Scissors
};
public RpsState CreateInitial(RoomConfig config, IRandom random)
{
return new RpsState { Round = 1, Phase = RpsPhase.Choosing };
}
public StepResult<RpsState, RpsEvent> Step(RpsState state, RpsInput input, IRandom random)
{
var events = new List<RpsEvent>();
if (state.Phase == RpsPhase.Finished)
return new StepResult<RpsState, RpsEvent>(state, events);
if (input.Kind == RpsInputKind.Choose)
{
if (state.Phase == RpsPhase.Choosing
&& state.Choices[input.Seat] == Choice.None
&& input.Choice != Choice.None)
{
state.Choices[input.Seat] = input.Choice;
}
return new StepResult<RpsState, RpsEvent>(state, events);
}
state.PhaseElapsed += input.Dt;
state.TotalElapsed += input.Dt;
if (state.Phase == RpsPhase.Choosing && state.PhaseElapsed >= ChooseSeconds)
{
ResolveRound(state, random, events);
state.Phase = RpsPhase.Revealing;
state.PhaseElapsed = 0f;
}
else if (state.Phase == RpsPhase.Revealing && state.PhaseElapsed >= RevealSeconds)
{
state.Round++;
state.Phase = RpsPhase.Choosing;
state.PhaseElapsed = 0f;
state.Choices[0] = Choice.None;
state.Choices[1] = Choice.None;
}
if (state.Phase != RpsPhase.Finished && state.TotalElapsed >= TotalSeconds)
Finish(state, events);
return new StepResult<RpsState, RpsEvent>(state, events);
}
public byte[] Encode(RpsState state)
{
var writer = new PacketWriter();
writer.WriteVarInt(state.Round);
writer.WriteByte((byte)state.Phase);
writer.WriteSingle(state.PhaseElapsed);
writer.WriteSingle(state.TotalElapsed);
writer.WriteByte((byte)state.Choices[0]);
writer.WriteByte((byte)state.Choices[1]);
writer.WriteVarInt(state.Scores[0]);
writer.WriteVarInt(state.Scores[1]);
writer.WriteBool(state.IsAi[0]);
writer.WriteBool(state.IsAi[1]);
writer.WriteVarInt(state.Winner);
return writer.ToArray();
}
public RpsState Decode(byte[] data)
{
var reader = new PacketReader(data);
var state = new RpsState
{
Round = reader.ReadVarInt(),
Phase = (RpsPhase)reader.ReadByte(),
PhaseElapsed = reader.ReadSingle(),
TotalElapsed = reader.ReadSingle()
};
state.Choices[0] = (Choice)reader.ReadByte();
state.Choices[1] = (Choice)reader.ReadByte();
state.Scores[0] = reader.ReadVarInt();
state.Scores[1] = reader.ReadVarInt();
state.IsAi[0] = reader.ReadBool();
state.IsAi[1] = reader.ReadBool();
state.Winner = reader.ReadVarInt();
return state;
}
private static void ResolveRound(RpsState state, IRandom random, List<RpsEvent> events)
{
for (int i = 0; i < 2; i++)
{
if (state.Choices[i] == Choice.None)
state.Choices[i] = AllChoices[random.Next(3)];
}
int winner = WinnerOf(state.Choices[0], state.Choices[1]);
if (winner >= 0)
state.Scores[winner]++;
events.Add(new RpsEvent
{
Kind = RpsEventKind.RoundResolved,
Round = state.Round,
RoundWinner = winner
});
}
private static int WinnerOf(Choice first, Choice second)
{
if (first == second)
return -1;
bool firstWins = (first == Choice.Rock && second == Choice.Scissors)
|| (first == Choice.Paper && second == Choice.Rock)
|| (first == Choice.Scissors && second == Choice.Paper);
return firstWins ? 0 : 1;
}
private static void Finish(RpsState state, List<RpsEvent> events)
{
state.Phase = RpsPhase.Finished;
state.Winner = state.Scores[0] == state.Scores[1]
? 2
: state.Scores[0] > state.Scores[1] ? 0 : 1;
events.Add(new RpsEvent
{
Kind = RpsEventKind.GameFinished,
GameWinner = state.Winner
});
}
}
}
@@ -0,0 +1,67 @@
namespace RPS.Core
{
public enum Choice : byte
{
None = 0,
Rock = 1,
Paper = 2,
Scissors = 3
}
public enum RpsPhase : byte
{
Choosing = 0,
Revealing = 1,
Finished = 2
}
public sealed class RpsState
{
public int Round;
public RpsPhase Phase;
public float PhaseElapsed;
public float TotalElapsed;
public Choice[] Choices = new Choice[2];
public int[] Scores = new int[2];
public bool[] IsAi = new bool[2];
public int Winner = -1;
}
public enum RpsInputKind : byte
{
Tick = 0,
Choose = 1
}
public struct RpsInput
{
public RpsInputKind Kind;
public int Seat;
public Choice Choice;
public float Dt;
public static RpsInput Tick(float dt)
{
return new RpsInput { Kind = RpsInputKind.Tick, Dt = dt };
}
public static RpsInput Choose(int seat, Choice choice)
{
return new RpsInput { Kind = RpsInputKind.Choose, Seat = seat, Choice = choice };
}
}
public enum RpsEventKind : byte
{
RoundResolved = 0,
GameFinished = 1
}
public struct RpsEvent
{
public RpsEventKind Kind;
public int Round;
public int RoundWinner;
public int GameWinner;
}
}