Rework How Room OPs Are Handled Per Room (if the game server itself isn't handling it, just send it to the room every time)
307 lines
13 KiB
C#
307 lines
13 KiB
C#
using System.Net.Sockets;
|
|
|
|
namespace ReboundEmulator.Core
|
|
{
|
|
public class BowlingGame
|
|
{
|
|
public int CurrentScore { get; set; }
|
|
public int CurrentRoundNumber { get; set; }
|
|
public string LeaderboardData { get; set; } = string.Empty;
|
|
public string FrameCardData { get; set; } = string.Empty;
|
|
public BowlingGameState State { get; set; }
|
|
public int PrepTimerMili { get; set; } = 15000;
|
|
public int BetweenRoundTimerMili { get; set; } = 20000;
|
|
public int RoundTimerMili { get; set; } = 25000;
|
|
public int NextGameTimerMili { get; set; } = 30000;
|
|
|
|
private const int GameId = 1;
|
|
private const int FrameCount = 10;
|
|
private const string ScreenName = "NotPogoUser";
|
|
|
|
// Ball pins thrown per frame, e.g. _frames[0] = [10], _frames[1] = [7, 2]
|
|
private readonly List<List<int>> _frames = new();
|
|
// Cumulative per-frame score, indexed the same as _frames, filled in as frames complete/resolve
|
|
private readonly List<int> _frameScores = new();
|
|
|
|
// Signaled by RecordThrow() when the current frame is finished (strike, or all balls thrown)
|
|
private TaskCompletionSource<bool>? _frameDoneSignal;
|
|
|
|
public string GetStateString()
|
|
{
|
|
switch (State)
|
|
{
|
|
case BowlingGameState.PreparingRound:
|
|
return "gm.pre";
|
|
case BowlingGameState.RoundStarted:
|
|
return "gm.str";
|
|
case BowlingGameState.RoundOver:
|
|
return "gm.btw";
|
|
case BowlingGameState.GameEnded:
|
|
return "gm.end";
|
|
}
|
|
|
|
return "gm.inval";
|
|
}
|
|
|
|
public void IncrementScore(int count) => CurrentScore += count;
|
|
|
|
// ---------------------------------------------------------------
|
|
// Call this from wherever co.turn is currently handled, passing
|
|
// the number of pins knocked down on that single throw.
|
|
// ---------------------------------------------------------------
|
|
public void RecordThrow(int pins)
|
|
{
|
|
if (_frames.Count <= CurrentRoundNumber)
|
|
_frames.Add(new List<int>());
|
|
|
|
_frames[CurrentRoundNumber].Add(pins);
|
|
IncrementScore(pins);
|
|
|
|
if (IsFrameComplete(CurrentRoundNumber, _frames[CurrentRoundNumber]))
|
|
_frameDoneSignal?.TrySetResult(true);
|
|
}
|
|
|
|
private static bool IsFrameComplete(int frameIndex, List<int> balls)
|
|
{
|
|
if (frameIndex < 9)
|
|
return (balls.Count == 1 && balls[0] == 10) || balls.Count == 2;
|
|
|
|
// 10th frame: 3 balls if it opened with a strike or a spare, otherwise 2
|
|
if (balls.Count < 2) return false;
|
|
if (balls.Count == 2) return balls[0] != 10 && balls[0] + balls[1] != 10;
|
|
return true;
|
|
}
|
|
|
|
// Classic ball-list bowling scorer. A frame that just completed with a strike/spare
|
|
// will read low until the bonus ball(s) in the following frame(s) are thrown -- that
|
|
// matches how a real scoreboard behaves (shows "--" until the bonus resolves).
|
|
private static List<int> ScoreFrames(List<int> rolls)
|
|
{
|
|
var scores = new List<int>();
|
|
int idx = 0;
|
|
for (int frame = 0; frame < FrameCount; frame++)
|
|
{
|
|
int prev = frame > 0 ? scores[frame - 1] : 0;
|
|
if (idx >= rolls.Count) { scores.Add(prev); continue; }
|
|
|
|
if (rolls[idx] == 10) // strike
|
|
{
|
|
scores.Add(prev + 10 + Sum(rolls, idx + 1, 2));
|
|
idx += 1;
|
|
}
|
|
else if (idx + 1 < rolls.Count && rolls[idx] + rolls[idx + 1] == 10) // spare
|
|
{
|
|
scores.Add(prev + 10 + Sum(rolls, idx + 2, 1));
|
|
idx += 2;
|
|
}
|
|
else
|
|
{
|
|
scores.Add(prev + Sum(rolls, idx, 2));
|
|
idx += 2;
|
|
}
|
|
}
|
|
return scores;
|
|
}
|
|
|
|
private static int Sum(List<int> l, int start, int count) =>
|
|
Enumerable.Range(start, count).Where(i => i < l.Count).Sum(i => l[i]);
|
|
|
|
// frameIndex is 0-based; cumScore is that frame's running total from ScoreFrames()
|
|
private static string BuildFrameString(int frameIndex, List<int> balls, int cumScore)
|
|
{
|
|
int b0 = balls.Count > 0 ? balls[0] : -1;
|
|
int b1 = balls.Count > 1 ? balls[1] : -1;
|
|
int b2 = balls.Count > 2 ? balls[2] : -1;
|
|
// frameNumber:score:field3:ball1:ball2:ball3:field7:field8 -- field3/7/8 have no
|
|
// confirmed reader in the client, 0 is safe.
|
|
return $"{frameIndex + 1}:{cumScore}:0:{b0}:{b1}:{b2}:0:0";
|
|
}
|
|
|
|
private static string BuildGst(int gameId, int round, List<int> allBallsThisGame, List<int> ballsPerFrameCount, int currentFrame)
|
|
{
|
|
var c = new int[21];
|
|
Array.Fill(c, -1);
|
|
for (int i = 0; i < allBallsThisGame.Count && i < 21; i++)
|
|
c[i] = allBallsThisGame[i];
|
|
|
|
var d = new int[10];
|
|
int cursor = 0;
|
|
for (int f = 0; f < 10; f++)
|
|
{
|
|
d[f] = cursor;
|
|
if (f < ballsPerFrameCount.Count) cursor += ballsPerFrameCount[f];
|
|
}
|
|
|
|
float floatA = currentFrame; // 1-indexed, must be >= 1.0
|
|
int intB = allBallsThisGame.Count;
|
|
|
|
return $"{gameId},{round},{floatA},{intB},{string.Join(",", c)},{string.Join(",", d)},0.0";
|
|
}
|
|
|
|
// Pulls the flat ball list + current gst/rid strings out of the tracked frame state,
|
|
// keeping FrameCardData/CurrentScore in sync with the public properties too.
|
|
private (string rid, string gst) SnapshotState(int roundIndex)
|
|
{
|
|
var flat = _frames.SelectMany(f => f).ToList();
|
|
_frameScores.Clear();
|
|
_frameScores.AddRange(ScoreFrames(flat));
|
|
|
|
string rid = _frames.Count > roundIndex
|
|
? BuildFrameString(roundIndex, _frames[roundIndex], _frameScores[roundIndex])
|
|
: BuildFrameString(roundIndex, new List<int>(), 0);
|
|
|
|
string gst = BuildGst(GameId, roundIndex + 1, flat, _frames.Select(f => f.Count).ToList(), roundIndex + 1);
|
|
|
|
FrameCardData = rid;
|
|
CurrentScore = _frameScores.Count > 0 ? _frameScores[^1] : 0;
|
|
|
|
return (rid, gst);
|
|
}
|
|
|
|
// Build the "last 3 frames" leaderboard entry safely, whatever _frames.Count currently is
|
|
private string BuildLeaderboardEntry()
|
|
{
|
|
int start = Math.Max(0, _frames.Count - 3);
|
|
var frameStrs = new List<string>();
|
|
for (int f = start; f < _frames.Count && frameStrs.Count < 3; f++)
|
|
frameStrs.Add(BuildFrameString(f, _frames[f], f < _frameScores.Count ? _frameScores[f] : 0));
|
|
while (frameStrs.Count < 3) frameStrs.Add(BuildFrameString(frameStrs.Count, new List<int>(), 0));
|
|
|
|
return $"1;{ScreenName},{CurrentScore},{string.Join(",", frameStrs)}";
|
|
}
|
|
|
|
public async Task StartGameLoop(NetworkStream stream)
|
|
{
|
|
for (int i = 0; i < FrameCount; i++) // main game loop
|
|
{
|
|
CurrentRoundNumber = i;
|
|
_frames.Add([]);
|
|
_frameDoneSignal = new TaskCompletionSource<bool>();
|
|
|
|
State = BowlingGameState.PreparingRound;
|
|
var (rid, gst) = SnapshotState(i);
|
|
|
|
if (i == 0)
|
|
{
|
|
// this is the first round, send full gm.pre message
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.stat"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("gmlr", "0"),
|
|
("state", GetStateString()),
|
|
("ktok", "1"),
|
|
("scrn", ScreenName),
|
|
("rid", rid),
|
|
("gst", gst),
|
|
("bowlcom.numRounds", "10"),
|
|
("bowlcom.jackpotPoints", "500"),
|
|
("bowlcom.ui.standard", "true"),
|
|
("bowlcom.tok.silver", "3,6,9"),
|
|
("bowlcom.tok.gold", "0,0,0"), // must be 3 comma-separated values, same as silver -- a
|
|
// single bare "0" throws a StringTokenizer NPE client-side
|
|
("bowlcom.fast", "false")
|
|
);
|
|
}
|
|
else
|
|
{
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.stat"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("gmlr", "0"),
|
|
("state", GetStateString()),
|
|
("ktok", "1"),
|
|
("scrn", ScreenName),
|
|
("rid", rid),
|
|
("gst", gst)
|
|
);
|
|
}
|
|
|
|
// wait prep timer
|
|
await Task.Delay(PrepTimerMili);
|
|
|
|
State = BowlingGameState.RoundStarted;
|
|
(rid, gst) = SnapshotState(i);
|
|
|
|
// send status update to indicate round is starting
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.stat"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("gmlr", "0"),
|
|
("state", GetStateString()),
|
|
("ktok", "1"),
|
|
("scrn", ScreenName),
|
|
("rid", rid),
|
|
("gst", gst)
|
|
);
|
|
|
|
// start round -- this is what actually starts the client's countdown UI
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.strt"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("mili", RoundTimerMili.ToString())
|
|
);
|
|
|
|
// wait for the player to finish the frame, or the round timer to run out --
|
|
// whichever happens first. RecordThrow() (called from your co.turn handler)
|
|
// completes _frameDoneSignal once the frame is done.
|
|
await Task.WhenAny(_frameDoneSignal.Task, Task.Delay(RoundTimerMili));
|
|
|
|
State = BowlingGameState.RoundOver;
|
|
(rid, gst) = SnapshotState(i);
|
|
|
|
// send status update with the now-final scorecard for this frame
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.stat"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("gmlr", "0"),
|
|
("state", GetStateString()),
|
|
("ktok", "1"),
|
|
("scrn", ScreenName),
|
|
("rid", rid),
|
|
("gst", gst)
|
|
);
|
|
|
|
// send end round
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.endr"),
|
|
("gid", GameId.ToString()),
|
|
("rnd", (CurrentRoundNumber + 1).ToString()),
|
|
("mili", BetweenRoundTimerMili.ToString()),
|
|
("csts", "0"),
|
|
("cprm", "0")
|
|
);
|
|
|
|
// send leaderboard update
|
|
LeaderboardData = BuildLeaderboardEntry();
|
|
await TCPGameServer.SendAsync(stream, ("msg.op", "so.gb.ledr"), ("lbrd", LeaderboardData));
|
|
|
|
await Task.Delay(BetweenRoundTimerMili);
|
|
}
|
|
|
|
// end of game -- the loop above never sent this before, so the client had no
|
|
// signal the game was actually over
|
|
State = BowlingGameState.GameEnded;
|
|
await TCPGameServer.SendAsync(stream,
|
|
("msg.op", "so.gb.endg"),
|
|
("gid", GameId.ToString()),
|
|
("mili", "0")
|
|
);
|
|
|
|
LeaderboardData = BuildLeaderboardEntry();
|
|
await TCPGameServer.SendAsync(stream, ("msg.op", "so.gb.ledr"), ("lbrd", LeaderboardData));
|
|
}
|
|
}
|
|
|
|
public enum BowlingGameState
|
|
{
|
|
PreparingRound,
|
|
RoundStarted,
|
|
RoundOver,
|
|
GameEnded
|
|
}
|
|
} |