using System.Collections.Generic; using System.Linq; using GameCore; using System; using UnityEngine; using asap.core; using game; using cfg; public class EventBingoModel { public readonly static int BoardSize = 5; public readonly static int NumberCount = BoardSize * BoardSize; public readonly static string SfxItemIn = "audio_ui_eventbingobaseball_in", SfxItemOut = "audio_ui_eventbingobaseball_out"; public const int RecordLength = 4; public const char Splitter = '|', Comma = ','; private int _stepInRound, _ticketCount, _roundCount, _eventId, _chainProgress, _chainTaskTokenProgress; private EventBingoRoundData _roundData; private EventBingoProgressTaskData _taskData; private EventBingoTableContext _ctx; private bool _isFirstTime; // This is actually used as a dequeue. public LinkedList NumberRollerDataList; public int[] BoardNumbers => _roundData.BoardNumbers; public EventBingoStepState StepState => _roundData[_stepInRound]; public int BoardState => StepState.BoardState; public int TicketCount => _ticketCount; public int RoundCount => _roundCount; public EventBingoProgressTaskData TaskData => _taskData; public int EventId => _eventId; public int StepInRound => _stepInRound; public bool IsActivated { get { if (_ctx == null) return false; var timeLimit = _ctx.GetTimeLimit(); return timeLimit.Item1 <= ZZTimeHelper.UtcNow() && ZZTimeHelper.UtcNow() < timeLimit.Item2; } } public bool IsFirstTime => _isFirstTime; public EventBingoTableContext Ctx => _ctx; public int PackId => _ctx.GetPackId(); public void Init(FishingEvent e) { _eventId = e.ID; RefreshTableContext(); _roundCount = 0; var seed = _ctx.PickSeed(_roundCount); _roundData = new EventBingoRoundData(seed); _stepInRound = 0; NumberRollerDataList = new LinkedList(); _taskData = _ctx.GetTaskData(); _ticketCount = 0; AddTicket(_ctx.GetWelcomeGift()); SetUiPanel(_ctx.GetUiData()); _isFirstTime = true; ToPlayfabData().Save(); } public void Load(EventBingoPlayfabData pfData, EventBingoPlayerPreferenceData ppData, FishingEvent e) { _eventId = pfData.EventId; RefreshTableContext(); _ticketCount = pfData.TicketCount; _roundCount = pfData.RoundCount; _stepInRound = pfData.StepInRound; _roundData = new EventBingoRoundData(pfData.Seed); if (!_roundData.ValidateStepIndex(_stepInRound)) { Debug.LogWarning($"[EventBingo] Wrong data detected. Step count out of range. Reset round data."); _stepInRound = 0; var seed = _ctx.PickSeed(_roundCount); _roundData = new EventBingoRoundData(seed); } _taskData = _ctx.GetTaskData(pfData.TaskId, pfData.TaskProgress); if (ppData == null || ppData.RollerDataList == null) NumberRollerDataList = new LinkedList(); else NumberRollerDataList = new LinkedList(ppData.RollerDataList); SetUiPanel(_ctx.GetUiData()); _isFirstTime = ppData == null ? true : ppData.IsFirstTime; } public void InitTest() { Debug.Log("[EventBingo] Abandon."); } public void NextStep() { _stepInRound++; var taskUpdate = _taskData.AddProgress(1); ToPlayfabData().Save(); // Debug.Log($"[EventBingo] Step{_stepInRound}: {StepState.Number}."); } public void NextRound() { _roundCount++; var seed = _ctx.PickSeed(_roundCount); _roundData = new EventBingoRoundData(seed); _stepInRound = 0; NumberRollerDataList.Clear(); ToPlayfabData().Save(); ToPlayerPreferenceData().Save(); } public bool CheckIfTaken(int index) { var flag = 1 << index; return (BoardState & flag) != 0; } public List GetBoardInitiationData() { var res = new List(); for (int i = 0; i < NumberCount; i++) { _roundData.MinorRewards.TryGetValue(i, out var minorReward); res.Add(new EventBingoBlockViewData { number = _roundData.BoardNumbers[i], reward = minorReward, haveItem = CheckIfTaken(i) }); } return res; } public void AddNumberRecord(int number) { _ctx.GetRandomItemIcon(out int iconIdx, out string iconUrl); NumberRollerDataList.AddLast(new EventBingoNumberRollerItemData { number = number, iconIdx = iconIdx, }); while (NumberRollerDataList.Count > RecordLength) NumberRollerDataList.RemoveFirst(); ToPlayerPreferenceData().Save(); } public bool AddTicket(int count) { _ticketCount += count; if (_ticketCount < 0) { _ticketCount -= count; return false; } EventBingoAct.EventAggregator.Publish(new EventBingoTicketUpdateEvent()); ToPlayfabData().Save(); GContext.container.Resolve().SaveTransitionData(_eventId, _ticketCount); return true; } public bool DeliverMinorReward(int index, out ItemData reward) { bool haveReward = _roundData.MinorRewards.TryGetValue(index, out reward); if (haveReward) { EventBingoSystem.GrantReward(reward); } else { // Debug.Log($"[EventBingo] No reward for idx {index}"); } return haveReward; } public void RefreshTableContext() { _ctx = new EventBingoTableContext(_eventId); } public EventBingoPlayfabData ToPlayfabData() { return new EventBingoPlayfabData( _eventId, _ticketCount, _roundCount, _stepInRound, _roundData.Seed, TaskData.TaskId, TaskData.Progress, _chainProgress, _chainTaskTokenProgress); } public EventBingoEntranceData ToEntranceData() { (var startTime, var expiryTime) = _ctx.GetTimeLimit(); bool needRedPoint = _ticketCount >= _ctx.GetRedPointThreshold(); return new EventBingoEntranceData { NeedRedPoint = needRedPoint, ExpiryTime = expiryTime, StartTime = startTime, Icon = _ctx.GetEntranceIcon(), }; } public EventBingoPlayerPreferenceData ToPlayerPreferenceData() { return new EventBingoPlayerPreferenceData { RollerDataList = NumberRollerDataList.ToList(), IsFirstTime = _isFirstTime }; } #region ChainPack public int GetChainProgress() { return _chainProgress; } public void SetChainProgress(int p) { _chainProgress = p; } public int GetTaskProgress() { return _chainTaskTokenProgress; } public void SetTaskProgress(int p) { _chainTaskTokenProgress = p; } public void Save() { ToPlayfabData().Save(); } #endregion private void SetUiPanel(EventBingoUiData panelData) { UITypes.EventBingoInfoPanel.SetType(panelData.InfoPanel); UITypes.EventBingoChainPackPanel.SetType(panelData.ChainPackPanel); } public bool IsHit(int number) { foreach (var n in BoardNumbers) { if (n == number) return true; } return false; } public void SetFirstTimeFlag() { _isFirstTime = false; ToPlayerPreferenceData().Save(); } public void ActivateInstantBingo() { _stepInRound = _roundData.StepCount - 2; } } public class EventBingoRoundData { private int _seed; public int[] BoardNumbers; private List _playerNumberSequence; private List _stateSequence; private List _playerIdxSequence; public Dictionary MinorRewards; public int Seed => _seed; public int StepCount => _stateSequence.Count; public EventBingoStepState this[int idx] { get { if (idx >= _stateSequence.Count || idx >= _playerNumberSequence.Count) { var actualIdx = Mathf.Min(_stateSequence.Count - 2, _playerNumberSequence.Count - 2); return new EventBingoStepState { Number = _playerNumberSequence[actualIdx], BoardState = _stateSequence[actualIdx], Index = _playerIdxSequence[actualIdx], }; throw new IndexOutOfRangeException( $"[EventBingo] Index {idx} out of range. Expected range: [0, {Math.Min(_stateSequence.Count, _playerNumberSequence.Count)}]"); } return new EventBingoStepState { Number = _playerNumberSequence[idx], BoardState = _stateSequence[idx], Index = _playerIdxSequence[idx] }; } } public EventBingoRoundData(int seed) { _playerNumberSequence = new List(); _playerIdxSequence = new List(); _stateSequence = new List(); UpdateWithSeed(seed); } public void UpdateWithSeed(int seed) { var model = GContext.container.Resolve(); _seed = seed; var rng = new System.Random(_seed); BoardNumbers = EventBingoSystem.GenerateBoardNumbers(rng).ToArray(); _playerNumberSequence.Clear(); _playerIdxSequence.Clear(); _stateSequence.Clear(); _playerNumberSequence.Add(-1); _playerIdxSequence.Add(-1); _stateSequence.Add(0b0); var numberPool = Enumerable.Range(1, 99).ToList(); int state = 0b0; var pickedIndexSet = new HashSet(); while (numberPool.Count > 0 && EventBingoSystem.CheckBingo(state, out _) <= 0) { var pickedNumber = EventBingoSystem.PickRandomNumberFromList(numberPool, rng); var pickedIdx = Array.IndexOf(BoardNumbers, pickedNumber); if (pickedIdx > -1) { pickedIndexSet.Add(pickedIdx); state |= 1 << pickedIdx; } _playerIdxSequence.Add(pickedIdx); _playerNumberSequence.Add(pickedNumber); _stateSequence.Add(state); } MinorRewards = new Dictionary(); model.Ctx.GetMinorDropData(rng, out var availableRewards, out var displayRewards); var allIndices = Enumerable.Range(0, EventBingoModel.NumberCount).ToList(); FtMathUtils.ShuffleList(allIndices, rng); var pickedIndices = pickedIndexSet.ToList(); FtMathUtils.ShuffleList(pickedIndices, rng); var unpickedIndices = allIndices.Except(pickedIndexSet).ToList(); FtMathUtils.ShuffleList(unpickedIndices, rng); // Assign rewards to picked positions for (int i = 0; i < availableRewards.Count && i < pickedIndices.Count; i++) { MinorRewards[pickedIndices[i]] = availableRewards[i]; Debug.Log($"[EventBingo] Available MiniDrop: item {availableRewards[i].id} @ idx{pickedIndices[i]}"); } // Assign display rewards to unpicked positions for (int i = 0; i < displayRewards.Count && i < unpickedIndices.Count; i++) { MinorRewards[unpickedIndices[i]] = displayRewards[i]; Debug.Log($"[EventBingo] Unavailable MiniDrop: item {displayRewards[i].id} @ idx {unpickedIndices[i]}"); } // var leftOverIndices = Enumerable.Range(0, EventBingoModel.NumberCount) // .Where(x => !pickedIndexSet.Contains(x)) // .ToHashSet() // .ToList() // .OrderBy(x => rng.Next()) // .ToList(); // var pickedIndexList = pickedIndexSet.ToList().OrderBy(x => rng.Next()).ToList(); // for (int i = 0; i < availableRewards.Count && i < pickedIndexList.Count; i++) // { // var idx = pickedIndexList.First(); // MinorRewards[idx] = availableRewards[i]; // pickedIndexList.Remove(idx); // } // for (int i = 0; i < displayRewards.Count && i < leftOverIndices.Count; i++) // { // var idx = leftOverIndices.First(); // MinorRewards[idx] = displayRewards[i]; // leftOverIndices.Remove(idx); // } } public int[] GetNumberRollerDataList(int step) { var startIdx = Mathf.Max(1, step - EventBingoModel.RecordLength + 1); var count = Mathf.Min(EventBingoModel.RecordLength, step); return _playerNumberSequence.GetRange(startIdx, count).ToArray(); } public bool ValidateStepIndex(int idx) { return idx < _playerNumberSequence.Count && idx < _playerIdxSequence.Count && idx < _stateSequence.Count; } } public class EventBingoStepState { public int BoardState; public int Number; public int Index; } public class EventBingoProgressTaskData { public int TaskId; public int Progress; public ItemData Reward; public int TargetProgress; public int NextTaskId; /// /// Increase progress and check if task is completed. /// /// Usually 1 in this event. /// True is the old task is completed. public bool AddProgress(int count) { Progress += count; if (Progress >= TargetProgress) { EventBingoSystem.GrantReward(Reward); MoveToNextTask(); return true; } return false; } private void MoveToNextTask() { var nextTask = GContext.container.Resolve().Ctx.GetTaskData(NextTaskId); TaskId = NextTaskId; Progress = 0; Reward = nextTask.Reward; TargetProgress = nextTask.TargetProgress; NextTaskId = nextTask.NextTaskId; } } public class EventBingoPlayfabData { public int EventId, TicketCount, RoundCount, StepInRound, Seed, TaskId, TaskProgress, ChainProgress, ChainTaskTokenProgress; private const int TokenCount = 9; private const string Key = "EventBingo"; private string Serialize() { var sb = new System.Text.StringBuilder(); sb.Append(EventId).Append(EventBingoModel.Splitter); sb.Append(TicketCount).Append(EventBingoModel.Splitter); sb.Append(RoundCount).Append(EventBingoModel.Splitter); sb.Append(StepInRound).Append(EventBingoModel.Splitter); sb.Append(Seed).Append(EventBingoModel.Splitter); sb.Append(TaskId).Append(EventBingoModel.Splitter); sb.Append(TaskProgress).Append(EventBingoModel.Splitter); sb.Append(ChainProgress).Append(EventBingoModel.Splitter); sb.Append(ChainTaskTokenProgress).Append(EventBingoModel.Splitter); return sb.ToString(); } private static EventBingoPlayfabData Deserialize(string s) { if (s == null || s == "") return null; var tokens = s.Trim(EventBingoModel.Splitter).Split(EventBingoModel.Splitter); if (tokens.Length != TokenCount) { Debug.LogError($"[EventBingo]Wrong amount of parameters. Expect {TokenCount}, but got {tokens.Length} in \"{s}\"."); return null; } var eventId = int.Parse(tokens[0]); var ticketCount = int.Parse(tokens[1]); var roundCount = int.Parse(tokens[2]); var stepInRound = int.Parse(tokens[3]); var seed = int.Parse(tokens[4]); var taskId = int.Parse(tokens[5]); var taskProgress = int.Parse(tokens[6]); var chainProgress = int.Parse(tokens[7]); var chainTaskTokenProgress = int.Parse(tokens[8]); return new EventBingoPlayfabData(eventId, ticketCount, roundCount, stepInRound, seed, taskId, taskProgress, chainProgress, chainTaskTokenProgress); } public EventBingoPlayfabData(int eventId, int ticketCount, int roundCount, int stepInRound, int seed, int taskId, int taskProgress, int chainProgress, int chainTaskTokenProgress) { EventId = eventId; TicketCount = ticketCount; RoundCount = roundCount; StepInRound = stepInRound; Seed = seed; TaskId = taskId; TaskProgress = taskProgress; ChainProgress = chainProgress; ChainTaskTokenProgress = chainTaskTokenProgress; } public void Save() { PlayFabMgr.Instance.UpdateUserDataValue(Key, Serialize()); } public static EventBingoPlayfabData Load() { var s = PlayFabMgr.Instance.GetLocalData(Key); return Deserialize(s); } } public class EventBingoEntranceData { public bool NeedRedPoint; public DateTime ExpiryTime; public DateTime StartTime; public string Icon; } public class EventBingoPlayerPreferenceData { public List RollerDataList; public bool IsFirstTime = true; private readonly static string Key = "EventBingo" + GContext.container.Resolve().UserId; private string Serialize() { var sb = new System.Text.StringBuilder(); sb.Append(IsFirstTime ? "1" : "0").Append(EventBingoModel.Splitter); foreach (var data in RollerDataList) sb.Append(data.number).Append(EventBingoModel.Comma).Append(data.iconIdx).Append(EventBingoModel.Splitter); return sb.ToString(); } private static EventBingoPlayerPreferenceData Deserialize(string s) { var tokens = s.Trim(EventBingoModel.Splitter).Split(EventBingoModel.Splitter); var rollerData = new List(); bool isFirstTime; if (tokens.Length < 1) { throw new Exception("[EventBingo] Invalid serialized data, length < 1"); } isFirstTime = tokens[0] == "1"; for (int i = 1; i < tokens.Length; i++) { var token = tokens[i]; var tokenTokens = token.Split(EventBingoModel.Comma); if (tokenTokens.Length != 2) throw new Exception("[EventBingo] Wrong amount of parameters in " + token); rollerData.Add(new EventBingoNumberRollerItemData { number = int.Parse(tokenTokens[0]), iconIdx = int.Parse(tokenTokens[1]) }); } return new EventBingoPlayerPreferenceData { RollerDataList = rollerData, IsFirstTime = isFirstTime }; } public void Save() { PlayerPrefs.SetString(Key, Serialize()); } public static EventBingoPlayerPreferenceData Load() { var s = PlayerPrefs.GetString(Key); if (s == null || s == "") return null; try { var res = Deserialize(s); return res; } catch (Exception e) { Debug.LogError(e); return null; } } } public class EventBingoTicketUpdateEvent { } public class EventBingoUiData { public string InfoPanel { get; set; } public string ChainPackPanel { get; set; } }