Repository: MgAl2O4/FFTriadBuddy Branch: master Commit: 425f72ecb3ba Files: 169 Total size: 1.7 MB Directory structure: gitextract_9t11ryri/ ├── .gitignore ├── FFTriadBuddy.sln ├── LICENSE ├── README.md ├── assets/ │ └── data/ │ ├── cards.xml │ ├── hashes.xml │ ├── loc.xml │ ├── npcs.xml │ └── tournaments.xml ├── datasource/ │ └── !Start.bat ├── ml/ │ ├── patternMatch/ │ │ ├── nn.py │ │ ├── run-everything.bat │ │ ├── train-cactpot.py │ │ └── train-triad.py │ └── solver/ │ ├── agents/ │ │ ├── agent.py │ │ ├── agentDQN.py │ │ └── agentRandom.py │ ├── environment.py │ ├── gameSession.py │ ├── gamelogic/ │ │ ├── triadCard.py │ │ ├── triadDeck.py │ │ ├── triadGame.py │ │ └── triadMods.py │ ├── main.py │ ├── perfTests.py │ └── utils/ │ ├── codeGenerator.py │ ├── estimatorTorch.py │ └── trainingMemory.py └── sources/ ├── App.config ├── FFTriadBuddy.csproj ├── Properties/ │ ├── AssemblyInfo.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── data/ │ ├── ImageHashDB.cs │ ├── LocalizationDB.cs │ ├── PlayerSettingsDB.cs │ ├── TriadCardDB.cs │ ├── TriadNpcDB.cs │ └── TriadTournamentDB.cs ├── gamelogic/ │ ├── FavDeckSolver.cs │ ├── MiniCactpotGame.cs │ ├── TriadCard.cs │ ├── TriadDeck.cs │ ├── TriadDeckOptimizer.cs │ ├── TriadGameAgent.cs │ ├── TriadGameModifier.cs │ ├── TriadGameScreenMemory.cs │ ├── TriadGameSimulation.cs │ ├── TriadGameSolver.cs │ └── tests/ │ ├── TriadGameScreenTests.cs │ └── TriadGameTests.cs ├── googleapi/ │ ├── GoogleClientMissingIdentifiers.cs │ ├── GoogleDriveService.cs │ └── GoogleOAuth2.cs ├── loc/ │ ├── strings.cs │ ├── strings.de.resx │ ├── strings.es.resx │ ├── strings.fr.resx │ ├── strings.ja.resx │ ├── strings.ko.resx │ ├── strings.resx │ └── strings.zh.resx ├── ui/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── modelproxy/ │ │ ├── BulkObservableCollection.cs │ │ ├── CardModelProxy.cs │ │ ├── IconDB.cs │ │ ├── ImageHashDataModelProxy.cs │ │ ├── ModelProxyDB.cs │ │ ├── NpcModelProxy.cs │ │ ├── RuleModelProxy.cs │ │ ├── SettingsModel.cs │ │ ├── TournamentModelProxy.cs │ │ └── TriadGameModel.cs │ ├── view/ │ │ ├── AdjustCardDialog.xaml │ │ ├── AdjustCardDialog.xaml.cs │ │ ├── AdjustHashDialog.xaml │ │ ├── AdjustHashDialog.xaml.cs │ │ ├── DialogWindow.xaml │ │ ├── DialogWindow.xaml.cs │ │ ├── FavDeckEditDialog.xaml │ │ ├── FavDeckEditDialog.xaml.cs │ │ ├── FavDeckPreview.xaml │ │ ├── FavDeckPreview.xaml.cs │ │ ├── MainWindow.xaml │ │ ├── MainWindow.xaml.cs │ │ ├── OverlayWindowInteractive.xaml │ │ ├── OverlayWindowInteractive.xaml.cs │ │ ├── OverlayWindowTransparent.xaml │ │ ├── OverlayWindowTransparent.xaml.cs │ │ ├── PageCards.xaml │ │ ├── PageCards.xaml.cs │ │ ├── PageInfo.xaml │ │ ├── PageInfo.xaml.cs │ │ ├── PageNpcs.xaml │ │ ├── PageNpcs.xaml.cs │ │ ├── PageScreenshot.xaml │ │ ├── PageScreenshot.xaml.cs │ │ ├── PageSetup.xaml │ │ ├── PageSetup.xaml.cs │ │ ├── PageSimulate.xaml │ │ ├── PageSimulate.xaml.cs │ │ ├── controls/ │ │ │ ├── NumTextBox.xaml │ │ │ ├── NumTextBox.xaml.cs │ │ │ ├── OutlinedTextBlock.cs │ │ │ ├── SearchableComboBox.xaml │ │ │ └── SearchableComboBox.xaml.cs │ │ ├── controls-triad/ │ │ │ ├── CardGridView.xaml │ │ │ ├── CardGridView.xaml.cs │ │ │ ├── CardView.xaml │ │ │ ├── CardView.xaml.cs │ │ │ ├── DeckView.xaml │ │ │ ├── DeckView.xaml.cs │ │ │ ├── PlayerDeckPreview.xaml │ │ │ └── PlayerDeckPreview.xaml.cs │ │ └── utils/ │ │ ├── CanvasExtensions.cs │ │ ├── CardDragDropExtension.cs │ │ ├── Converters.cs │ │ ├── ListViewExtensions.cs │ │ ├── OverlayWindowService.cs │ │ └── ViewUtils.cs │ └── viewmodel/ │ ├── AdjustCardViewModel.cs │ ├── AdjustHashViewModel.cs │ ├── CardCollectionViewModel.cs │ ├── CardViewModel.cs │ ├── ContextActionViewModel.cs │ ├── DeckViewModel.cs │ ├── FavDeckEditViewModel.cs │ ├── ImageCardDataViewModel.cs │ ├── LocalSavesViewModel.cs │ ├── MainWindowViewModel.cs │ ├── OverlayWindowViewModel.cs │ ├── PageCardsViewModel.cs │ ├── PageInfoViewModel.cs │ ├── PageNpcsViewModel.cs │ ├── PageScreenshotViewModel.cs │ ├── PageSetupViewModel.cs │ ├── PageSimulateViewModel.cs │ ├── SetupFavDeckViewModel.cs │ ├── SimulateRulesViewModel.cs │ ├── SolvableDeckViewModel.cs │ ├── ViewModelUtils.cs │ └── WinChanceViewModel.cs ├── utils/ │ ├── DataCoverter.cs │ ├── TestManager.cs │ ├── XInputStub.cs │ ├── datamine/ │ │ ├── CsvData.cs │ │ └── GameData.cs │ └── tlsh/ │ ├── BucketSize.cs │ ├── ChecksumSize.cs │ ├── TlshBuilder.cs │ ├── TlshHash.cs │ └── TlshUtilities.cs ├── utils-shared/ │ ├── AssetManager.cs │ ├── GithubUpdater.cs │ ├── JsonParser.cs │ ├── LocResourceManager.cs │ ├── Logger.cs │ ├── MLDataExporter.cs │ └── MLUtils.cs └── vision/ ├── ImageUtils.cs ├── MLClassifierCactpot.cs ├── MLClassifierTriadDigit.cs ├── ScannerBase.cs ├── ScannerCactpot.cs ├── ScannerTriad.cs ├── ScreenAnalyzer.cs └── ScreenReader.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.json *.lnk sources/assets.zip sources/googleapi/GoogleClientIdentifiers.cs test/* datasource/* releases/* ml/*/data/* ml/.vscode/* !datasource/!Start.bat !dalamud/*.json ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- Backup*.rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ ================================================ FILE: FFTriadBuddy.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27703.2026 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFTriadBuddy", "sources\FFTriadBuddy.csproj", "{4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4BB10FD8-07CE-4000-A373-6D18CE17C244} EndGlobalSection EndGlobal ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 MgAl2O4 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # FFTriadBuddy Helper program for Triple Triad minigame in [Final Fantasy XIV](https://www.finalfantasyxiv.com/), NPC matches only. (All used icons are property of SQUARE-ENIX Ltd. All rights reserved) Features: * suggest next move in standalone simulation * suggest next move with in-game overlay (screenshot based) * organize cards and npc fights * prepare deck for given npc * all game rules are supported * **super secret** Mini Cactpot mode for in-game overlay Dalamud plugin can be found here: https://github.com/MgAl2O4/FFTriadBuddyDalamud ## Instructions 1. Download [latest release](https://github.com/MgAl2O4/FFTriadBuddy/releases/latest) 2. Unpack and run. Program will attempt to auto update on startup from this repository. ## Known issues UI buttons glitching out after mouse over: Please refer to [this bug report](https://github.com/MgAl2O4/FFTriadBuddy/issues/53#issuecomment-879286853) for solution, it's a known incompatibility between UI framework and Nahimic software. ## Bug reports Bug happens. Whenever it's related to Play: Screenshot mode not recognizing images/cards correctly, please include a screenshot in bug report. Ideally, the one saved by tool in secret-debug-mode, which is exact input image for processing. * make sure that game show broken state and board is not obscured * switch to Play: Simulate tab, press [F12] and hit "Apply rule" button * look for screenshot-source-xx.jpg file, saved next to tool's executable ## Translation Localization of game data (cards, npcs, rules, tournaments, etc) is created from client datamining and is more or less automatic. Tool's UI is now separate from it, but relies on people to contribute translations. You can help with translation here: https://crowdin.com/project/fftriadbuddy Contact: MgAl2O4@protonmail.com ================================================ FILE: assets/data/cards.xml ================================================  ================================================ FILE: assets/data/hashes.xml ================================================ ================================================ FILE: assets/data/loc.xml ================================================  ================================================ FILE: assets/data/npcs.xml ================================================  ================================================ FILE: assets/data/tournaments.xml ================================================  ================================================ FILE: datasource/!Start.bat ================================================ @echo off setlocal if not exist SaintCoinach goto MISSING_SAINT if not exist SetUserVars.bat goto MISSING_PATH call SetUserVars.bat if %GamePath%=="" goto MISSING_PATH echo Game path: %GamePath% echo. echo Export data using commands: echo ui 87000 88999 echo allexd echo exit echo. pushd SaintCoinach if exist SaintCoinach.History.zip ( del SaintCoinach.History.zip > nul ) SaintCoinach.Cmd.exe %GamePath% popd FOR /F "tokens=* USEBACKQ" %%F IN (`dir SaintCoinach\2* /A:D /B`) DO ( SET DataPath=%%F ) echo. rmdir export /s /q > nul echo Copying exported data from: %DataPath% xcopy SaintCoinach\%DataPath%\*.* export\ /e > nul del ..\assets\icons\*.png xcopy export\ui\icon\087000\*.png ..\assets\icons\ /s > nul xcopy export\ui\icon\088000\*.png ..\assets\icons\ /s > nul rmdir SaintCoinach\%DataPath% /s /q > nul :GIT_MIRRORS SET /P CANDOWNLOAD=Do you want to download from github mirrors (curl required) (Y/[N])? IF /I "%CANDOWNLOAD%" NEQ "Y" GOTO EXPORTED rem raw blobs with URL like: rem https://raw.githubusercontent.com/thewakingsands/ffxiv-datamining-cn/master/ENpcResident.csv call :CURL_WORKER cn thewakingsands/ffxiv-datamining-cn master call :CURL_WORKER ko Ra-Workspace/ffxiv-datamining-ko master/csv goto EXPORTED :CURL_WORKER echo Downloading from: %2... for %%F in (ENpcResident Item PlaceName TripleTriadCard TripleTriadCardType TripleTriadRule TripleTriadCompetition TripleTriadCardResident TripleTriadResident) do ( curl https://raw.githubusercontent.com/%2/%3/%%F.csv --output export\exd-all\%%F.%1.csv --silent ) exit /b :EXPORTED echo Done! echo. echo Run DEBUG build with -dataConvert cmdline to process data tables. echo Output logs will show all needed information goto FINISHED :MISSING_PATH echo Game path not set, edit SetUserVars.bat and update GamePath variable echo. echo Example: "C:\Games\SquareEnix\FINAL FANTASY XIV - A Realm Reborn" echo set GamePath="C:\Games\SquareEnix\FINAL FANTASY XIV - A Realm Reborn" > SetUserVars.bat goto FINISHED :MISSING_SAINT echo Can't find SaintCoinach binaries! echo. echo Grab latest release of SaintCoinach.Cmd from github: echo https://github.com/xivapi/SaintCoinach/releases echo. echo Expected path: datasource/SaintCoinach/SaintCoinach.Cmd.exe goto FINISHED :FINISHED echo. pause ================================================ FILE: ml/patternMatch/nn.py ================================================ import tensorflow as tf import numpy as np import json import textwrap from tensorflow import keras from tqdm.keras import TqdmCallback class NNTraining(): def __init__(self, inputFile, outputFile, labelKey="output"): path = 'data/' self.inputFile = path + inputFile self.outputFile = path + outputFile self.labelKey = labelKey pass def printToLines(self, prefix, values, suffix): longstr = prefix + ', '.join(('%ff' % v) for v in values) + suffix return textwrap.wrap(longstr, 250) def loadData(self): with open(self.inputFile) as file: training_sets = json.load(file) inputs = [] outputs = [] for elem in training_sets["dataset"]: inputs.append(elem["input"]) outputs.append(elem[self.labelKey]) return inputs, outputs def writeCodeFile(self, model, codeSuffix): lines = [] for i in range(len(model.layers)): layer = model.layers[i] print('Layer[%i]:' % i) print(' input_shape:', layer.input_shape) print(' output_shape:', layer.output_shape) weights = layer.get_weights() for w in weights: print(' w.shape:', w.shape) print(' use_bias:', layer.use_bias) print(' activation:', layer.activation) if (len(weights) == 2 and layer.use_bias): listWeights = np.reshape(weights[0], -1) listBias = np.reshape(weights[1], -1) lines += self.printToLines('Layer%s%iW = new float[]{' % (codeSuffix, i), listWeights, '};') lines += self.printToLines('Layer%s%iB = new float[]{' % (codeSuffix, i), listBias, '};') with open(self.outputFile, "w") as file: for line in lines: file.write(line) file.write("\n") def run(self, numHidden1, numHidden2=0, numEpochs=20, batchSize=512, codeSuffix=''): x_train, y_train = self.loadData() x_train = np.array(x_train, np.float32) numClasses = max(y_train) + 1 train_data = tf.data.Dataset.from_tensor_slices((x_train, y_train)) train_data = train_data.repeat().shuffle(5000).batch(batchSize).prefetch(1) layers = [] if (numHidden1 > 0): layers += [ tf.keras.layers.Dense(numHidden1, activation='relu') ] if (numHidden2 > 0): layers += [ tf.keras.layers.Dense(numHidden2, activation='relu') ] layers += [ tf.keras.layers.Dense(numClasses) ]; model = tf.keras.Sequential(layers) model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=['accuracy']) model.fit(train_data, epochs=numEpochs, steps_per_epoch=100, verbose=0, callbacks=[TqdmCallback(verbose=2)]) self.writeCodeFile(model, codeSuffix) ================================================ FILE: ml/patternMatch/run-everything.bat ================================================ @echo off set ToolPath=%localappdata%/Programs/Python/Python38 for %%i in (train-*.py) do %ToolPath%/python %%i pause ================================================ FILE: ml/patternMatch/train-cactpot.py ================================================ from nn import NNTraining training = NNTraining(inputFile='ml-cactpot.json', outputFile='ml-cactpot.txt') training.run(numHidden1=80, numEpochs=500) ================================================ FILE: ml/patternMatch/train-triad.py ================================================ from nn import NNTraining training = NNTraining(inputFile='ml-triad.json', outputFile='ml-triad.txt') training.run(numHidden1=64, numEpochs=200) ================================================ FILE: ml/solver/agents/agent.py ================================================ class Agent: def __init__(self, game): pass def findAction(self, game, state): raise RuntimeError() def findTrainingAction(self, game, state): raise RuntimeError() def onTrainingGameStart(self, game, playerId): pass def onTrainingGameEnd(self, game, playerId): pass def onTrainingStep(self, game, playerId, state, action, nextState, reward): pass def train(self): pass def save(self, name): pass def load(self, name): pass def getTrainingDetails(self): return {} def generateModelCode(self, name): pass ================================================ FILE: ml/solver/agents/agentDQN.py ================================================ import os import math import numpy as np from .agent import Agent from utils.trainingMemory import TrainingMemoryCircular from utils.estimatorTorch import EstimatorModel # Deep Q Network # class AgentDQN(Agent): def __init__(self, game): self.memorySize = 1 * 1000 * 1000 self.batchSize = 256 self.numLayersHidden = [500, 500] self.learningRate = 0.0001 self.discountFactor = 0.99 self.epsilonStart = 1 self.epsilonEnd = 0.1 self.epsilonDecay = 2000 self.numActions = game.getMaxActions() numInputs = len(game.getState(0)) self.estimatorQ = EstimatorModel(numInputs, self.numLayersHidden, self.numActions, self.learningRate) self.estimatorTarget = EstimatorModel(numInputs, self.numLayersHidden, self.numActions, self.learningRate) self.replayMemory = TrainingMemoryCircular(self.memorySize) self.epsilon = self.epsilonStart self.numSteps = 0 self.historyLoss = [] self.historyReward = [] self.trainedOnce = False def findAction(self, game, state): probs = self.estimatorQ.predict(np.expand_dims(state, 0))[0] probs = self.sanitizeActions(game, state, probs) return np.argmax(probs) def findTrainingAction(self, game, state): useRandom = np.random.random() < self.epsilon if useRandom or not self.trainedOnce: allowedActions = game.getAllowedActions(state) return np.random.choice(allowedActions) return self.findAction(game, state) def onTrainingStep(self, game, playerId, state, action, nextState, reward): self.numSteps += 1 self.epsilon = self.epsilonEnd + ((self.epsilonStart - self.epsilonEnd) * math.exp(-1.0 * self.numSteps / self.epsilonDecay)) memorySample = (state, action, nextState, reward, game.isFinished()) self.historyReward.append(reward) self.replayMemory.add(memorySample) if len(self.replayMemory) >= self.batchSize: self.trainOnBatch(game) def train(self): updateTargetCheckpoint = 'updateTarget.tmp' self.estimatorQ.save(updateTargetCheckpoint) self.estimatorTarget.load(updateTargetCheckpoint) os.remove(updateTargetCheckpoint) self.trainAvgLoss = np.average(self.historyLoss) self.trainAvgReward = np.average(self.historyReward) self.historyLoss = [] self.historyReward = [] def save(self, name): self.estimatorQ.save(name) def load(self, name): self.estimatorQ.load(name) self.estimatorTarget.load(name) def getTrainingDetails(self): return { 'epsilon': self.epsilon, 'memory': len(self.replayMemory) / self.replayMemory.capacity, 'loss': self.trainAvgLoss, 'reward': self.trainAvgReward, } def generateModelCode(self, name): self.estimatorQ.generateModelCode(name) def sanitizeActions(self, game, state, actionValues, badValue = -np.inf): maskedValues = badValue * np.ones(self.numActions, dtype=float) allowedActions = game.getAllowedActions(state) maskedValues[allowedActions] = actionValues[allowedActions] return maskedValues def trainOnBatch(self, game): states, actions, nextStates, rewards, dones = self.replayMemory.sample(self.batchSize) states = np.array(states) targets = self.estimatorTarget.predict(states) for i in range(self.batchSize): targets[i] = self.sanitizeActions(game, states[i], targets[i], badValue=-1) bestActions = np.argmax(targets, axis=1) nextTargets = self.estimatorTarget.predict(nextStates) predictedRewards = rewards + (np.invert(dones).astype(np.float) * self.discountFactor * nextTargets[np.arange(self.batchSize), bestActions]) loss = self.estimatorQ.fit(states, actions, predictedRewards) self.historyLoss.append(loss) self.trainedOnce = True ================================================ FILE: ml/solver/agents/agentRandom.py ================================================ import random from .agent import Agent class AgentRandom(Agent): def __init__(self, game): self.randGen = random.Random() def setSeed(self, seedValue): self.randGen = random.Random(seedValue) def findAction(self, game, state): actions = game.getAllowedActions(state) return self.randGen.choice(actions) def findTrainingAction(self, game, state): return self.findAction(game, state) ================================================ FILE: ml/solver/environment.py ================================================ class Environment(object): def __init__(self, game, playerAgents): self.game = game self.players = playerAgents def runTrainingGame(self): self.game.init() for idx in range(len(self.players)): self.players[idx].onTrainingGameStart(self.game, idx) while not self.game.isFinished(): playerId = self.game.getCurrentPlayer() state = self.game.getState(playerId) action = self.players[playerId].findTrainingAction(self.game, state) self.game.step(action) # TODO: reuse nextState in next step, it contains relative data relative to player though (board owner, card visibility) nextState = self.game.getState(playerId) reward = self.game.getReward(playerId) self.players[playerId].onTrainingStep(self.game, playerId, state, action, nextState, reward) for idx in range(len(self.players)): self.players[idx].onTrainingGameEnd(self.game, idx) def runEvalGame(self): self.game.init() while not self.game.isFinished(): playerId = self.game.getCurrentPlayer() state = self.game.getState(playerId) action = self.players[playerId].findAction(self.game, state) self.game.step(action) playerRewards = [] for idx in range(len(self.players)): playerRewards.append(self.game.getReward(idx)) return playerRewards ================================================ FILE: ml/solver/gameSession.py ================================================ from gamelogic.triadCard import TriadCardDB from gamelogic.triadDeck import TriadDeck from gamelogic.triadGame import TriadGame, TriadGameState from gamelogic.triadMods import TriadMod import random class GameSession(): def __init__(self, useModifiers = False): self.randomSeed = 0 self.useModifiers = useModifiers self.init() def init(self): self.game = TriadGame() if self.useModifiers: self.game.mods = TriadMod.generateMods() self.game.redDeck = TriadDeck.generateDeckPlayer() self.game.blueDeck = TriadDeck.generateDeckPlayer() self.game.blueDeck.makeAllVisible() self.game.state = TriadGameState.BlueTurn if (random.random() < 0.5) else TriadGameState.RedTurn self.game.initModifiers() self.game.onTurnStart() def step(self, action): cardIdx = int(action / 9) boardIdx = action % 9 ownerId = TriadGame.OwnerIdBlue if (self.game.state == TriadGameState.BlueTurn) else TriadGame.OwnerIdRed result = self.game.placeCardFromDeck(boardIdx, cardIdx, ownerId) if not result: raise RuntimeError('Step failed, random seed:', self.randomSeed) def isFinished(self): return (self.game.state != TriadGameState.BlueTurn) and (self.game.state != TriadGameState.RedTurn) def getCurrentPlayer(self): return 0 if self.game.state == TriadGameState.BlueTurn else 1 @staticmethod def getMaxActions(): return 9 * 5 def getAllowedActions(self, state): actions = [] for idxB in range(9): # is board pos available? (avail flag = 1) if state[idxB] == 1: for idxC in range(5): # is card available? (avail flag = 1) if state[idxC + 9] == 1: actions.append((idxC * 9) + idxB) return actions def getReward(self, playerId): if self.game.state == TriadGameState.EndBlueWin: return 1 if playerId == 0 else -1 if self.game.state == TriadGameState.EndRedWin: return -1 if playerId == 0 else 1 if self.game.state == TriadGameState.EndDraw: return 0.1 return 0 def getState(self, playerId): state = [] # precalc values ownerPlayer = TriadGame.OwnerIdBlue if playerId == 0 else TriadGame.OwnerIdRed deckPlayer = self.game.blueDeck if playerId == 0 else self.game.redDeck deckOpp = self.game.redDeck if playerId == 1 else self.game.blueDeck cardInfoPlayer = deckPlayer.getCardsInfo(True) cardInfoOpp = deckOpp.getCardsInfo() # one-hot: valid board placement if self.game.forcedBoardIdx >= 0: state += [1 if (pos == self.game.forcedBoardIdx) else 0 for pos in range(len(self.game.owner))] else: state += [1 if (ownerId == 0) else 0 for ownerId in self.game.owner] # one-hot: valid cards for move's owner if self.game.forcedCardIdx >= 0: state += [1 if (cardIdx == self.game.forcedCardIdx) else 0 for cardIdx in range(5)] else: state += [1 if (cardState != TriadDeck.cardNone) else 0 for cardState in deckPlayer.state] # one-hot: active modifiers allGameMods = [mod.name for mod in TriadMod.modDB] activeMods = [mod.name for mod in self.game.mods] state += [1 if (s in activeMods) else 0 for s in allGameMods] # value of type modes state += self.game.typeMod # board cells: [ relative ownerId, type, sides 0..3] for pos in range(len(self.game.owner)): cellInfo = [0, 0, 0, 0, 0, 0] if self.game.owner[pos] != 0: cardOb = self.game.board[pos] cellInfo = [1 if self.game.owner[pos] == ownerPlayer else -1, cardOb.cardType, cardOb.sides[0], cardOb.sides[1], cardOb.sides[2], cardOb.sides[3]] state += cellInfo # card data for move owner & opponent for i in range(5): if cardInfoPlayer[i][0] != 0: sides = cardInfoPlayer[i][2].sides state += [ cardInfoPlayer[i][0], sides[0], sides[1], sides[2], sides[3], cardInfoPlayer[i][3] ] else: state += [ 0, 0, 0, 0, 0, 0 ] if cardInfoOpp[i][0] != 0: sides = cardInfoOpp[i][2].sides if (cardInfoOpp[i][2] != None) else TriadCardDB.mapAvgSides[cardInfoOpp[i][3]] state += [ cardInfoOpp[i][0], cardInfoOpp[i][1], sides[0], sides[1], sides[2], sides[3], cardInfoOpp[i][3] ] else: state += [ 0, 0, 0, 0, 0, 0, 0 ] return state ================================================ FILE: ml/solver/gamelogic/triadCard.py ================================================ from xml.dom import minidom class TriadCard: def __init__(self, name, rarity, cardType, sideUp, sideLeft, sideDown, sideRight): self.name = name self.rarity = rarity self.cardType = cardType self.sides = [ sideUp, sideLeft, sideDown, sideRight ] self.valid = True self.id = 0 def __str__(self): if self.valid == False: return "invalid" return 'type:%s, sides:[T:%i, L:%i, D:%i, R:%i], name:%s %s' % ( self.cardType, self.sides[0], self.sides[1], self.sides[2], self.sides[3], self.name, '*' * (self.rarity + 1)) class TriadCardDB: typeList = ['None', 'Primal', 'Scion', 'Beastman', 'Garlean'] rarityList = ['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'] cards = [] # [rarity] = list of cards mapRarity = [] mapAvgSides = [] @staticmethod def load(): assetFolder = '../../assets/data/' if (len(TriadCardDB.cards) > 0): return unknownCard = TriadCard('', 0, 0, 0, 0, 0, 0) unknownCard.valid = False TriadCardDB.cards = [ unknownCard ] TriadCardDB.mapRarity = [] TriadCardDB.mapAvgSides = [] for i in range(len(TriadCardDB.rarityList)): TriadCardDB.mapRarity.append([]) TriadCardDB.mapAvgSides.append([1,1,1,1]) cardNames = {} locXml = minidom.parse(assetFolder + 'loc.xml') locElems = locXml.getElementsByTagName('loc') for elem in locElems: locType = int(elem.attributes['type'].value) if locType == 3: cardNames[elem.attributes['id'].value] = elem.attributes['en'].value cardXml = minidom.parse(assetFolder + 'cards.xml') cardElems = cardXml.getElementsByTagName('card') for elem in cardElems: cardOb = TriadCard( cardNames[elem.attributes['id'].value], int(elem.attributes['rarity'].value), int(elem.attributes['type'].value), int(elem.attributes['up'].value), int(elem.attributes['lt'].value), int(elem.attributes['dn'].value), int(elem.attributes['rt'].value)) cardOb.id = len(TriadCardDB.cards) TriadCardDB.cards.append(cardOb) TriadCardDB.mapRarity[cardOb.rarity].append(cardOb) for i in range(len(TriadCardDB.rarityList)): avgSidesAcc = [0, 0, 0, 0] for card in TriadCardDB.mapRarity[i]: avgSidesAcc[0] += card.sides[0] avgSidesAcc[1] += card.sides[1] avgSidesAcc[2] += card.sides[2] avgSidesAcc[3] += card.sides[3] for side in range(4): TriadCardDB.mapAvgSides[i][side] = avgSidesAcc[side] / len(TriadCardDB.mapRarity[i]) print('Loaded cards:',len(TriadCardDB.cards)) @staticmethod def find(name): for card in TriadCardDB.cards: if (card.name == name): return card return TriadCardDB.cards[0] # load cards on startup TriadCardDB.load() ================================================ FILE: ml/solver/gamelogic/triadDeck.py ================================================ from .triadCard import * import random class TriadDeck(): cardNone = 0 cardHidden = 1 cardVisible = 2 def __init__(self): self.cards = [ None, None, None, None, None ] self.state = [ TriadDeck.cardNone, TriadDeck.cardNone, TriadDeck.cardNone, TriadDeck.cardNone, TriadDeck.cardNone ] self.usedRarityCount = [ 0 ] * len(TriadCardDB.rarityList) self.expectedRarityCount = [0, 0, 3, 1, 1] self.numAvail = 0 def initialize(self, cards, state): self.cards = cards self.numAvail = len(cards) for i in range(len(self.state)): self.state[i] = state def hasCard(self, idx): return self.state[idx] != TriadDeck.cardNone def useCard(self, idx): card = self.cards[idx] self.cards[idx] = None self.state[idx] = TriadDeck.cardNone self.numAvail -= 1 self.usedRarityCount[card.rarity] += 1 return card def makeAllVisible(self): for i in range(len(self.state)): self.state[i] = TriadDeck.cardVisible def onRestart(self): self.numAvail = len(self.cards) self.usedRarityCount = [ 0 ] * len(TriadCardDB.rarityList) def getCardsInfo(self, forceVisible = False): result = [] remainingRarity = self.expectedRarityCount.copy() hiddenList = [] for i in range(len(self.cards)): if (self.state[i] == TriadDeck.cardNone): result.append([0, 0, None, 0]) elif (self.state[i] == TriadDeck.cardVisible) or forceVisible: cardOb = self.cards[i] remainingRarity[cardOb.rarity] -= 1 result.append([1, 1, cardOb, cardOb.rarity]) else: result.append([]) hiddenList.append(i) if len(hiddenList) > 0: for i in range(len(remainingRarity)): remainingRarity[i] -= self.usedRarityCount[i] if remainingRarity[i] < 0: if i < (len(remainingRarity) - 1): remainingRarity[i + 1] += remainingRarity[i] remainingRarity[i] = 0 else: remainingRarity[i] = min(remainingRarity[i], len(hiddenList)) if remainingRarity[i] > 0: for iR in range(remainingRarity[i]): cardIdx = hiddenList.pop() result[cardIdx] = [1, 0, None, i] return result def __str__(self): stateDesc = ['none','hidden','visible'] desc = [] for i in range(len(self.state)): desc += [ '[%i]:%s (%i*%s)' % ( i, self.cards[i].name if isinstance(self.cards[i], TriadCard) else "--", (self.cards[i].rarity + 1) if isinstance(self.cards[i], TriadCard) else 0, "" if self.state[i] == TriadDeck.cardVisible else ", " + stateDesc[self.state[i]] )] return ', '.join(desc) @staticmethod def generateDeckForRarityRange(state, minRarity, maxRarity): cards = [] while len(cards) < 5: rarity = random.randrange(minRarity, maxRarity + 1) cardIdx = random.randrange(len(TriadCardDB.mapRarity[rarity])) testCard = TriadCardDB.mapRarity[rarity][cardIdx] if (testCard in cards) == False: cards.append(testCard) deck = TriadDeck() deck.initialize(cards, state) return deck @staticmethod def generateDeckRandom(state): return TriadDeck.generateDeckForRarityRange(state, 0, 4) @staticmethod def generateDeckNpc(minRarity, maxRarity): return TriadDeck.generateDeckForRarityRange(TriadDeck.cardHidden, minRarity, maxRarity) @staticmethod def generateDeckPlayer(): randR4 = random.randrange(len(TriadCardDB.mapRarity[4])) randR3 = random.randrange(len(TriadCardDB.mapRarity[3])) cards = [ TriadCardDB.mapRarity[4][randR4], TriadCardDB.mapRarity[3][randR3] ] while len(cards) < 5: rarity = random.randrange(3) cardIdx = random.randrange(len(TriadCardDB.mapRarity[rarity])) testCard = TriadCardDB.mapRarity[rarity][cardIdx] if (testCard in cards) == False: cards.append(testCard) deck = TriadDeck() deck.initialize(cards, TriadDeck.cardHidden) return deck ================================================ FILE: ml/solver/gamelogic/triadGame.py ================================================ from .triadCard import TriadCardDB from enum import Enum class TriadGameState(Enum): Unknown = 0 BlueTurn = 1 RedTurn = 2 EndBlueWin = 3 EndDraw = 4 EndRedWin = 5 class TriadGame: OwnerIdBlue = 1 OwnerIdRed = 2 ModTurnStart = 0 ModCardPlaced = 1 ModAllPlaced = 2 ModCaptureNei = 3 ModCaptureWeight = 4 ModCaptureCondition = 5 ModOverrides = 6 cachedNeis = [] def __init__(self): self.board = [ None ] * 9 self.owner = [ 0 ] * 9 self.typeMod = [ 0 ] * len(TriadCardDB.typeList) self.blueDeck = None self.redDeck = None self.mods = [] self.modOverrides = [ False ] * TriadGame.ModOverrides self.numRestarts = 0 self.mapPlaced = [ 0, 0, 0 ] self.forcedCardIdx = -1 self.forcedBoardIdx = -1 self.state = TriadGameState.Unknown self.cachedSideValues = [[ 0, 0, 0, 0]] * 9 self.debug = False def getNumCardsPlaced(self): return sum(self.mapPlaced) def getNumCardsByOwner(self, ownerId): return sum([1 if (testId == ownerId) else 0 for testId in self.owner]) def getCardSideValue(self, card, side): return max(1, min(10, card.sides[side] + self.typeMod[card.cardType])) def getCardOppositeSideValue(self, card, side): return self.getCardSideValue(card, (side + 2) % 4) def initModifiers(self): self.modOverrides = [ False ] * TriadGame.ModOverrides for mod in self.mods: mod.onMatchStart(self) for idx in mod.overrides: self.modOverrides[idx] = True def restartGame(self): self.board = [ None ] * 9 self.owner = [ 0 ] * 9 self.typeMod = [ 0 ] * len(TriadCardDB.typeList) self.numRestarts += 1 self.mapPlaced = [ 0, 0, 0 ] self.cachedSideValues = [[ 0, 0, 0, 0]] * 9 if self.debug: print('restartGame',self.numRestarts) def placeCardFromDeck(self, pos, idx, ownerId): deck = self.blueDeck if ownerId == TriadGame.OwnerIdBlue else self.redDeck isValid = self.isMoveValid(deck, pos, idx) if isValid: card = deck.useCard(idx) self.mapPlaced[ownerId] += 1 self.placeCard(pos, card, ownerId) return True return False def isMoveValid(self, deck, pos, idx): if (self.forcedBoardIdx >= 0 and self.forcedBoardIdx != pos): return False if (self.forcedCardIdx >= 0 and self.forcedCardIdx != idx): return False if (not deck.hasCard(idx) or self.owner[pos] != 0): return False return True def placeCard(self, pos, card, ownerId): if self.debug: print('Place card:%s, pos:%i, ownerId:%i' % (card, pos, ownerId)) if ((self.owner[pos] == 0) and card != None and card.valid): self.setBoardRaw(pos, card, ownerId) allowCombo = False if self.modOverrides[TriadGame.ModCardPlaced]: for mod in self.mods: mod.onCardPlaced(self, pos) allowCombo = allowCombo or mod.allowCombo comboCounter = 0 comboList = self.getCaptures(pos, comboCounter) while (allowCombo and len(comboList) > 0): comboCounter += 1 nextCombo = [] for pos in comboList: nextComboPart = self.getCaptures(pos, comboCounter) nextCombo = nextCombo + nextComboPart comboList = nextCombo totalPlaced = sum(self.mapPlaced) if (totalPlaced == len(self.board)): self.onAllCardsPlaced() # count again, all placed may trigger sudden death rule totalPlaced = sum(self.mapPlaced) if (totalPlaced < len(self.board)): self.onTurnStart() else: print('ERROR: failed to place card:%s, pos:%i, ownerId:%i' % (card, pos, ownerId)) self.showState('DEBUG') def setBoardRaw(self, pos, card, ownerId): self.board[pos] = card self.owner[pos] = ownerId self.state = TriadGameState.BlueTurn if (ownerId == TriadGame.OwnerIdRed) else TriadGameState.RedTurn self.updateCachedValuesForCard(pos) def updateCachedValuesForCard(self, pos): card = self.board[pos] if card != None: self.cachedSideValues[pos] = [ self.getCardSideValue(card, side) for side in range(4) ] else: self.cachedSideValues[pos] = [0, 0, 0, 0] def onTurnStart(self): self.forcedBoardIdx = -1 self.forcedCardIdx = -1 if (self.state == TriadGameState.BlueTurn or self.state == TriadGameState.RedTurn) and self.modOverrides[TriadGame.ModTurnStart]: for mod in self.mods: mod.onTurnStart(self) def onAllCardsPlaced(self): numPlayerCards = self.blueDeck.numAvail + self.getNumCardsByOwner(TriadGame.OwnerIdBlue) numPlayerOwnedToWin = 5 if self.debug: print('onAllCardsPlaced, player:%i+%i vs %i' % (self.blueDeck.numAvail, self.getNumCardsByOwner(TriadGame.OwnerIdBlue), numPlayerOwnedToWin)) if (numPlayerCards > numPlayerOwnedToWin): self.state = TriadGameState.EndBlueWin elif (numPlayerCards == numPlayerOwnedToWin): self.state = TriadGameState.EndDraw else: self.state = TriadGameState.EndRedWin if self.modOverrides[TriadGame.ModAllPlaced]: for mod in self.mods: mod.onAllCardsPlaced(self) @staticmethod def getBoardPos(x, y): return x + (y * 3) def getCaptures(self, pos, comboCounter): neiInfo = TriadGame.cachedNeis[pos] if self.debug: print('getCaptures(',pos,', combo:',comboCounter,'), neis:',neiInfo) comboList = [] allowMods = comboCounter == 0 if allowMods and self.modOverrides[TriadGame.ModCaptureNei]: for mod in self.mods: comboListPart = mod.getCaptureNeis(self, pos, neiInfo) if self.debug: print('>> capture(',mod.name,') = ',comboListPart) if (len(comboListPart) > 0): comboList = comboList + comboListPart for side, neiPos in neiInfo: if self.debug: print('>> side:%i, neiPos:%i' % (side, neiPos)) if ((self.owner[neiPos] != 0) and (self.owner[neiPos] != self.owner[pos])): sideV = self.cachedSideValues[pos][side] neiV = self.cachedSideValues[neiPos][(side + 2) % 4] # opposite side if allowMods and self.modOverrides[TriadGame.ModCaptureWeight]: for mod in self.mods: sideV, neiV = mod.getCaptureWeights(self, sideV, neiV) if self.debug: print(' sideV:%i neiV:%i' % (sideV, neiV)) canCaptureNei = sideV > neiV if allowMods and self.modOverrides[TriadGame.ModCaptureCondition]: for mod in self.mods: overrideCapture, newcanCaptureNei = mod.getCaptureCondition(self, neiV, sideV) if overrideCapture: canCaptureNei = newcanCaptureNei if canCaptureNei: if self.debug: print(' captured!') self.owner[neiPos] = self.owner[pos] if comboCounter > 0: comboList.append(neiPos) return comboList def showState(self, debugName): print('[%s] ==> State: %s' % (debugName, self.state.name)) for i in range(len(self.owner)): if (self.owner[i] == 0): print(' [%i] empty' % i) else: print(' [%i] %s owner:%i' % (i, str(self.board[i]), self.owner[i])) print('Type mods:', '(empty)' if (sum(self.typeMod) == 0) else '') for i in range(len(self.typeMod)): if (self.typeMod[i] != 0): print(' [%i] %i' % (i,self.typeMod[i])) print('Player hand:', '(empty)' if (self.blueDeck == None or self.blueDeck.numAvail == 0) else '') if self.blueDeck != None: for i in range(5): print(' [%i]: %s' % (i, str(self.blueDeck.cards[i]))) print('Opponent hand:', '(empty)' if (self.redDeck == None or self.redDeck.numAvail == 0) else '') if self.redDeck != None: for i in range(5): print(' [%i]: %s, state:%s' % (i, str(self.redDeck.cards[i]), self.redDeck.state[i])) print('Modifiers:', '(empty)' if (len(self.mods) == 0) else '') for i in range(len(self.mods)): print(' [%i]: %s' % (i, self.mods[i].name)) if (self.forcedCardIdx >= 0 or self.forcedBoardIdx >= 0): print('Forced card:%i, placement:%i' % (self.forcedCardIdx, self.forcedBoardIdx)) @staticmethod def staticInit(): for i in range(9): boardX = i % 3 boardY = int(i / 3) info = [] # [up, left, down, right] if boardY > 0: info.append([0, TriadGame.getBoardPos(boardX, boardY - 1)]) if boardX < 2: info.append([1, TriadGame.getBoardPos(boardX + 1, boardY)]) if boardY < 2: info.append([2, TriadGame.getBoardPos(boardX, boardY + 1)]) if boardX > 0: info.append([3, TriadGame.getBoardPos(boardX - 1, boardY)]) TriadGame.cachedNeis.append(info) TriadGame.staticInit() ================================================ FILE: ml/solver/gamelogic/triadMods.py ================================================ import random from .triadDeck import TriadDeck from .triadGame import TriadGame, TriadGameState import numpy as np class TriadMod: allowCombo = False name = '??' blockedMods = [] modDB = [] def onMatchStart(self, game): pass def onTurnStart(self, game): pass def onCardPlaced(self, game, pos): pass def onAllCardsPlaced(self, game): pass def getCaptureNeis(self, game, pos, neis): return [] def getCaptureWeights(self, game, cardNum, neiNum): return cardNum, neiNum def getCaptureCondition(self, game, cardNum, neiNum): return False, False @staticmethod def generateMods(maxMods = 4): numMods = random.randrange(0, maxMods + 1) mods = [] blocked = [] while len(mods) < numMods: idx = random.randrange(0, len(TriadMod.modDB)) testMod = TriadMod.modDB[idx] if any(testMod.name in s for s in blocked): continue mods.append(testMod) blocked += testMod.blockedMods blocked.append(testMod.name) return mods # reverse: capture when number of lower class TriadModReverse(TriadMod): def __init__(self): self.name = 'Reverse' self.overrides = [TriadGame.ModCaptureCondition] def getCaptureCondition(self, game, cardNum, neiNum): return True, cardNum < neiNum TriadMod.modDB.append(TriadModReverse()) # fallen ace: 1 captures 10/A class TriadModFallenAce(TriadMod): def __init__(self): self.name = 'FallenAce' self.overrides = [TriadGame.ModCaptureWeight] def getCaptureWeights(self, game, cardNum, neiNum): if (cardNum == 10) and (neiNum == 1): return 0, 1 elif (cardNum == 1) and (neiNum == 10): return 1, 0 else: return cardNum, neiNum TriadMod.modDB.append(TriadModFallenAce()) # same: capture when 2+ sides have the same values class TriadModSame(TriadMod): def __init__(self): self.name = 'Same' self.allowCombo = True self.overrides = [TriadGame.ModCaptureNei] def getCaptureNeis(self, game, pos, neis): captureNeis = [] sameNeis = [] for side,neiPos in neis: if (game.owner[neiPos] != 0): sideV = game.cachedSideValues[pos][side] neiV = game.cachedSideValues[neiPos][(side + 2) % 4] # opp side if (sideV == neiV): sameNeis.append(neiPos) if (game.owner[neiPos] != game.owner[pos]): captureNeis.append(neiPos) captureList = [] if (len(sameNeis) >= 2) and (len(captureNeis) > 0): captureList = captureNeis return captureList TriadMod.modDB.append(TriadModSame()) # plus: capture when 2+ sides have the same sum of values class TriadModPlus(TriadMod): def __init__(self): self.name = 'Plus' self.allowCombo = True self.overrides = [TriadGame.ModCaptureNei] def getCaptureNeis(self, game, pos, neis): cardOwner = game.owner[pos] captureList = [] for side,neiPos in neis: if game.owner[neiPos] != 0: if (game.owner[neiPos] != cardOwner): sideV = game.cachedSideValues[pos][side] neiV = game.cachedSideValues[neiPos][(side + 2) % 4] # opp side totalV = sideV + neiV captured = False for vsSide,vsNeiPos in neis: if ((game.owner[vsNeiPos] != 0) and (vsSide != side)): vsSideV = game.cachedSideValues[pos][vsSide] vsSideNeiV = game.cachedSideValues[vsNeiPos][(vsSide + 2) % 4] # opp side vsSideTotalV = vsSideV + vsSideNeiV if (vsSideTotalV == totalV): captured = True if (game.owner[vsNeiPos] != cardOwner): game.owner[vsNeiPos] = cardOwner captureList.append(vsNeiPos) if captured: game.owner[neiPos] = cardOwner captureList.append(neiPos) return captureList TriadMod.modDB.append(TriadModPlus()) # ascention: type mod goes up after each placement class TriadModAscention(TriadMod): def __init__(self): self.name = 'Ascention' self.blockedMods = ['Descention'] self.overrides = [TriadGame.ModCardPlaced] def onCardPlaced(self, game, pos): cardType = game.board[pos].cardType if (cardType> 0): game.typeMod[cardType] += 1 TriadMod.modDB.append(TriadModAscention()) # descention: type mod goes down after each placement class TriadModDescention(TriadMod): def __init__(self): self.name = 'Descention' self.blockedMods = ['Ascention'] self.overrides = [TriadGame.ModCardPlaced] def onCardPlaced(self, game, pos): cardType = game.board[pos].cardType if (cardType> 0): game.typeMod[cardType] -= 1 TriadMod.modDB.append(TriadModDescention()) # sudden death: restart game on draw, keeping card ownership, max 3 times class TriadModSuddenDeath(TriadMod): def __init__(self): self.name = 'SuddenDeath' self.overrides = [TriadGame.ModAllPlaced] def onAllCardsPlaced(self, game): if ((game.state == TriadGameState.EndDraw) and (game.numRestarts < 3)): for i in range(len(game.owner)): if (game.owner[i] == 0): continue deck = game.blueDeck if (game.owner[i] == TriadGame.OwnerIdBlue) else game.redDeck for freeIdx in range(5): if not deck.hasCard(freeIdx): deck.cards[freeIdx] = game.board[i] deck.state[freeIdx] = TriadDeck.cardVisible break nextTurn = TriadGameState.BlueTurn if (game.mapPlaced[TriadGame.OwnerIdBlue] < 5) else TriadGameState.RedTurn game.restartGame() game.state = nextTurn game.blueDeck.onRestart() game.redDeck.onRestart() TriadMod.modDB.append(TriadModSuddenDeath()) # all open: makes all opponent cards on hand visible class TriadModAllOpen(TriadMod): def __init__(self): self.name = 'AllOpen' self.blockedMods = ['ThreeOpen'] self.overrides = [] def onMatchStart(self, game): game.redDeck.makeAllVisible() TriadMod.modDB.append(TriadModAllOpen()) # three open: makes random 3 opponent cards on hand visible class TriadModThreeOpen(TriadMod): def __init__(self): self.name = 'ThreeOpen' self.blockedMods = ['AllOpen'] self.overrides = [] def onMatchStart(self, game): random3 = np.random.choice(range(5), 3, False) for idx in random3: game.redDeck.state[idx] = TriadDeck.cardVisible pass TriadMod.modDB.append(TriadModThreeOpen()) # order: forces card selection order (sequential from hand) class TriadModOrder(TriadMod): def __init__(self): self.name = 'Order' self.blockedMods = ['Chaos'] self.overrides = [TriadGame.ModTurnStart] def onTurnStart(self, game): deck = game.blueDeck if (game.state == TriadGameState.BlueTurn) else game.blueDeck for i in range(len(deck.cards)): if deck.hasCard(i): game.forcedCardIdx = i break TriadMod.modDB.append(TriadModOrder()) # chaos: forces board placement order (random) class TriadModChaos(TriadMod): def __init__(self): self.name = 'Chaos' self.blockedMods = ['Order'] self.overrides = [TriadGame.ModTurnStart] def onTurnStart(self, game): availPos = [] for i in range(len(game.owner)): if (game.owner[i] == 0): availPos.append(i) if (len(availPos) > 0): game.forcedBoardIdx = np.random.choice(availPos) TriadMod.modDB.append(TriadModChaos()) # swap: swaps a card between player and opponent (1 each) at match start class TriadModSwap(TriadMod): def __init__(self): self.name = 'Swap' self.overrides = [] def onMatchStart(self, game): playerIdx = np.random.randint(0, 5) opponentIdx = np.random.randint(0, 5) swapCard = game.redDeck.cards[opponentIdx] game.redDeck.cards[opponentIdx] = game.blueDeck.cards[playerIdx] game.redDeck.state[opponentIdx] = TriadDeck.cardVisible game.blueDeck.cards[playerIdx] = swapCard if game.debug: print('SWAP player[%i] <> opp[%i]' % (playerIdx, opponentIdx)) TriadMod.modDB.append(TriadModSwap()) print('Loaded game modifiers:',len(TriadMod.modDB)) ================================================ FILE: ml/solver/main.py ================================================ from gameSession import GameSession from environment import Environment from agents.agentRandom import AgentRandom from agents.agentDQN import AgentDQN from tqdm import tqdm import matplotlib.pyplot as plt import time def trainModel(): numEpochs = 20 numGamesToTrain = 200 numGamesToEval = 1000 game = GameSession(useModifiers=True) trainingAgent = AgentDQN(game) randomAgent = AgentRandom(game) learningEnv = Environment(game, [trainingAgent, trainingAgent]) evalEnv = Environment(game, [trainingAgent, randomAgent]) # load checkpoint data #trainingAgent.load('data/model.tmp') print('Start training, epochs:',numEpochs) timeStart = time.time() history = [] for epochIdx in range(numEpochs): # play a lot and learn and stuff for _ in tqdm(range(numGamesToTrain), leave=False, desc='Training'.ljust(15)): learningEnv.runTrainingGame() # training update after exploration step trainingAgent.train() # eval with same seed for every iteration randomAgent.setSeed(0) score = 0 for _ in tqdm(range(numGamesToEval), leave=False, desc='Evaluating'.ljust(15)): rewards = evalEnv.runEvalGame() score += rewards[0] # collect info about training to see if it's actually working (lol, who am i kidding, it just fails constantly T.T) stepDetails = trainingAgent.getTrainingDetails() stepDetails['score'] = score history.append(stepDetails) print('[%d] %s' % (epochIdx, stepDetails)) timeElapsed = time.time() - timeStart print('Done. Total time:%s, epoch avg:%.0fs' % (time.strftime("%H:%M:%Ss", time.gmtime(timeElapsed)), timeElapsed / numEpochs)) trainingAgent.save('data/model.tmp') trainingAgent.generateModelCode('data/model.cs') return history def showTrainingData(history): epochs = range(len(history)) fig, axs = plt.subplots(2, sharex=True) axs[0].title.set_text('Reward') axs[0].plot(epochs, [stepDetails['reward'] for stepDetails in history]) axs[1].title.set_text('Loss') axs[1].plot(epochs, [stepDetails['loss'] for stepDetails in history]) plt.xlabel('Epochs') plt.show() if __name__ == "__main__": history = trainModel() showTrainingData(history) ================================================ FILE: ml/solver/perfTests.py ================================================ import numpy as np import random from tqdm import tqdm from gameSession import GameSession from environment import Environment from agents.agentRandom import AgentRandom from agents.agentDQN import AgentDQN def runTestPlayRandomGames(numGames, reproSeed): if reproSeed < 0: print('Running',numGames,'random test games...') for _ in tqdm(range(numGames)): seed = random.randrange(999999) random.seed(seed) np.random.seed(seed) session = GameSession() session.randomSeed = seed while not session.isFinished(): playerId = session.getCurrentPlayer() state = session.getState(playerId) actions = session.getAllowedActions(state) action = random.choice(actions) session.step(action) else: random.seed(reproSeed) np.random.seed(reproSeed) session = GameSession() session.game.debug = True session.randomSeed = reproSeed print('DEBUG game') print(' mods:', [mod.name for mod in session.game.mods]) print(' blue:', session.game.blueDeck) print(' red:', session.game.redDeck) while not session.isFinished(): playerId = session.getCurrentPlayer() state = session.getState(playerId) actions = session.getAllowedActions(state) action = random.choice(actions) print('Move, player:%d numActions:%d (board:%d + card:%d) => action:%d (board:%d + card:%d)' % (playerId, len(actions), sum([ state[i] for i in range(9) ]), sum([ state[9 + i] for i in range(5) ]), action, action % 9, int(action / 9))) session.step(action) def runTestAgentEval(numGames, agentName): print('Running',numGames,'eval games for agent',agentName) game = GameSession() evalAgent = AgentRandom(game) if agentName == 'dqn': evalAgent = AgentDQN(game) env = Environment(game, [evalAgent, AgentRandom(game)]) score = 0 for _ in tqdm(range(numGames)): rewards = env.runEvalGame() score += rewards[0] if __name__ == "__main__": runTestPlayRandomGames(10000, reproSeed=-1) runTestAgentEval(10000, 'random') runTestAgentEval(10000, 'dqn') ================================================ FILE: ml/solver/utils/codeGenerator.py ================================================ import os import textwrap class CodeGenerator(): def __init__(self): self.layers = [] def addLayer(self, weights, biases, activation): self.layers.append((weights, biases, activation)) def save(self, name): lines = [] lines.append('// Auto generated model data') lines.append('private void InitModel()') lines.append('{') numInputs = self.layers[0][0].shape[1] lines.append('\tNumInputs = %d;' % numInputs) numOutputs = [str(layer[0].shape[0]) for layer in self.layers] lines.append('\tNumOutputs = {%s};' % ', '.join(numOutputs)) for i in range(len(self.layers)): lines.append('\tLayer%d_W = %s;' % (i, self.convertToCSArray(self.layers[i][0]))) lines.append('\tLayer%d_B = %s;' % (i, self.convertToCSArray(self.layers[i][1]))) lines.append('}') file = open(name, 'w') for line in lines: wrapLines = textwrap.wrap(line, width=120, tabsize=4, subsequent_indent='\t\t') for fileLine in wrapLines: file.write(fileLine + '\n') file.close() print('Saved generated code:',name) def convertToCSArray(self, arr): values = [] if len(arr.shape) == 1: values = [str(x) for x in arr] else: for i in range(arr.shape[0]): values.append(self.convertToCSArray(arr[i])) return '{' + ', '.join(values) + '}' ================================================ FILE: ml/solver/utils/estimatorTorch.py ================================================ import numpy as np import torch import torch.nn as nn from .codeGenerator import CodeGenerator torchDevice = torch.device("cuda" if torch.cuda.is_available() else "cpu") class EstimatorNN(nn.Module): def __init__(self, numInputs, numHiddenArr, numOutputs): super(EstimatorNN, self).__init__() layerSizes = [ numInputs ] + numHiddenArr modelArgs = [nn.Flatten()] for i in range(len(layerSizes) - 1): modelArgs.append(nn.Linear(layerSizes[i], layerSizes[i + 1])) modelArgs.append(nn.ReLU()) modelArgs.append(nn.Linear(layerSizes[-1], numOutputs)) self.model = nn.Sequential(*modelArgs) def forward(self, s): return self.model(s) def generateModelCode(self, name): sd = self.state_dict() weightSuffix = ".weight" biasSuffix = ".bias" layers = [] # layers will be in the same order as model for entryName in sd.keys(): if entryName.endswith(weightSuffix): biasName = entryName.replace(weightSuffix, biasSuffix) if biasName in sd.keys(): layers.append({ 'w': sd[entryName].cpu().numpy(), 'b':sd[biasName].cpu().numpy(), 'act':'relu', 'name':entryName.replace(weightSuffix, '') }) layers[-1]['act'] = '' codeGen = CodeGenerator() for layer in layers: codeGen.addLayer(layer['w'], layer['b'], layer['act']) codeGen.save(name) class EstimatorModel: def __init__(self, numInputs, numHiddenArr, numOutput, learningRate): self.nn = EstimatorNN(numInputs, numHiddenArr, numOutput).to(torchDevice) self.nn.eval() self.lossFunc = nn.MSELoss(reduction='mean') self.optimizer = torch.optim.Adam(self.nn.parameters(), lr=learningRate) def predict(self, input): with torch.no_grad(): input = torch.tensor(input).float().to(torchDevice) return self.nn(input).cpu().numpy() def fit(self, inputS, inputA, targetAR): self.nn.train() inputS = torch.tensor(inputS).float().to(torchDevice) inputA = torch.tensor(inputA).long().to(torchDevice) targetAR = torch.tensor(targetAR).float().to(torchDevice) outputAProbs = self.nn(inputS) outputAR = torch.gather(outputAProbs, dim=-1, index=inputA.unsqueeze(-1)).squeeze(-1) loss = self.lossFunc(outputAR, targetAR) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.nn.eval() return loss.item() def save(self, name): torch.save(self.nn.state_dict(), name) def load(self, name): self.nn.load_state_dict(torch.load(name)) self.nn.eval() def generateModelCode(self, name): self.nn.generateModelCode(name) ================================================ FILE: ml/solver/utils/trainingMemory.py ================================================ from collections import deque import random class TrainingMemory: def __init__(self, capacity): self.capacity = capacity self.clear() def add(self, sampleTuple): self.numWrites += 1 if len(self) < self.capacity: self.buffer.append(sampleTuple) return True return False def clear(self): self.numWrites = 0 self.buffer = deque([], maxlen=self.capacity) def __len__(self): return len(self.buffer) def sample(self, batchSize): samples = random.sample(self.buffer, batchSize) return zip(*samples) class TrainingMemoryCircular(TrainingMemory): def add(self, sampleTuple): isAdded = super().add(sampleTuple) if not isAdded: writeIdx = self.numWrites % self.capacity self.buffer[writeIdx] = sampleTuple class TrainingMemoryReservoir(TrainingMemory): def add(self, sampleTuple): isAdded = super().add(sampleTuple) if not isAdded: # Algorithm R writeIdx = random.randint(0, self.numWrites) if writeIdx < self.capacity: self.buffer[writeIdx] = sampleTuple ================================================ FILE: sources/App.config ================================================ ================================================ FILE: sources/FFTriadBuddy.csproj ================================================  Debug AnyCPU {4FE48C06-9E9C-46C9-A53E-CFE06EFDCBDC} WinExe FFTriadBuddy FFTriadBuddy v4.8 512 true false publish\ true Disk false Foreground 7 Days false false true 0 1.0.0.%2a false true AnyCPU true full false bin\Debug\ DEBUG;TRACE prompt 4 true false AnyCPU pdbonly true bin\Release\ TRACE prompt 4 true false card.ico 4.0 MSBuild:Compile Designer AdjustCardDialog.xaml AdjustHashDialog.xaml DialogWindow.xaml FavDeckEditDialog.xaml FavDeckPreview.xaml OverlayWindowTransparent.xaml OverlayWindowInteractive.xaml NumTextBox.xaml PageCards.xaml PageInfo.xaml PageNpcs.xaml PageScreenshot.xaml PlayerDeckPreview.xaml PageSimulate.xaml CardGridView.xaml CardView.xaml DeckView.xaml PageSetup.xaml SearchableComboBox.xaml Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile MSBuild:Compile Designer App.xaml Code MainWindow.xaml Code Designer MSBuild:Compile MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer MSBuild:Compile Designer Designer MSBuild:Compile Designer MSBuild:Compile MSBuild:Compile Designer Designer MSBuild:Compile ResXFileCodeGenerator Designer SettingsSingleFileGenerator Settings.Designer.cs True Settings.settings True Designer MSBuild:Compile Designer MSBuild:Compile False Microsoft .NET Framework 4.6.1 %28x86 and x64%29 true False .NET Framework 3.5 SP1 false (\d+) %(myAssemblyInfo.Version) $([System.Text.RegularExpressions.Regex]::Match($(In), $(Pattern))) $(SolutionDir)\releases\release-v$(MajorVersion).zip $(SolutionDir)\releases\temp ================================================ FILE: sources/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("FFTriadBuddy")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("FFTriadBuddy")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("4fe48c06-9e9c-46c9-a53e-cfe06efdcbdc")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("121.0.0.0")] [assembly: AssemblyFileVersion("121.0.0.0")] ================================================ FILE: sources/Properties/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ..\assets.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ================================================ FILE: sources/Properties/Settings.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace FFTriadBuddy.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); public static Settings Default { get { return defaultInstance; } } } } ================================================ FILE: sources/Properties/Settings.settings ================================================  ================================================ FILE: sources/data/ImageHashDB.cs ================================================ using MgAl2O4.Utils; using Palit.TLSHSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Reflection; using System.Security.Cryptography; using System.Xml; namespace FFTriadBuddy { public enum EImageHashType { CardNumber, CardImage, Rule, Cactpot, } public class ImageHashData : IComparable { public byte[] hashMD5; public TlshHash hashTLSH; public EImageHashType type; public object ownerOb; public bool isAuto; public bool isKnown; public int matchDistance; public Image previewImage; public Bitmap sourceImage; public Rectangle previewBounds; public Rectangle previewContextBounds; public void CalculateHash(byte[] data) { TlshBuilder hashBuilder = new TlshBuilder(); hashBuilder.Update(data); hashTLSH = hashBuilder.IsValid(false) ? hashBuilder.GetHash(false) : null; using (MD5 md5Builder = MD5.Create()) { hashMD5 = md5Builder.ComputeHash(data); } } public void CalculateHash(float[] data) { byte[] byteData = new byte[data.Length * sizeof(float)]; Buffer.BlockCopy(data, 0, byteData, 0, byteData.Length); CalculateHash(byteData); } private static int GetHexVal(char hex) { return (hex >= 'a' && hex <= 'f') ? (hex - 'a' + 10) : (hex >= 'A' && hex <= 'F') ? (hex - 'A' + 10) : hex; } public void LoadFromString(string descTLSH, string descBuffer) { if (!string.IsNullOrEmpty(descTLSH)) { hashTLSH = TlshHash.FromTlshStr(descTLSH); } if (!string.IsNullOrEmpty(descBuffer)) { hashMD5 = new byte[descBuffer.Length / 2]; for (int idx = 0; idx < descBuffer.Length; idx++) { hashMD5[idx / 2] = (byte)((GetHexVal(descBuffer[idx]) << 4) + GetHexVal(descBuffer[idx])); } } } public bool IsMatching(ImageHashData other, int maxDistance, out int matchDistance) { matchDistance = GetHashDistance(other); return matchDistance <= maxDistance; } public int GetHashDistance(ImageHashData other) { if (hashMD5 != null && other.hashMD5 != null && hashMD5.Length == other.hashMD5.Length) { bool isMatching = true; for (int idx = 0; idx < hashMD5.Length; idx++) { if (hashMD5[idx] != other.hashMD5[idx]) { isMatching = false; break; } } if (isMatching) { return 0; } } if (hashTLSH != null && other.hashTLSH != null) { return hashTLSH.TotalDiff(other.hashTLSH, false); } return int.MaxValue; } public void UpdatePreviewImage() { if (previewImage == null) { previewImage = ImageUtils.CreatePreviewImage(sourceImage, previewBounds, previewContextBounds); } } public int CompareTo(object obj) { ImageHashData otherOb = (ImageHashData)obj; if (otherOb == null) { return 1; } if (type != otherOb.type) { return type.CompareTo(otherOb.type); } return ownerOb.ToString().CompareTo(otherOb.ownerOb.ToString()); } public bool IsValid() { return (hashMD5 != null) || (hashTLSH != null); } public override string ToString() { return type + ": " + ownerOb; } } public class ImageHashDB { public List hashes; public string DBPath; private static ImageHashDB instance = new ImageHashDB(); public List modObjects; public ImageHashDB() { DBPath = "data/hashes.xml"; hashes = new List(); modObjects = new List(); } public static ImageHashDB Get() { return instance; } public bool Load() { hashes.Clear(); modObjects.Clear(); foreach (Type type in Assembly.GetAssembly(typeof(TriadGameModifier)).GetTypes()) { if (type.IsSubclassOf(typeof(TriadGameModifier))) { modObjects.Add((TriadGameModifier)Activator.CreateInstance(type)); } } try { XmlDocument xdoc = new XmlDocument(); xdoc.Load(AssetManager.Get().GetAsset(DBPath)); foreach (XmlNode testNode in xdoc.DocumentElement.ChildNodes) { XmlElement testElem = (XmlElement)testNode; ImageHashData hashEntry = LoadHashEntry(testElem); if (hashEntry != null && hashEntry.IsValid()) { hashes.Add(hashEntry); } } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } Logger.WriteLine("Loaded hashes: " + hashes.Count); return true; } public ImageHashData LoadHashEntry(XmlElement xmlElem) { ImageHashData result = null; if (xmlElem != null && xmlElem.Name == "hash" && xmlElem.HasAttribute("type") && (xmlElem.HasAttribute("value") || xmlElem.HasAttribute("valueB"))) { string typeName = xmlElem.GetAttribute("type"); string hashValueC = xmlElem.HasAttribute("value") ? xmlElem.GetAttribute("value") : null; string hashValueB = xmlElem.HasAttribute("valueB") ? xmlElem.GetAttribute("valueB") : null; if (typeName.Equals("rule", StringComparison.InvariantCultureIgnoreCase)) { string ruleName = xmlElem.GetAttribute("name"); result = new ImageHashData() { type = EImageHashType.Rule, isKnown = true }; result.ownerOb = ParseRule(ruleName); result.LoadFromString(hashValueC, hashValueB); } else if (typeName.Equals("card", StringComparison.InvariantCultureIgnoreCase)) { string cardIdName = xmlElem.GetAttribute("id"); int cardId = int.Parse(cardIdName); result = new ImageHashData() { type = EImageHashType.CardImage, isKnown = true }; result.ownerOb = TriadCardDB.Get().cards[cardId]; result.LoadFromString(hashValueC, hashValueB); } else if (typeName.Equals("cactpot", StringComparison.InvariantCultureIgnoreCase)) { string numIdName = xmlElem.GetAttribute("id"); result = new ImageHashData() { type = EImageHashType.Cactpot, isKnown = true }; result.ownerOb = int.Parse(numIdName); result.LoadFromString(hashValueC, hashValueB); } } return result; } public List LoadImageHashes(JsonParser.ObjectValue jsonOb) { List list = new List(); string[] enumArr = Enum.GetNames(typeof(EImageHashType)); foreach (var kvp in jsonOb.entries) { EImageHashType groupType = (EImageHashType)Array.IndexOf(enumArr, kvp.Key); JsonParser.ArrayValue typeArr = (JsonParser.ArrayValue)kvp.Value; foreach (JsonParser.Value value in typeArr.entries) { JsonParser.ObjectValue jsonHashOb = (JsonParser.ObjectValue)value; string idStr = jsonHashOb["id"]; bool hasIdNum = int.TryParse(idStr, out int idNum); bool needsIdNum = (groupType != EImageHashType.Rule); if (hasIdNum != needsIdNum) { continue; } ImageHashData hashEntry = new ImageHashData() { type = groupType, isKnown = true }; switch (groupType) { case EImageHashType.Rule: hashEntry.ownerOb = ParseRule(idStr); break; case EImageHashType.CardImage: hashEntry.ownerOb = TriadCardDB.Get().cards[idNum]; break; default: hashEntry.ownerOb = idNum; break; } if (hashEntry.ownerOb != null) { string descHashTLSH = jsonHashOb["hashC", JsonParser.StringValue.Empty]; string descHashMd5 = jsonHashOb["hashB", JsonParser.StringValue.Empty]; hashEntry.LoadFromString(descHashTLSH, descHashMd5); if (hashEntry.IsValid()) { list.Add(hashEntry); } } } } return list; } public void StoreHashes(List entries, JsonWriter jsonWriter) { foreach (EImageHashType subType in Enum.GetValues(typeof(EImageHashType))) { List sortedSubtypeList = entries.FindAll(x => x.type == subType); sortedSubtypeList.Sort(); jsonWriter.WriteArrayStart(subType.ToString()); foreach (ImageHashData entry in sortedSubtypeList) { jsonWriter.WriteObjectStart(); switch (subType) { case EImageHashType.CardImage: jsonWriter.WriteString(((TriadCard)entry.ownerOb).Id.ToString(), "id"); break; default: jsonWriter.WriteString(entry.ownerOb.ToString(), "id"); break; } if (entry.hashTLSH != null) { jsonWriter.WriteString(entry.hashTLSH.ToString(), "hashC"); } else { string hexStr = BitConverter.ToString(entry.hashMD5).ToLower(); jsonWriter.WriteString(hexStr, "hashB"); } jsonWriter.WriteObjectEnd(); } jsonWriter.WriteArrayEnd(); } } private TriadGameModifier ParseRule(string ruleName) { TriadGameModifier result = null; foreach (TriadGameModifier mod in modObjects) { if (ruleName.Equals(mod.GetCodeName(), StringComparison.InvariantCultureIgnoreCase)) { result = (TriadGameModifier)Activator.CreateInstance(mod.GetType()); break; } } if (result == null) { Logger.WriteLine("Loading failed! Can't parse rule: " + ruleName); } return result; } public ImageHashData FindExactMatch(ImageHashData hashData) { return FindBestMatch(hashData, 0, out int dummyV); } public ImageHashData FindBestMatch(ImageHashData hashData, int maxDistance, out int matchDistance) { int bestDistance = 0; int bestIdx = -1; for (int idx = 0; idx < hashes.Count; idx++) { if (hashes[idx].type == hashData.type) { int distance = hashes[idx].GetHashDistance(hashData); if (distance <= maxDistance) { if (bestIdx < 0 || bestDistance > distance) { bestIdx = idx; bestDistance = distance; } } } } matchDistance = bestDistance; return (bestIdx < 0) ? null : hashes[bestIdx]; } } } ================================================ FILE: sources/data/LocalizationDB.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Xml; namespace FFTriadBuddy { public enum ELocStringType { Unknown, RuleName, CardType, CardName, NpcName, NpcLocation, TournamentName, } public class LocString { public string[] Text = new string[LocalizationDB.Languages.Length]; public ELocStringType Type; public int Id; public LocString() { Type = ELocStringType.Unknown; Id = 0; } public LocString(ELocStringType Type, int Id) { this.Type = Type; this.Id = Id; } public LocString(ELocStringType Type, int Id, string DefaultText) { this.Type = Type; this.Id = Id; Text[LocalizationDB.CodeLanguageIdx] = DefaultText; } public override string ToString() { return string.Format("{0}:{1} '{2}'", Type, Id, GetCodeName()); } public string Get(string lang) { int langIdx = Array.IndexOf(LocalizationDB.Languages, lang); return Get(langIdx); } public string Get(int langIdx) { if (Text != null) { string resultStr = (langIdx < 0) ? null : Text[langIdx]; if (string.IsNullOrEmpty(resultStr)) { // fallback language if game data is not available? resultStr = Text[LocalizationDB.CodeLanguageIdx]; } if (resultStr != null) { return resultStr; } } return string.Format("--LOC:{0}:{1}--", Type, Id); } public string GetLocalized() { return Get(LocalizationDB.UserLanguageIdx); } public string GetCodeName() { return Get(LocalizationDB.CodeLanguageIdx); } } public class LocalizationDB { public readonly static string[] Languages = { "de", "en", "fr", "ja", "cn", "ko" }; public readonly static string CodeLanguage = "en"; public readonly static int CodeLanguageIdx = Array.IndexOf(Languages, CodeLanguage); public static int UserLanguageIdx = -1; public string DBPath; private static LocalizationDB instance = new LocalizationDB(); public delegate void LangChangedDelegate(); public static event LangChangedDelegate OnLanguageChanged; public List LocUnknown; public List LocRuleNames; public List LocCardTypes; public List LocCardNames; public List LocNpcNames; public List LocNpcLocations; public List LocTournamentNames; public Dictionary> mapLocStrings; public Dictionary mapCardTypes; public LocalizationDB() { DBPath = "data/loc.xml"; LocUnknown = new List(); LocRuleNames = new List(); LocCardTypes = new List(); LocCardNames = new List(); LocNpcNames = new List(); LocNpcLocations = new List(); LocTournamentNames = new List(); mapLocStrings = new Dictionary>(); mapLocStrings.Add(ELocStringType.Unknown, LocUnknown); mapLocStrings.Add(ELocStringType.RuleName, LocRuleNames); mapLocStrings.Add(ELocStringType.CardType, LocCardTypes); mapLocStrings.Add(ELocStringType.CardName, LocCardNames); mapLocStrings.Add(ELocStringType.NpcName, LocNpcNames); mapLocStrings.Add(ELocStringType.NpcLocation, LocNpcLocations); mapLocStrings.Add(ELocStringType.TournamentName, LocTournamentNames); mapCardTypes = new Dictionary(); string[] enumNames = Enum.GetNames(typeof(ETriadCardType)); for (int enumIdx = 0; enumIdx < enumNames.Length; enumIdx++) { var locStr = new LocString(ELocStringType.CardType, enumIdx, enumIdx == 0 ? "" : enumNames[enumIdx]); mapCardTypes.Add((ETriadCardType)enumIdx, locStr); LocCardTypes.Add(locStr); } } public static LocalizationDB Get() { return instance; } public static void SetCurrentUserLanguage(string cultureCode) { int newLangIdx = CodeLanguageIdx; if (cultureCode == "de" || cultureCode.StartsWith("de-")) { newLangIdx = Array.IndexOf(Languages, "de"); } else if (cultureCode == "fr" || cultureCode.StartsWith("fr-")) { newLangIdx = Array.IndexOf(Languages, "fr"); } else if (cultureCode == "ja" || cultureCode.StartsWith("ja-")) { newLangIdx = Array.IndexOf(Languages, "ja"); } else if (cultureCode == "ko" || cultureCode.StartsWith("ko-")) { newLangIdx = Array.IndexOf(Languages, "ko"); } else if (cultureCode == "zh" || cultureCode.StartsWith("zh-")) { newLangIdx = Array.IndexOf(Languages, "cn"); } else { newLangIdx = CodeLanguageIdx; } bool changed = false; if (newLangIdx != UserLanguageIdx) { UserLanguageIdx = newLangIdx; changed = true; } var locManager = LocResourceManager.Get(); if (locManager.UserCultureCode != cultureCode) { CultureInfo.CurrentUICulture = new CultureInfo(cultureCode); locManager.SetCurrentUserLanguage(CultureInfo.CurrentUICulture, typeof(loc.strings)); changed = true; } if (changed) { OnLanguageChanged?.Invoke(); Logger.WriteLine("Init localization: culture:{0} -> gameData:{1}, UI:{2}", cultureCode, Languages[UserLanguageIdx], locManager.UserCultureCode); } } public LocString FindOrAddLocString(ELocStringType Type, int Id) { var list = mapLocStrings[Type]; // so far those ids are continuous from 0, switch to dictionaries when it changes while (list.Count <= Id) { list.Add(new LocString(Type, list.Count)); } return list[Id]; } public bool Load() { int numLoaded = 0; try { XmlDocument xdoc = new XmlDocument(); xdoc.Load(AssetManager.Get().GetAsset(DBPath)); foreach (XmlNode locNode in xdoc.DocumentElement.ChildNodes) { XmlElement locElem = (XmlElement)locNode; if (locElem != null && locElem.Name == "loc") { try { int locType = int.Parse(locElem.GetAttribute("type")); int locId = int.Parse(locElem.GetAttribute("id")); LocString locStr = FindOrAddLocString((ELocStringType)locType, locId); for (int idx = 0; idx < locStr.Text.Length; idx++) { if (locElem.HasAttribute(Languages[idx])) { locStr.Text[idx] = WebUtility.HtmlDecode(locElem.GetAttribute(Languages[idx])); } } FixLocalizedNameCasing(locStr); numLoaded++; } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } } } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } Logger.WriteLine("Loaded localized strings: " + numLoaded); return numLoaded > 0; } public void Save() { string RawFilePath = AssetManager.Get().CreateFilePath("assets/" + DBPath); try { XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.Indent = true; XmlWriter xmlWriter = XmlWriter.Create(RawFilePath, writerSettings); xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("root"); foreach (var kvp in mapLocStrings) { foreach (var locStr in kvp.Value) { bool isEmpty = true; for (int idx = 0; idx < locStr.Text.Length; idx++) { if (locStr.Text[idx] != null) { // empty str is valid value, ignore only nulls isEmpty = false; break; } } if (!isEmpty) { xmlWriter.WriteStartElement("loc"); xmlWriter.WriteAttributeString("type", ((int)locStr.Type).ToString()); xmlWriter.WriteAttributeString("id", locStr.Id.ToString()); for (int idx = 0; idx < locStr.Text.Length; idx++) { if (locStr.Text[idx] != null) { xmlWriter.WriteAttributeString(Languages[idx], locStr.Text[idx]); } } xmlWriter.WriteEndElement(); } } } xmlWriter.WriteEndDocument(); xmlWriter.Close(); } catch (Exception ex) { Logger.WriteLine("Saving failed! Exception:" + ex); } } private static string[] caseFixExclusions = new string[] { "the", "goe", "van", "des", "sas", "yae", "tol", "der", "rem" }; private void FixLocalizedNameCasing(LocString entry) { bool canFixCase = (entry.Type == ELocStringType.CardName) || (entry.Type == ELocStringType.NpcName); if (!canFixCase) { return; } for (int idxLang = 0; idxLang < entry.Text.Length; idxLang++) { if (string.IsNullOrEmpty(entry.Text[idxLang])) { continue; } string[] tokens = entry.Text[idxLang].Split(' '); int numChangedTokens = 0; for (int idx = 0; idx < tokens.Length; idx++) { if (tokens[idx].Length > 2 && Array.IndexOf(caseFixExclusions, tokens[idx]) < 0) { if (tokens[idx][1] == '\'') { // don't touch, i have no idea how french capitalization work continue; } bool hasLowerCase = char.IsLower(tokens[idx], 0); if (hasLowerCase) { var newToken = tokens[idx].Substring(0, 1).ToUpper() + tokens[idx].Substring(1); tokens[idx] = newToken; numChangedTokens++; } } } if (numChangedTokens > 0) { var newText = string.Join(" ", tokens); entry.Text[idxLang] = newText; } } } } } ================================================ FILE: sources/data/PlayerSettingsDB.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.IO; using System.Reflection; namespace FFTriadBuddy { public class PlayerSettingsDB { public List ownedCards; public List completedNpcs; public List customHashes; public TriadCard[] starterCards; public Dictionary lastDeck; public List favDecks; public bool useCloudStorage; public bool useXInput; public bool useSmallIcons; public bool useSoftwareRendering; public bool alwaysOnTop; public bool skipOptionalSimulateRules; public bool isDirty; public string DBPath; public string cloudToken; public string forcedLanguage; public int lastNpcId; public float lastWidth; public float lastHeight; public float fontSize; public float markerDurationCard; public float markerDurationSwap; public float markerDurationCactpot; private static PlayerSettingsDB instance = new PlayerSettingsDB(); public delegate void UpdatedDelegate(bool bCards, bool bNpcs, bool bDecks); public event UpdatedDelegate OnUpdated; public PlayerSettingsDB() { DBPath = "FFTriadBuddy-settings.json"; ownedCards = new List(); completedNpcs = new List(); lastDeck = new Dictionary(); favDecks = new List(); starterCards = new TriadCard[5]; customHashes = new List(); useCloudStorage = false; useXInput = true; useSmallIcons = false; alwaysOnTop = false; skipOptionalSimulateRules = false; isDirty = false; cloudToken = null; forcedLanguage = null; lastNpcId = -1; lastWidth = 0; lastHeight = 0; fontSize = 12.0f; markerDurationCard = 4.0f; markerDurationSwap = 10.0f; markerDurationCactpot = 1.5f; } public static PlayerSettingsDB Get() { return instance; } public bool Load() { bool bResult = false; string FilePath = AssetManager.Get().CreateFilePath(DBPath); if (File.Exists(FilePath)) { using (StreamReader file = new StreamReader(FilePath)) { string fileContent = file.ReadToEnd(); bResult = LoadFromJson(fileContent); file.Close(); } } TriadCardDB cardDB = TriadCardDB.Get(); starterCards[0] = cardDB.Find("Dodo"); starterCards[1] = cardDB.Find("Sabotender"); starterCards[2] = cardDB.Find("Bomb"); starterCards[3] = cardDB.Find("Mandragora"); starterCards[4] = cardDB.Find("Coeurl"); foreach (TriadCard starterCard in starterCards) { if (!ownedCards.Contains(starterCard)) { ownedCards.Add(starterCard); } } return bResult; } public bool LoadFromJson(string jsonStr) { TriadCardDB cardDB = TriadCardDB.Get(); TriadNpcDB npcDB = TriadNpcDB.Get(); ownedCards.Clear(); completedNpcs.Clear(); lastDeck.Clear(); favDecks.Clear(); try { JsonParser.ObjectValue jsonOb = JsonParser.ParseJson(jsonStr); JsonParser.ObjectValue uiOb = (JsonParser.ObjectValue)jsonOb["ui", null]; if (uiOb != null) { JsonParser.Value BoolTrue = new JsonParser.BoolValue(true); JsonParser.Value BoolFalse = new JsonParser.BoolValue(false); useXInput = (JsonParser.BoolValue)uiOb["xInput", BoolTrue]; useSmallIcons = (JsonParser.BoolValue)uiOb["smallIcons", BoolFalse]; useSoftwareRendering = (JsonParser.BoolValue)uiOb["noGPU", BoolFalse]; alwaysOnTop = (JsonParser.BoolValue)uiOb["onTop", BoolFalse]; skipOptionalSimulateRules = (JsonParser.BoolValue)uiOb["skipOptRules", BoolFalse]; forcedLanguage = (JsonParser.StringValue)uiOb["lang", null]; TryGettingFloatValue(uiOb, "fontSize", ref fontSize); TryGettingFloatValue(uiOb, "markerCard", ref markerDurationCard); TryGettingFloatValue(uiOb, "markerSwap", ref markerDurationSwap); TryGettingFloatValue(uiOb, "markerCactpot", ref markerDurationCactpot); TryGettingIntValue(uiOb, "lastNpcId", ref lastNpcId); TryGettingFloatValue(uiOb, "lastWidth", ref lastWidth); TryGettingFloatValue(uiOb, "lastHeight", ref lastHeight); fontSize = Math.Min(Math.Max(fontSize, 10), 40); } JsonParser.ObjectValue cloudOb = (JsonParser.ObjectValue)jsonOb["cloud", null]; if (cloudOb != null) { useCloudStorage = (JsonParser.BoolValue)cloudOb["use", JsonParser.BoolValue.Empty]; cloudToken = (JsonParser.StringValue)cloudOb["token", null]; } JsonParser.ArrayValue cardsArr = (JsonParser.ArrayValue)jsonOb["cards", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value value in cardsArr.entries) { int cardId = (JsonParser.IntValue)value; ownedCards.Add(cardDB.cards[cardId]); } JsonParser.ArrayValue npcsArr = (JsonParser.ArrayValue)jsonOb["npcs", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value value in npcsArr.entries) { int npcId = (JsonParser.IntValue)value; completedNpcs.Add(npcDB.npcs[npcId]); } JsonParser.ArrayValue decksArr = (JsonParser.ArrayValue)jsonOb["decks", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value value in decksArr.entries) { JsonParser.ObjectValue deckOb = (JsonParser.ObjectValue)value; int npcId = (JsonParser.IntValue)deckOb["id"]; TriadNpc npc = TriadNpcDB.Get().npcs[npcId]; if (npc != null) { TriadDeck deckCards = new TriadDeck(); cardsArr = (JsonParser.ArrayValue)deckOb["cards", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value cardValue in cardsArr.entries) { int cardId = (JsonParser.IntValue)cardValue; deckCards.knownCards.Add(cardDB.cards[cardId]); } lastDeck.Add(npc, deckCards); } } JsonParser.ArrayValue favDecksArr = (JsonParser.ArrayValue)jsonOb["favDecks", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value value in favDecksArr.entries) { JsonParser.ObjectValue deckOb = (JsonParser.ObjectValue)value; TriadDeckNamed deckCards = new TriadDeckNamed(); cardsArr = (JsonParser.ArrayValue)deckOb["cards", JsonParser.ArrayValue.Empty]; foreach (JsonParser.Value cardValue in cardsArr.entries) { int cardId = (JsonParser.IntValue)cardValue; deckCards.knownCards.Add(cardDB.cards[cardId]); } if (deckCards.knownCards.Count > 0) { deckCards.Name = deckOb["name", JsonParser.StringValue.Empty]; favDecks.Add(deckCards); } } JsonParser.ObjectValue imageHashesOb = (JsonParser.ObjectValue)jsonOb["images", null]; if (imageHashesOb != null) { customHashes = ImageHashDB.Get().LoadImageHashes(imageHashesOb); ImageHashDB.Get().hashes.AddRange(customHashes); } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } Logger.WriteLine("Loaded player cards: " + ownedCards.Count + ", npcs: " + completedNpcs.Count + ", hashes: " + customHashes.Count); return ownedCards.Count > 0; } public void OnImport() { OnUpdated?.Invoke(true, true, true); } private void TryGettingIntValue(JsonParser.ObjectValue ob, string key, ref int value) { if (ob.entries.ContainsKey(key)) { var jsonValue = ob[key]; var jsonInt = jsonValue as JsonParser.IntValue; if (jsonInt != null) { value = jsonInt.Number; } } } private void TryGettingFloatValue(JsonParser.ObjectValue ob, string key, ref float value) { if (ob.entries.ContainsKey(key)) { var jsonValue = ob[key]; var jsonInt = jsonValue as JsonParser.IntValue; var jsonFloat = jsonValue as JsonParser.FloatValue; if (jsonInt != null) { value = jsonInt.Number; } else if (jsonFloat != null) { value = jsonFloat.Number; } } } public bool MergeWithContent(string jsonString) { PlayerSettingsDB mergeDB = new PlayerSettingsDB(); bool bLoaded = mergeDB.LoadFromJson(jsonString); bool bHadUniqueSettings = false; if (bLoaded) { bool bUpdatedOwnedCards = false; foreach (TriadCard card in mergeDB.ownedCards) { if (!ownedCards.Contains(card)) { ownedCards.Add(card); bUpdatedOwnedCards = true; } } bHadUniqueSettings = bHadUniqueSettings || (ownedCards.Count > mergeDB.ownedCards.Count); bool bUpdatedNpcs = false; foreach (TriadNpc npc in mergeDB.completedNpcs) { if (!completedNpcs.Contains(npc)) { completedNpcs.Add(npc); bUpdatedNpcs = true; } } bHadUniqueSettings = bHadUniqueSettings || (completedNpcs.Count > mergeDB.completedNpcs.Count); bool bUpdatedDecks = false; foreach (KeyValuePair kvp in mergeDB.lastDeck) { if (!lastDeck.ContainsKey(kvp.Key)) { lastDeck.Add(kvp.Key, kvp.Value); } // replace existing? skip for now... } bHadUniqueSettings = bHadUniqueSettings || (lastDeck.Count > mergeDB.lastDeck.Count); OnUpdated.Invoke(bUpdatedOwnedCards, bUpdatedNpcs, bUpdatedDecks); } return bHadUniqueSettings; } public void Save() { string FilePath = AssetManager.Get().CreateFilePath(DBPath); using (StreamWriter file = new StreamWriter(FilePath)) { string jsonString = SaveToJson(false); file.Write(jsonString); file.Close(); } } public string SaveToString() { isDirty = false; return SaveToJson(true); } public string SaveToJson(bool bLimitedMode = false) { JsonWriter jsonWriter = new JsonWriter(); try { jsonWriter.WriteObjectStart(); if (!bLimitedMode) { jsonWriter.WriteObjectStart("ui"); jsonWriter.WriteBool(useXInput, "xInput"); jsonWriter.WriteBool(useSmallIcons, "smallIcons"); jsonWriter.WriteBool(useSoftwareRendering, "noGPU"); jsonWriter.WriteBool(alwaysOnTop, "onTop"); jsonWriter.WriteBool(skipOptionalSimulateRules, "skipOptRules"); jsonWriter.WriteString(forcedLanguage, "lang"); jsonWriter.WriteInt(lastNpcId, "lastNpcId"); jsonWriter.WriteFloat(lastWidth, "lastWidth"); jsonWriter.WriteFloat(lastHeight, "lastHeight"); jsonWriter.WriteFloat(fontSize, "fontSize"); jsonWriter.WriteFloat(markerDurationCard, "markerCard"); jsonWriter.WriteFloat(markerDurationSwap, "markerSwap"); jsonWriter.WriteFloat(markerDurationCactpot, "markerCactpot"); jsonWriter.WriteObjectEnd(); } if (!bLimitedMode) { jsonWriter.WriteObjectStart("cloud"); jsonWriter.WriteBool(useCloudStorage, "use"); if (cloudToken != null) { jsonWriter.WriteString(cloudToken, "token"); } jsonWriter.WriteObjectEnd(); } { List listIds = new List(); foreach (TriadCard card in ownedCards) { listIds.Add(card.Id); } listIds.Sort(); jsonWriter.WriteArrayStart("cards"); foreach (int id in listIds) { jsonWriter.WriteInt(id); } jsonWriter.WriteArrayEnd(); } { List listIds = new List(); foreach (TriadNpc npc in completedNpcs) { listIds.Add(npc.Id); } listIds.Sort(); jsonWriter.WriteArrayStart("npcs"); foreach (int id in listIds) { jsonWriter.WriteInt(id); } jsonWriter.WriteArrayEnd(); } { jsonWriter.WriteArrayStart("decks"); foreach (KeyValuePair kvp in lastDeck) { jsonWriter.WriteObjectStart(); jsonWriter.WriteInt(kvp.Key.Id, "id"); jsonWriter.WriteArrayStart("cards"); for (int Idx = 0; Idx < kvp.Value.knownCards.Count; Idx++) { jsonWriter.WriteInt(kvp.Value.knownCards[Idx].Id); } jsonWriter.WriteArrayEnd(); jsonWriter.WriteObjectEnd(); } jsonWriter.WriteArrayEnd(); } { jsonWriter.WriteArrayStart("favDecks"); foreach (TriadDeckNamed deck in favDecks) { jsonWriter.WriteObjectStart(); if (deck != null) { jsonWriter.WriteString(deck.Name, "name"); jsonWriter.WriteArrayStart("cards"); for (int Idx = 0; Idx < deck.knownCards.Count; Idx++) { jsonWriter.WriteInt(deck.knownCards[Idx].Id); } jsonWriter.WriteArrayEnd(); } jsonWriter.WriteObjectEnd(); } jsonWriter.WriteArrayEnd(); } if (!bLimitedMode) { jsonWriter.WriteObjectStart("images"); ImageHashDB.Get().StoreHashes(customHashes, jsonWriter); jsonWriter.WriteObjectEnd(); } jsonWriter.WriteObjectEnd(); } catch (Exception ex) { Logger.WriteLine("Saving failed! Exception:" + ex); } return jsonWriter.ToString(); } public string GetBackupFolderPath() { var assembly = Assembly.GetEntryAssembly().GetName(); string settingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), assembly.Name); return settingsPath; } public void SaveBackup() { string backupJson = SaveToJson(true); if (!string.IsNullOrEmpty(backupJson)) { var assembly = Assembly.GetEntryAssembly().GetName(); int currentVersion = assembly.Version.Major; string settingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), assembly.Name); string backupPath = Path.Combine(settingsPath, "settings-backup-v" + currentVersion + ".json"); try { if (File.Exists(backupPath)) { File.Delete(backupPath); } File.WriteAllText(backupPath, backupJson); Logger.WriteLine("Saved player settings backup"); } catch (Exception ex) { Logger.WriteLine("Failed to save backup: {0}, exception:{1}", backupPath, ex); } } } public void MarkDirty() { isDirty = true; } public void UpdatePlayerDeckForNpc(TriadNpc npc, TriadDeck deck) { if (npc != null && deck != null && deck.knownCards.Count == 5) { bool bIsStarterDeck = true; for (int Idx = 0; Idx < starterCards.Length; Idx++) { bIsStarterDeck = bIsStarterDeck && (starterCards[Idx] == deck.knownCards[Idx]); } if (!bIsStarterDeck) { bool bChanged = true; if (lastDeck.ContainsKey(npc)) { if (lastDeck[npc].Equals(deck)) { bChanged = false; } else { lastDeck.Remove(npc); } } if (bChanged) { TriadCard[] deckCardsCopy = deck.knownCards.ToArray(); lastDeck.Add(npc, new TriadDeck(deckCardsCopy)); MarkDirty(); } } } } public void UpdateFavDeck(int slot, TriadDeckNamed deck) { if (slot < 0 || slot > 16) { return; } if (deck == null) { if (slot < favDecks.Count) { favDecks.RemoveAt(slot); } MarkDirty(); } else { while (favDecks.Count <= slot) { favDecks.Add(null); } bool bChanged = (deck == null) != (favDecks[slot] == null); if (!bChanged && (deck != null)) { bChanged = !deck.Name.Equals(favDecks[slot].Name) || (deck.knownCards != favDecks[slot].knownCards); } if (bChanged) { MarkDirty(); } favDecks[slot] = deck; } } public void AddKnownHash(ImageHashData hashData) { foreach (ImageHashData testHash in customHashes) { if (hashData.IsMatching(testHash, 0, out int dummyDistance)) { Logger.WriteLine("Adding hash ({0}: {1}) failed! Colision with already known ({2}: {3})", hashData.type, hashData.ownerOb, testHash.type, testHash.ownerOb); return; } } customHashes.Add(hashData); ImageHashDB.Get().hashes.Add(hashData); MarkDirty(); } public void RemoveKnownHash(ImageHashData hashData) { for (int idx = customHashes.Count - 1; idx >= 0; idx--) { ImageHashData testHash = customHashes[idx]; if (hashData.IsMatching(testHash, 0, out int dummyDistance)) { customHashes.RemoveAt(idx); ImageHashDB.Get().hashes.Remove(testHash); MarkDirty(); } } } } } ================================================ FILE: sources/data/TriadCardDB.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.IO; using System.Xml; namespace FFTriadBuddy { public class TriadCardDB { public List cards; public TriadCard hiddenCard; public string DBPath; public Dictionary> sameNumberMap; private static TriadCardDB instance = new TriadCardDB(); public TriadCardDB() { DBPath = "data/cards.xml"; cards = new List(); hiddenCard = new TriadCard(0, ETriadCardRarity.Common, ETriadCardType.None, 0, 0, 0, 0, 0, 0); hiddenCard.Name.Text[LocalizationDB.CodeLanguageIdx] = "(hidden)"; sameNumberMap = new Dictionary>(); } public static TriadCardDB Get() { return instance; } public bool Load() { try { XmlDocument xdoc = new XmlDocument(); Stream dataStream = AssetManager.Get().GetAsset(DBPath); xdoc.Load(dataStream); foreach (XmlNode cardNode in xdoc.DocumentElement.ChildNodes) { XmlElement cardElem = (XmlElement)cardNode; if (cardElem != null && cardElem.Name == "card") { try { ETriadCardRarity cardRarity = (ETriadCardRarity)int.Parse(cardElem.GetAttribute("rarity")); ETriadCardType cardType = (ETriadCardType)int.Parse(cardElem.GetAttribute("type")); int sortOrder = int.Parse(cardElem.GetAttribute("sort")); int cardGroup = int.Parse(cardElem.GetAttribute("group")); TriadCard newCard = new TriadCard( int.Parse(cardElem.GetAttribute("id")), cardRarity, cardType, ParseCardSideNum(cardElem.GetAttribute("up")), ParseCardSideNum(cardElem.GetAttribute("dn")), ParseCardSideNum(cardElem.GetAttribute("lt")), ParseCardSideNum(cardElem.GetAttribute("rt")), sortOrder, cardGroup); while (cards.Count <= newCard.Id) { cards.Add(null); } cards[newCard.Id] = newCard; } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } } } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } sameNumberMap.Clear(); int sameNumberId = 0; for (int Idx1 = 0; Idx1 < cards.Count; Idx1++) { TriadCard card1 = cards[Idx1]; if (card1 != null && card1.SameNumberId < 0) { bool bHasSameNumberCards = false; for (int Idx2 = (Idx1 + 1); Idx2 < cards.Count; Idx2++) { TriadCard card2 = cards[Idx2]; if (card2 != null && card2.SameNumberId < 0) { bool bHasSameNumbers = (card1.Sides[0] == card2.Sides[0]) && (card1.Sides[1] == card2.Sides[1]) && (card1.Sides[2] == card2.Sides[2]) && (card1.Sides[3] == card2.Sides[3]); bHasSameNumberCards = bHasSameNumberCards || bHasSameNumbers; if (bHasSameNumbers) { if (!sameNumberMap.ContainsKey(sameNumberId)) { sameNumberMap.Add(sameNumberId, new List()); sameNumberMap[sameNumberId].Add(card1); card1.SameNumberId = sameNumberId; } sameNumberMap[sameNumberId].Add(card2); card2.SameNumberId = sameNumberId; } } } if (bHasSameNumberCards) { sameNumberId++; } } } Logger.WriteLine("Loaded cards: " + cards.Count + ", same sides: " + sameNumberMap.Count); return cards.Count > 0; } public void Save() { string RawFilePath = AssetManager.Get().CreateFilePath("assets/" + DBPath); try { XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.Indent = true; XmlWriter xmlWriter = XmlWriter.Create(RawFilePath, writerSettings); xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("root"); foreach (TriadCard card in cards) { if (card != null) { xmlWriter.WriteStartElement("card"); xmlWriter.WriteAttributeString("id", card.Id.ToString()); xmlWriter.WriteAttributeString("rarity", ((int)card.Rarity).ToString()); xmlWriter.WriteAttributeString("type", ((int)card.Type).ToString()); xmlWriter.WriteAttributeString("up", card.Sides[(int)ETriadGameSide.Up].ToString()); xmlWriter.WriteAttributeString("lt", card.Sides[(int)ETriadGameSide.Left].ToString()); xmlWriter.WriteAttributeString("dn", card.Sides[(int)ETriadGameSide.Down].ToString()); xmlWriter.WriteAttributeString("rt", card.Sides[(int)ETriadGameSide.Right].ToString()); xmlWriter.WriteAttributeString("sort", card.SortOrder.ToString()); xmlWriter.WriteAttributeString("group", card.Group.ToString()); xmlWriter.WriteEndElement(); } } xmlWriter.WriteEndDocument(); xmlWriter.Close(); } catch (Exception ex) { Logger.WriteLine("Saving failed! Exception:" + ex); } } public TriadCard Find(string Name) { foreach (TriadCard testCard in cards) { if (testCard != null && testCard.Name.GetCodeName().Equals(Name, StringComparison.InvariantCultureIgnoreCase)) { return testCard; } } return null; } public TriadCard Find(int numUp, int numLeft, int numDown, int numRight) { foreach (TriadCard testCard in cards) { if (testCard != null && testCard.Sides[(int)ETriadGameSide.Up] == numUp && testCard.Sides[(int)ETriadGameSide.Down] == numDown && testCard.Sides[(int)ETriadGameSide.Left] == numLeft && testCard.Sides[(int)ETriadGameSide.Right] == numRight) { return testCard; } } return null; } private int ParseCardSideNum(string desc) { if (desc == "A" || desc == "a" || desc == "10") { return 10; } if (desc.Length == 1 && desc[0] >= '1' && desc[0] <= '9') { return desc[0] - '0'; } return -1; } } } ================================================ FILE: sources/data/TriadNpcDB.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Xml; namespace FFTriadBuddy { public class TriadNpc { public int Id; public LocString Name; public LocString LocationMap; public int LocationX; public int LocationY; public List Rules; public List Rewards; public TriadDeck Deck; public TriadNpc(int id, List rules, List rewards, int[] cardsAlways, int[] cardsPool) { Id = id; Name = LocalizationDB.Get().FindOrAddLocString(ELocStringType.NpcName, id); LocationMap = LocalizationDB.Get().FindOrAddLocString(ELocStringType.NpcLocation, id); Rules = rules; Rewards = rewards; Deck = new TriadDeck(cardsAlways, cardsPool); } public TriadNpc(int id, List rules, List rewards, TriadDeck deck) { Id = id; Name = LocalizationDB.Get().FindOrAddLocString(ELocStringType.NpcName, id); LocationMap = LocalizationDB.Get().FindOrAddLocString(ELocStringType.NpcLocation, id); Rules = rules; Rewards = rewards; Deck = deck; } public override string ToString() { return Name.GetCodeName(); } public string GetLocationDesc() { return string.Format("{0} ({1}, {2})", LocationMap.GetLocalized(), LocationX, LocationY); } } public class TriadNpcDB { public List npcs; public string DBPath; private static TriadNpcDB instance = new TriadNpcDB(); public TriadNpcDB() { DBPath = "data/npcs.xml"; npcs = new List(); } public static TriadNpcDB Get() { return instance; } public bool Load() { try { XmlDocument xdoc = new XmlDocument(); xdoc.Load(AssetManager.Get().GetAsset(DBPath)); foreach (XmlNode npcNode in xdoc.DocumentElement.ChildNodes) { XmlElement npcElem = (XmlElement)npcNode; if (npcElem != null && npcElem.Name == "npc") { try { List rules = new List(); List rewards = new List(); int[] deckA = new int[5]; int[] deckV = new int[5]; foreach (XmlNode innerNode in npcElem.ChildNodes) { XmlElement testElem = (XmlElement)innerNode; if (testElem != null) { if (testElem.Name == "rule") { int ruleId = int.Parse(testElem.GetAttribute("id")); rules.Add(TriadGameModifierDB.Get().mods[ruleId].Clone()); } else if (testElem.Name == "reward") { int cardId = int.Parse(testElem.GetAttribute("id")); rewards.Add(TriadCardDB.Get().cards[cardId]); } else if (testElem.Name == "deckA") { deckA[0] = int.Parse(testElem.GetAttribute("id0")); deckA[1] = int.Parse(testElem.GetAttribute("id1")); deckA[2] = int.Parse(testElem.GetAttribute("id2")); deckA[3] = int.Parse(testElem.GetAttribute("id3")); deckA[4] = int.Parse(testElem.GetAttribute("id4")); } else if (testElem.Name == "deckV") { deckV[0] = int.Parse(testElem.GetAttribute("id0")); deckV[1] = int.Parse(testElem.GetAttribute("id1")); deckV[2] = int.Parse(testElem.GetAttribute("id2")); deckV[3] = int.Parse(testElem.GetAttribute("id3")); deckV[4] = int.Parse(testElem.GetAttribute("id4")); } } } TriadNpc newNpc = new TriadNpc( int.Parse(npcElem.GetAttribute("id")), rules, rewards, deckA, deckV); newNpc.LocationX = int.Parse(npcElem.GetAttribute("mx")); newNpc.LocationY = int.Parse(npcElem.GetAttribute("my")); while (npcs.Count <= newNpc.Id) { npcs.Add(null); } npcs[newNpc.Id] = newNpc; } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } } } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } Logger.WriteLine("Loaded npcs: " + npcs.Count); return npcs.Count > 0; } public void Save() { string RawFilePath = AssetManager.Get().CreateFilePath("assets/" + DBPath); try { XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.Indent = true; XmlWriter xmlWriter = XmlWriter.Create(RawFilePath, writerSettings); xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("root"); foreach (TriadNpc npc in npcs) { if (npc != null) { xmlWriter.WriteStartElement("npc"); xmlWriter.WriteAttributeString("id", npc.Id.ToString()); xmlWriter.WriteAttributeString("mx", npc.LocationX.ToString()); xmlWriter.WriteAttributeString("my", npc.LocationY.ToString()); xmlWriter.WriteStartElement("deckA"); for (int Idx = 0; Idx < 5; Idx++) { xmlWriter.WriteAttributeString("id" + Idx, (npc.Deck.knownCards != null && npc.Deck.knownCards.Count > Idx) ? npc.Deck.knownCards[Idx].Id.ToString() : "0"); } xmlWriter.WriteEndElement(); xmlWriter.WriteStartElement("deckV"); for (int Idx = 0; Idx < 5; Idx++) { xmlWriter.WriteAttributeString("id" + Idx, (npc.Deck.unknownCardPool != null && npc.Deck.unknownCardPool.Count > Idx) ? npc.Deck.unknownCardPool[Idx].Id.ToString() : "0"); } xmlWriter.WriteEndElement(); for (int Idx = 0; Idx < npc.Rules.Count; Idx++) { xmlWriter.WriteStartElement("rule"); xmlWriter.WriteAttributeString("id", npc.Rules[Idx].GetLocalizationId().ToString()); xmlWriter.WriteEndElement(); } for (int Idx = 0; Idx < npc.Rewards.Count; Idx++) { xmlWriter.WriteStartElement("reward"); xmlWriter.WriteAttributeString("id", npc.Rewards[Idx].Id.ToString()); xmlWriter.WriteEndElement(); } xmlWriter.WriteEndElement(); } } xmlWriter.WriteEndDocument(); xmlWriter.Close(); } catch (Exception ex) { Logger.WriteLine("Saving failed! Exception:" + ex); } } public TriadNpc Find(string Name) { foreach (TriadNpc testNpc in npcs) { if (testNpc != null && testNpc.Name.GetCodeName().Equals(Name, StringComparison.InvariantCultureIgnoreCase)) { return testNpc; } } return null; } public List FindByReward(TriadCard card) { List result = new List(); foreach (TriadNpc testNpc in npcs) { if (testNpc != null && testNpc.Rewards.Contains(card)) { result.Add(testNpc); } } return result; } public TriadNpc FindByDeckId(string deckId) { foreach (TriadNpc testNpc in npcs) { if (testNpc != null && testNpc.Deck != null && testNpc.Deck.deckId == deckId) { return testNpc; } } return null; } } } ================================================ FILE: sources/data/TriadTournamentDB.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Xml; namespace FFTriadBuddy { public class TriadTournament { public readonly LocString Name; public readonly List Rules; public readonly int Id; public TriadTournament(int id, List rules) { Id = id; Name = LocalizationDB.Get().FindOrAddLocString(ELocStringType.TournamentName, id); Rules = rules; } public override string ToString() { return Name.GetCodeName(); } } public class TriadTournamentDB { public List tournaments; public string DBPath; private static TriadTournamentDB instance = new TriadTournamentDB(); public TriadTournamentDB() { DBPath = "data/tournaments.xml"; tournaments = new List(); } public static TriadTournamentDB Get() { return instance; } public bool Load() { try { XmlDocument xdoc = new XmlDocument(); xdoc.Load(AssetManager.Get().GetAsset(DBPath)); foreach (XmlNode ttNode in xdoc.DocumentElement.ChildNodes) { XmlElement ttElem = (XmlElement)ttNode; if (ttElem != null && ttElem.Name == "tournament") { try { List rules = new List(); foreach (XmlNode innerNode in ttElem.ChildNodes) { XmlElement testElem = (XmlElement)innerNode; if (testElem != null) { if (testElem.Name == "rule") { int ruleId = int.Parse(testElem.GetAttribute("id")); rules.Add(TriadGameModifierDB.Get().mods[ruleId].Clone()); } } } TriadTournament newTournament = new TriadTournament(int.Parse(ttElem.GetAttribute("id")), rules); while (tournaments.Count <= newTournament.Id) { tournaments.Add(null); } tournaments[newTournament.Id] = newTournament; } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } } } } catch (Exception ex) { Logger.WriteLine("Loading failed! Exception:" + ex); } Logger.WriteLine("Loaded tournaments: " + tournaments.Count); return tournaments.Count > 0; } public void Save() { string RawFilePath = AssetManager.Get().CreateFilePath("assets/" + DBPath); try { XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.Indent = true; XmlWriter xmlWriter = XmlWriter.Create(RawFilePath, writerSettings); xmlWriter.WriteStartDocument(); xmlWriter.WriteStartElement("root"); foreach (TriadTournament tournament in tournaments) { xmlWriter.WriteStartElement("tournament"); xmlWriter.WriteAttributeString("id", tournament.Id.ToString()); for (int Idx = 0; Idx < tournament.Rules.Count; Idx++) { xmlWriter.WriteStartElement("rule"); xmlWriter.WriteAttributeString("id", tournament.Rules[Idx].GetLocalizationId().ToString()); xmlWriter.WriteEndElement(); } xmlWriter.WriteEndElement(); } xmlWriter.WriteEndDocument(); xmlWriter.Close(); } catch (Exception ex) { Logger.WriteLine("Saving failed! Exception:" + ex); } } } } ================================================ FILE: sources/gamelogic/FavDeckSolver.cs ================================================ using System; using System.Threading.Tasks; namespace FFTriadBuddy { public class FavDeckSolver { private TriadDeck deck; private TriadGameSimulation currentGame; private TriadNpc npc; private TriadGameSolver solver; public int calcId; public int contextId; public int progress => (solver == null) ? 0 : (int)(solver.GetAgentProgress() * 100); public delegate void SolvedDelegate(int id, TriadDeck deck, SolverResult chance); public event SolvedDelegate OnSolved; public FavDeckSolver() { calcId = 0; } public void SetDeck(TriadDeck deck) { if (this.deck == null || deck == null || !this.deck.Equals(deck)) { this.deck = deck; CalcWinChance(); } } public void Update(TriadGameSimulation currentGame, TriadNpc npc) { bool isDirty = true; if (solver != null && solver.simulation.modifiers.Count == currentGame.modifiers.Count) { int numMatching = 0; for (int Idx = 0; Idx < currentGame.modifiers.Count; Idx++) { TriadGameModifier currentMod = solver.simulation.modifiers[Idx]; TriadGameModifier reqMod = currentGame.modifiers[Idx]; if (currentMod.GetType() == reqMod.GetType()) { numMatching++; } } isDirty = (numMatching != solver.simulation.modifiers.Count); } if (npc != this.npc) { this.npc = npc; isDirty = true; } if (isDirty) { this.currentGame = currentGame; CalcWinChance(); } } private class CalcContext { public TriadGameSolver solver; public TriadGameSimulationState gameState; public int calcId; } private void CalcWinChance() { if (currentGame != null && deck != null && npc != null) { calcId++; solver = new TriadGameSolver() { name = string.Format("Solv{0}:{1}", contextId + 1, calcId) }; solver.InitializeSimulation(currentGame.modifiers); var gameState = solver.StartSimulation(deck, npc.Deck, ETriadGameState.InProgressBlue); var calcContext = new CalcContext() { solver = solver, gameState = gameState, calcId = calcId }; Action solverAction = (ctxOb) => { var ctx = ctxOb as CalcContext; ctx.solver.FindNextMove(ctx.gameState, out var dummyCardIdx, out var dummyBoardPos, out SolverResult bestChance); OnSolved(ctx.calcId, ctx.gameState.deckBlue.deck, bestChance); }; new TaskFactory().StartNew(solverAction, calcContext); } } } } ================================================ FILE: sources/gamelogic/MiniCactpotGame.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Diagnostics; namespace FFTriadBuddy { public class CactpotGame { private static readonly int[,] cachedSolverData = new int[,] { { 2, 2, 2, 4, 4, 4, 4, 2, 2 }, { 4, 4, 4, 6, 4, 4, 4, 0, 0 }, { 0, 0, 0, 4, 4, 4, 4, 0, 0 }, { 4, 4, 4, 2, 2, 4, 4, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 4, 4, 4, 0, 0, 4, 4, 2, 2 }, { 0, 0, 0, 4, 4, 4, 4, 0, 0 }, { 4, 4, 4, 0, 0, 4, 4, 6, 6 }, { 2, 2, 2, 4, 4, 4, 4, 2, 2 } }; private static readonly int[] payouts = new int[] { 0, 0, 0, 0, 0, 0, // 0..5 - padding 10000, // 6 36, // 7 720, // 8 360, // 9 80, // 10 252, // 11 108, // 12 72, // 13 54, // 14 180, // 15 72, // 16 180, // 17 119, // 18 36, // 19 306, // 20 1080, // 21 144, // 22 1800, // 23 3600, // 24 }; private static IEnumerable> Permutate(List seq, int count) { if (count == 1) { yield return seq; } else { for (int Idx = 0; Idx < count; Idx++) { foreach (var perm in Permutate(seq, count - 1)) { yield return perm; } int swap = seq[count - 1]; seq.RemoveAt(count - 1); seq.Insert(0, swap); } } } private static void CalculateLinePayouts(int[] board, List remainingNumbers, out float[] linePayouts, bool bDebugMode = false) { // build mapping to combine board and missing number list into fully filled test board // val >= 0 : use number from board[val] // val < 0 : use number from missingNumbers[val] int[] mapBoard = new int[9]; int nextMissingId = -1; for (int Idx = 0; Idx < board.Length; Idx++) { if (board[Idx] == 0) { mapBoard[Idx] = nextMissingId; nextMissingId--; } else { mapBoard[Idx] = Idx; } } // calc avg payouts on each line from all possible permutations linePayouts = new float[8] { 0, 0, 0, 0, 0, 0, 0, 0 }; int[] testBoard = new int[9]; foreach (List permNumbers in Permutate(remainingNumbers, remainingNumbers.Count)) { for (int Idx = 0; Idx < testBoard.Length; Idx++) { testBoard[Idx] = (mapBoard[Idx] < 0) ? permNumbers[-mapBoard[Idx] - 1] : board[Idx]; } // 3 horizontal: 012, 345, 678 linePayouts[0] += payouts[testBoard[0] + testBoard[1] + testBoard[2]]; linePayouts[1] += payouts[testBoard[3] + testBoard[4] + testBoard[5]]; linePayouts[2] += payouts[testBoard[6] + testBoard[7] + testBoard[8]]; // 3 vertical: 036, 147, 258 linePayouts[3] += payouts[testBoard[0] + testBoard[3] + testBoard[6]]; linePayouts[4] += payouts[testBoard[1] + testBoard[4] + testBoard[7]]; linePayouts[5] += payouts[testBoard[2] + testBoard[5] + testBoard[8]]; // 2 diagonal: 048, 246 linePayouts[6] += payouts[testBoard[0] + testBoard[4] + testBoard[8]]; linePayouts[7] += payouts[testBoard[2] + testBoard[4] + testBoard[6]]; } int permCount = 1; for (int Idx = 2; Idx <= remainingNumbers.Count; Idx++) { permCount *= Idx; } for (int Idx = 0; Idx < linePayouts.Length; Idx++) { linePayouts[Idx] /= permCount; if (bDebugMode) { Logger.WriteLine("> line[" + Idx + "]: " + linePayouts[Idx]); } } } private static float GetBestScore(int[] board, List remainingNumbers, List remainingPos, out int bestIdx, bool bDebugMode = false) { bestIdx = 0; float bestScore = 0; // spot score: explore all possibles results recursively until reaching 4 revealed, avg all best line scores if (remainingPos.Count <= 5) { CalculateLinePayouts(board, remainingNumbers, out float[] linePayouts); for (int Idx = 0; Idx < linePayouts.Length; Idx++) { if (bestScore < linePayouts[Idx]) { bestScore = linePayouts[Idx]; bestIdx = Idx; } } return bestScore; } else { for (int PosIdx = 0; PosIdx < remainingPos.Count; PosIdx++) { int useBoardPos = remainingPos[PosIdx]; remainingPos.RemoveAt(PosIdx); float sumPos = 0; for (int NumberIdx = 0; NumberIdx < remainingNumbers.Count; NumberIdx++) { int useNumber = remainingNumbers[NumberIdx]; remainingNumbers.RemoveAt(NumberIdx); board[useBoardPos] = useNumber; float maxNestedScore = GetBestScore(board, remainingNumbers, remainingPos, out int dummyPos, bDebugMode); sumPos += maxNestedScore; remainingNumbers.Insert(NumberIdx, useNumber); } board[useBoardPos] = 0; remainingPos.Insert(PosIdx, useBoardPos); if (bestScore < sumPos) { bestScore = sumPos; bestIdx = useBoardPos; } } return bestScore / remainingPos.Count; } } public static int FindNextCircle(int[] board, bool bDebugMode = false) { // this takes way too long with only 1 known number (~3s to solve) // and is acceptable with 2+ (up to 100ms) // cache all possibile combinations of first step - call BuildCachedData() on program startup Stopwatch timer = new Stopwatch(); timer.Start(); int bestIdx = -1; List remainingSpots = new List(); for (int Idx = 0; Idx < board.Length; Idx++) { if (board[Idx] == 0) { remainingSpots.Add(Idx); } else { bestIdx = cachedSolverData[Idx, board[Idx] - 1]; } } if (remainingSpots.Count < 8) { List remainingNumbers = new List(); for (int Idx = 1; Idx <= 9; Idx++) { if (Array.IndexOf(board, Idx) < 0) { remainingNumbers.Add(Idx); } } GetBestScore(board, remainingNumbers, remainingSpots, out bestIdx, bDebugMode); } if (bDebugMode) { Logger.WriteLine("> best: " + bestIdx); } timer.Stop(); Logger.WriteLine("FindNextCircle: " + timer.ElapsedMilliseconds + "ms (missing: " + remainingSpots.Count + ")"); return bestIdx; } public static void FindBestLine(int[] board, out int fromIdx, out int toIdx, bool bDebugMode = false) { List remainingNumbers = new List(); for (int Idx = 1; Idx <= 9; Idx++) { if (Array.IndexOf(board, Idx) < 0) { remainingNumbers.Add(Idx); } } fromIdx = -1; toIdx = -1; if (remainingNumbers.Count <= 5) { List dummyList = new List(); GetBestScore(board, remainingNumbers, dummyList, out int bestLine); switch (bestLine) { case 0: fromIdx = 0; toIdx = 2; break; case 1: fromIdx = 3; toIdx = 5; break; case 2: fromIdx = 6; toIdx = 8; break; case 3: fromIdx = 0; toIdx = 6; break; case 4: fromIdx = 1; toIdx = 7; break; case 5: fromIdx = 2; toIdx = 8; break; case 6: fromIdx = 0; toIdx = 8; break; case 7: fromIdx = 2; toIdx = 6; break; default: break; } } } public static void BuildCachedData() { int[] board = new int[9] { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; string cacheStr = "{ "; for (int PosIdx = 0; PosIdx < 9; PosIdx++) { cacheStr += "{ "; for (int NumberIdx = 1; NumberIdx <= 9; NumberIdx++) { board[PosIdx] = NumberIdx; int bestCircleIdx = FindNextCircle(board); cacheStr += bestCircleIdx + (NumberIdx < 9 ? ", " : ""); } cacheStr += " }" + (PosIdx < 8 ? ", " : ""); board[PosIdx] = 0; } cacheStr += " };"; Logger.WriteLine("int[,] cachedSolverData = new int[,]" + cacheStr); } } } ================================================ FILE: sources/gamelogic/TriadCard.cs ================================================ using System; namespace FFTriadBuddy { public enum ETriadCardRarity { Common, Uncommon, Rare, Epic, Legendary } public enum ETriadCardType { None, Beastman, Primal, Scion, Garlean, } public enum ETriadCardOwner { Unknown, Blue, Red, } public enum ETriadGameSide { Up, Left, Down, Right, } public class TriadCard : IEquatable { public int Id; public LocString Name; public ETriadCardRarity Rarity; public ETriadCardType Type; public int[] Sides; public int SameNumberId; public int SortOrder; public int Group; public float OptimizerScore; public int SmallIconId => 88000 + Id; public int BigIconId => 87000 + Id; public TriadCard() { Id = -1; Sides = new int[4] { 0, 0, 0, 0 }; SameNumberId = -1; SortOrder = 0; Group = 0; OptimizerScore = 0.0f; } public TriadCard(int id, ETriadCardRarity rarity, ETriadCardType type, int numUp, int numDown, int numLeft, int numRight, int sortOrder, int group) { Id = id; Name = LocalizationDB.Get().FindOrAddLocString(ELocStringType.CardName, id); Rarity = rarity; Type = type; Sides = new int[4] { numUp, numLeft, numDown, numRight }; SameNumberId = -1; SortOrder = sortOrder; Group = group; if (group != 0 && SortOrder < 1000) { SortOrder += 1000; } OptimizerScore = TriadDeckOptimizer.GetCardScore(this); } public override bool Equals(object obj) { return Equals(obj as TriadCard); } public bool Equals(TriadCard other) { return (other != null) && (Id == other.Id); } public override int GetHashCode() { return 2108858624 + Id.GetHashCode(); } public bool IsValid() { return (Id >= 0) && (Sides[0] >= 1) && (Sides[0] <= 10) && (Sides[1] >= 1) && (Sides[1] <= 10) && (Sides[2] >= 1) && (Sides[2] <= 10) && (Sides[3] >= 1) && (Sides[3] <= 10); } public string ToShortCodeString() { return "[" + Id + ":" + Name.GetCodeName() + "]"; } public string ToShortLocalizedString() { return "[" + Id + ":" + Name.GetLocalized() + "]"; } public string ToLocalizedString() { return string.Format("[{0}] {1} {2} [{3}, {4}, {5}, {6}]", Id, Name.GetLocalized(), new string('*', (int)Rarity + 1), Sides[0], Sides[1], Sides[2], Sides[3], (Type != ETriadCardType.None) ? " [" + LocalizationDB.Get().LocCardTypes[(int)Type] + "]" : ""); } public override string ToString() { return string.Format("[{0}] {1} {2} [{3}, {4}, {5}, {6}]", Id, Name.GetCodeName(), new string('*', (int)Rarity + 1), Sides[0], Sides[1], Sides[2], Sides[3], (Type != ETriadCardType.None) ? " [" + Type + "]" : ""); } } public class TriadCardInstance { public readonly TriadCard card; public ETriadCardOwner owner; public int scoreModifier; public TriadCardInstance(TriadCard card, ETriadCardOwner owner) { this.card = card; this.owner = owner; scoreModifier = 0; } public TriadCardInstance(TriadCardInstance copyFrom) { card = copyFrom.card; owner = copyFrom.owner; scoreModifier = copyFrom.scoreModifier; } public override string ToString() { return owner + " " + card + ((scoreModifier > 0) ? (" +" + scoreModifier) : (scoreModifier < 0) ? (" -" + scoreModifier) : ""); } public int GetRawNumber(ETriadGameSide side) { return card.Sides[(int)side]; } public int GetNumber(ETriadGameSide side) { return Math.Min(Math.Max(GetRawNumber(side) + scoreModifier, 1), 10); } public int GetOppositeNumber(ETriadGameSide side) { return GetNumber((ETriadGameSide)(((int)side + 2) % 4)); } } } ================================================ FILE: sources/gamelogic/TriadDeck.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; namespace FFTriadBuddy { public enum ETriadDeckState { Valid, MissingCards, HasDuplicates, TooMany4Star, TooMany5Star, }; public class TriadDeck { public List knownCards; public List unknownCardPool; public string deckId; public TriadDeck() { knownCards = new List(); unknownCardPool = new List(); } public TriadDeck(List knownCards, List unknownCardPool) { this.knownCards = new List(); this.unknownCardPool = new List(); this.knownCards.AddRange(knownCards); this.unknownCardPool.AddRange(unknownCardPool); UpdateDeckId(); } public TriadDeck(IEnumerable knownCards) { this.knownCards = new List(); unknownCardPool = new List(); this.knownCards.AddRange(knownCards); UpdateDeckId(); } public TriadDeck(IEnumerable knownCardIds, IEnumerable unknownCardlIds) { TriadCardDB cardDB = TriadCardDB.Get(); knownCards = new List(); foreach (int id in knownCardIds) { TriadCard card = cardDB.cards[id]; if (card != null && card.IsValid()) { knownCards.Add(card); } } unknownCardPool = new List(); foreach (int id in unknownCardlIds) { TriadCard card = cardDB.cards[id]; if (card != null && card.IsValid()) { unknownCardPool.Add(card); } } UpdateDeckId(); } public TriadDeck(IEnumerable knownCardIds) { TriadCardDB cardDB = TriadCardDB.Get(); knownCards = new List(); foreach (int id in knownCardIds) { TriadCard card = cardDB.cards[id]; if (card != null && card.IsValid()) { knownCards.Add(card); } } unknownCardPool = new List(); UpdateDeckId(); } public ETriadDeckState GetDeckState() { PlayerSettingsDB playerDB = PlayerSettingsDB.Get(); int[] rarityCounters = new int[5]; for (int DeckIdx = 0; DeckIdx < knownCards.Count; DeckIdx++) { TriadCard deckCard = knownCards[DeckIdx]; bool bIsOwned = playerDB.ownedCards.Contains(deckCard); if (!bIsOwned) { return ETriadDeckState.MissingCards; } for (int TestIdx = 0; TestIdx < knownCards.Count; TestIdx++) { if ((TestIdx != DeckIdx) && knownCards[TestIdx].Equals(deckCard)) { return ETriadDeckState.HasDuplicates; } } rarityCounters[(int)deckCard.Rarity]++; } int numRare5 = rarityCounters[(int)ETriadCardRarity.Legendary]; int numRare45 = rarityCounters[(int)ETriadCardRarity.Epic] + numRare5; if (numRare5 > 1) { return ETriadDeckState.TooMany5Star; } else if (numRare45 > 2) { return ETriadDeckState.TooMany4Star; } return ETriadDeckState.Valid; } public TriadCard GetCard(int Idx) { if (Idx < knownCards.Count) { return knownCards[Idx]; } else if (Idx < (knownCards.Count + unknownCardPool.Count)) { return unknownCardPool[Idx - knownCards.Count]; } return null; } public int GetCardIndex(TriadCard card) { int cardIdx = knownCards.IndexOf(card); if (cardIdx >= 0) { return cardIdx; } cardIdx = unknownCardPool.IndexOf(card); if (cardIdx >= 0) { return cardIdx + knownCards.Count; } return -1; } public bool SetCard(int Idx, TriadCard card) { bool bResult = false; if (Idx < knownCards.Count) { knownCards[Idx] = card; bResult = true; } else if (Idx < (knownCards.Count + unknownCardPool.Count)) { unknownCardPool[Idx - knownCards.Count] = card; bResult = true; } return bResult; } public int GetPower() { int SumRating = 0; foreach (TriadCard card in knownCards) { SumRating += (int)card.Rarity + 1; } foreach (TriadCard card in unknownCardPool) { SumRating += (int)card.Rarity + 1; } int NumCards = knownCards.Count + unknownCardPool.Count; int DeckPower = (NumCards > 0) ? System.Math.Min(System.Math.Max((SumRating * 2 / NumCards), 1), 10) : 1; return DeckPower; } public void UpdateDeckId() { deckId = "K"; List sortedList = new List(); sortedList.AddRange(knownCards); sortedList.Sort((a, b) => a.Id.CompareTo(b.Id)); foreach (TriadCard card in sortedList) { deckId += ":"; deckId += card.Id; } deckId += "U"; sortedList.Clear(); sortedList.AddRange(unknownCardPool); sortedList.Sort((a, b) => a.Id.CompareTo(b.Id)); foreach (TriadCard card in sortedList) { deckId += ":"; deckId += card.Id; } } public override bool Equals(object obj) { return Equals(obj as TriadDeck); } public bool Equals(TriadDeck otherDeck) { if (deckId != null && otherDeck.deckId != null) { return deckId.Equals(otherDeck.deckId); } if ((knownCards.Count != otherDeck.knownCards.Count) || (unknownCardPool.Count != otherDeck.unknownCardPool.Count)) { return false; } for (int Idx = 0; Idx < knownCards.Count; Idx++) { if (!knownCards[Idx].Equals(otherDeck.knownCards[Idx])) { return false; } } for (int Idx = 0; Idx < unknownCardPool.Count; Idx++) { if (!unknownCardPool[Idx].Equals(otherDeck.unknownCardPool[Idx])) { return false; } } return true; } public override int GetHashCode() { var hashCode = 739328532; hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(knownCards); hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(unknownCardPool); return hashCode; } public override string ToString() { string desc = ""; foreach (TriadCard card in knownCards) { desc += card.ToShortCodeString() + ", "; } desc = (desc.Length > 2) ? desc.Remove(desc.Length - 2, 2) : "(none)"; if (unknownCardPool.Count > 0) { desc += " + unknown("; foreach (TriadCard card in unknownCardPool) { desc += card.ToShortCodeString() + ", "; } desc = desc.Remove(desc.Length - 2, 2); desc += ")"; } int power = GetPower(); desc += ", power:" + power; return desc; } } public class TriadDeckNamed : TriadDeck { public string Name; public TriadDeckNamed() { } public TriadDeckNamed(TriadDeck copyFrom) : base(copyFrom.knownCards) { } } public abstract class TriadDeckInstance { public abstract void OnCardPlacedFast(int Idx); public abstract int GetFirstAvailableCardFast(); public abstract TriadCard GetCard(int Idx); public abstract int GetCardIndex(TriadCard card); public abstract TriadDeckInstance CreateCopy(); public TriadDeck deck; public int availableCardMask; public int numUnknownPlaced; public int numPlaced; // manual, player = 5 // manual, npc = (up to 5 fixed) + (up to 5 variable) // screen, npc = (up to 5 hidden) + (up to 5 fixed) + (up to 5 variable) public const int maxAvailableCards = 15; public bool IsPlaced(int cardIdx) { return (availableCardMask & (1 << cardIdx)) == 0; } public TriadCard GetFirstAvailableCard() { int cardIdx = GetFirstAvailableCardFast(); return (cardIdx < 0) ? null : GetCard(cardIdx); } public List GetAvailableCards() { List cards = new List(); for (int Idx = 0; Idx < maxAvailableCards; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { TriadCard card = GetCard(Idx); cards.Add(card); } } return cards; } } public class TriadDeckInstanceManual : TriadDeckInstance { public TriadDeckInstanceManual(TriadDeck deck) { this.deck = deck; numUnknownPlaced = 0; numPlaced = 0; availableCardMask = (1 << (deck.knownCards.Count + deck.unknownCardPool.Count)) - 1; } public TriadDeckInstanceManual(TriadDeckInstanceManual copyFrom) { deck = copyFrom.deck; numUnknownPlaced = copyFrom.numUnknownPlaced; numPlaced = copyFrom.numPlaced; availableCardMask = copyFrom.availableCardMask; } public override TriadDeckInstance CreateCopy() { TriadDeckInstanceManual deckCopy = new TriadDeckInstanceManual(this); return deckCopy; } public override void OnCardPlacedFast(int cardIdx) { availableCardMask &= ~(1 << cardIdx); numPlaced++; if (cardIdx >= deck.knownCards.Count) { const int maxCardsToPlace = ((TriadGameSimulationState.boardSize * TriadGameSimulationState.boardSize) / 2) + 1; int maxUnknownToPlace = maxCardsToPlace - deck.knownCards.Count; numUnknownPlaced++; if (numUnknownPlaced >= maxUnknownToPlace) { availableCardMask &= ((1 << deck.knownCards.Count) - 1); } } } public override int GetFirstAvailableCardFast() { for (int Idx = 0; Idx < deck.knownCards.Count; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { return Idx; } } return -1; } public override TriadCard GetCard(int Idx) { return deck.GetCard(Idx); } public override int GetCardIndex(TriadCard card) { int cardIdx = deck.knownCards.IndexOf(card); if (cardIdx < 0) { cardIdx = deck.unknownCardPool.IndexOf(card) + deck.knownCards.Count; } return cardIdx; } public override string ToString() { string desc = "Placed: " + numPlaced + ", Available: "; if (availableCardMask > 0) { for (int Idx = 0; Idx < maxAvailableCards; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { TriadCard card = GetCard(Idx); desc += card.ToShortCodeString() + ", "; } } desc = desc.Remove(desc.Length - 2, 2); } else { desc += "none"; } return desc; } } public class TriadDeckInstanceScreen : TriadDeckInstance { public TriadCard[] cards; public TriadCard swappedCard; public int unknownPoolMask; public int swappedCardIdx; public TriadDeckInstanceScreen() { cards = new TriadCard[5]; availableCardMask = 0; unknownPoolMask = 0; numUnknownPlaced = 0; numPlaced = 0; swappedCardIdx = -1; swappedCard = null; } public TriadDeckInstanceScreen(TriadDeckInstanceScreen copyFrom) { cards = new TriadCard[copyFrom.cards.Length]; for (int Idx = 0; Idx < copyFrom.cards.Length; Idx++) { cards[Idx] = copyFrom.cards[Idx]; } deck = copyFrom.deck; numUnknownPlaced = copyFrom.numUnknownPlaced; numPlaced = copyFrom.numPlaced; availableCardMask = copyFrom.availableCardMask; unknownPoolMask = copyFrom.unknownPoolMask; swappedCardIdx = copyFrom.swappedCardIdx; swappedCard = copyFrom.swappedCard; } public override TriadDeckInstance CreateCopy() { TriadDeckInstanceScreen deckCopy = new TriadDeckInstanceScreen(this); return deckCopy; } public override int GetFirstAvailableCardFast() { for (int Idx = 0; Idx < cards.Length; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { return Idx; } } return -1; } public void UpdateAvailableCards(TriadCard[] screenCards) { availableCardMask = 0; numPlaced = 0; numUnknownPlaced = 0; Array.Copy(screenCards, cards, 5); int hiddenCardId = TriadCardDB.Get().hiddenCard.Id; for (int Idx = 0; Idx < cards.Length; Idx++) { if (cards[Idx] != null) { if (cards[Idx].Id != hiddenCardId) { availableCardMask |= (1 << Idx); } } else { numPlaced++; } } } public void SetSwappedCard(TriadCard swappedCard, int swappedCardIdx) { this.swappedCard = swappedCard; this.swappedCardIdx = swappedCardIdx; unknownPoolMask &= ~(1 << swappedCardIdx); } public override void OnCardPlacedFast(int cardIdx) { int cardMask = (1 << cardIdx); availableCardMask &= ~cardMask; if (deck != null) { bool bIsUnknown = (unknownPoolMask & cardMask) != 0; if (bIsUnknown) { numUnknownPlaced++; int maxUnknownToUse = cards.Length - deck.knownCards.Count; if (numUnknownPlaced >= maxUnknownToUse) { availableCardMask &= ~unknownPoolMask; } } } } public override TriadCard GetCard(int Idx) { return (Idx < 0) ? null : (Idx == swappedCardIdx) ? swappedCard : (Idx < cards.Length) ? cards[Idx] : (deck != null) ? deck.GetCard(Idx - cards.Length) : null; } public override int GetCardIndex(TriadCard card) { if (card == swappedCard) { return swappedCardIdx; } int cardIdx = Array.IndexOf(cards, card); if (cardIdx < 0 && deck != null) { cardIdx = deck.GetCardIndex(card); if (cardIdx >= 0) { cardIdx += cards.Length; } } return cardIdx; } public override string ToString() { string desc = "[SCREEN] Available: "; if (availableCardMask > 0) { for (int Idx = 0; Idx < cards.Length; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { TriadCard card = GetCard(Idx); desc += (card != null ? card.Name.GetCodeName() : "") + (Idx == swappedCardIdx ? ":SWAP" : "") + ", "; } } desc = desc.Remove(desc.Length - 2, 2); } else { desc += "none"; } int visibleCardsMask = (cards != null) ? ((1 << cards.Length) - 1) : 0; bool hasHiddenCards = (availableCardMask & ~visibleCardsMask) != 0; if (hasHiddenCards) { desc += ", Unknown: "; if (deck != null) { for (int Idx = cards.Length; Idx < maxAvailableCards; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; if (bIsAvailable) { TriadCard card = GetCard(Idx); bool bIsKnownPool = (unknownPoolMask & (1 << Idx)) == 0; desc += card.ToShortCodeString() + ":" + Idx + ":" + (bIsKnownPool ? "K" : "U") + (Idx == swappedCardIdx ? ":SWAP" : "") + ", "; } } desc = desc.Remove(desc.Length - 2, 2); } else { desc += "(missing deck!)"; } } return desc; } public void LogAvailableCards(string deckName) { Logger.WriteLine(deckName + " state> numPlaced:" + numPlaced + ", numUnknownPlaced:" + numUnknownPlaced); for (int Idx = 0; Idx < maxAvailableCards; Idx++) { bool bIsAvailable = (availableCardMask & (1 << Idx)) != 0; bool bIsUnknown = (unknownPoolMask & (1 << Idx)) != 0; TriadCard card = GetCard(Idx); Logger.WriteLine(" [" + Idx + "]:" + (card != null ? card.Name.GetCodeName() : "??") + (Idx == swappedCardIdx ? " (SWAP)" : bIsUnknown ? " (U)" : "") + " => " + (bIsAvailable ? "available" : "nope")); } } } } ================================================ FILE: sources/gamelogic/TriadDeckOptimizer.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Threading; using System.Threading.Tasks; namespace FFTriadBuddy { public class TriadDeckOptimizer { public TriadDeck optimizedDeck; private TriadNpc npc; private long numPossibleDecks; private long numTestedDecks; private long numMsElapsed; private int numGamesToPlay; private int numPriorityToBuild; private int numCommonToBuild; private int numCommonPctToDropPerPriSlot; private Dictionary maxSlotsPerRarity; private ETriadCardRarity commonRarity; private int[][] permutationList; private bool bAbort; private bool debugMode; public delegate void FoundDeckDelegate(TriadDeck deck, float estWinChance); public delegate void UpdatePossibleCount(string numPossibleDesc); public event FoundDeckDelegate OnFoundDeck; public float parallelLoadPct = -1.0f; private struct CardPool { public TriadCard[][] priorityLists; public TriadCard[] commonList; public int[] deckSlotTypes; } private CardPool currentPool; private TriadGameSolver currentSolver; private bool isOrderImportant; private const int DeckSlotCommon = -1; private const int DeckSlotLocked = -2; private ManualResetEvent loopPauseEvent = new ManualResetEvent(true); private bool isPaused; public TriadDeckOptimizer() { numGamesToPlay = 2000; numPriorityToBuild = 10; numCommonToBuild = 20; numCommonPctToDropPerPriSlot = 10; maxSlotsPerRarity = new Dictionary(); maxSlotsPerRarity.Add(ETriadCardRarity.Legendary, 1); maxSlotsPerRarity.Add(ETriadCardRarity.Epic, 2); commonRarity = ETriadCardRarity.Rare; debugMode = false; bAbort = false; #if DEBUG debugMode = true; #endif // DEBUG // generate lookup for permutations used when deck order is important // num entries = 5! = 120 permutationList = new int[120][]; int ListIdx = 0; for (int IdxP0 = 0; IdxP0 < 5; IdxP0++) { for (int IdxP1 = 0; IdxP1 < 5; IdxP1++) { if (IdxP1 == IdxP0) { continue; } for (int IdxP2 = 0; IdxP2 < 5; IdxP2++) { if (IdxP2 == IdxP0 || IdxP2 == IdxP1) { continue; } for (int IdxP3 = 0; IdxP3 < 5; IdxP3++) { if (IdxP3 == IdxP0 || IdxP3 == IdxP1 || IdxP3 == IdxP2) { continue; } for (int IdxP4 = 0; IdxP4 < 5; IdxP4++) { if (IdxP4 == IdxP0 || IdxP4 == IdxP1 || IdxP4 == IdxP2 || IdxP4 == IdxP3) { continue; } permutationList[ListIdx] = new int[5] { IdxP0, IdxP1, IdxP2, IdxP3, IdxP4 }; ListIdx++; } } } } } } public void Initialize(TriadNpc npc, TriadGameModifier[] regionMods, List lockedCards) { this.npc = npc; numPossibleDecks = 1; numTestedDecks = 0; numMsElapsed = 0; PlayerSettingsDB playerDB = PlayerSettingsDB.Get(); currentSolver = new TriadGameSolver(); currentSolver.InitializeSimulation(npc.Rules, regionMods); isOrderImportant = false; foreach (TriadGameModifier mod in currentSolver.simulation.modifiers) { isOrderImportant = isOrderImportant || mod.IsDeckOrderImportant(); } bool foundCards = FindCardPool(playerDB.ownedCards, currentSolver.simulation.modifiers, lockedCards); if (foundCards) { UpdatePossibleDeckCount(); } } public Task Process(TriadNpc npc, TriadGameModifier[] regionMods, List lockedCards) { this.npc = npc; numTestedDecks = 0; numMsElapsed = 0; bAbort = false; return Task.Run(() => { FindDecksScored(regionMods, lockedCards); }); } public void AbortProcess() { bAbort = true; } public bool IsAborted() { return bAbort; } public void GuessDeck(List lockedCards) { if (currentPool.commonList == null && currentPool.priorityLists == null) { Logger.WriteLine("Skip deck building, everything was locked"); optimizedDeck = new TriadDeck(lockedCards); } else { SlotIterator slotIterator = new SlotIterator(currentPool, lockedCards); optimizedDeck = null; long skipCounter = 0; long randomSkipRange = numPossibleDecks / 100; if (randomSkipRange > 0) { var rand = new Random(); skipCounter = rand.Next((int)randomSkipRange); Logger.WriteLine("GuessDeck: {0} / {1}", skipCounter, randomSkipRange); } var deckList = slotIterator.GetDecks(skipCounter); foreach (var deckInfo in deckList) { if (deckInfo.IsValid()) { optimizedDeck = new TriadDeck(deckInfo.Cards); break; } } if (optimizedDeck == null) { optimizedDeck = new TriadDeck(PlayerSettingsDB.Get().starterCards); } } } private void UpdatePossibleDeckCount() { numPossibleDecks = 1; // common slot loops will avoid repeating the same items, include in num iterations int numCommonSlots = 0; for (int idx = 0; idx < currentPool.deckSlotTypes.Length; idx++) { int slotType = currentPool.deckSlotTypes[idx]; if (slotType == DeckSlotCommon) { numCommonSlots++; } else if (slotType >= 0) { numPossibleDecks *= currentPool.priorityLists[slotType].Length; } } // num combinations without repetition: // = (pool! / (pool - common)!) / common! // = (pool * (pool - 1) * .. * (pool - common + 1) * (pool - common)! / (pool - common)!) / common! // = (pool * (pool - 1) * .. * (pool - common + 1)) / common! if (numCommonSlots > 0) { int FactNumCommon = 1; for (int Idx = 0; Idx < numCommonSlots; Idx++) { numPossibleDecks *= (currentPool.commonList.Length - Idx); FactNumCommon *= (Idx + 1); } numPossibleDecks /= FactNumCommon; } } private int GetRandomSeed(int Idx0, int Idx1, int Idx2, int Idx3, int Idx4) { int Hash = 13; Hash = (Hash * 37) + Idx0; Hash = (Hash * 37) + Idx1; Hash = (Hash * 37) + Idx2; Hash = (Hash * 37) + Idx3; Hash = (Hash * 37) + Idx4; return Hash; } private int GetDeckScore(TriadGameSolver solver, TriadDeck testDeck, int randomSeed, int numGamesDiv) { var agentRandom = new TriadGameAgentRandom(solver, randomSeed); int deckScore = 0; int maxGames = (numGamesToPlay / numGamesDiv) / 2; for (int IdxGame = 0; IdxGame < maxGames; IdxGame++) { var gameStateR = solver.StartSimulation(testDeck, npc.Deck, ETriadGameState.InProgressRed); solver.RunSimulation(gameStateR, agentRandom, agentRandom); deckScore += (gameStateR.state == ETriadGameState.BlueWins) ? 2 : (gameStateR.state == ETriadGameState.BlueDraw) ? 1 : 0; var gameStateB = solver.StartSimulation(testDeck, npc.Deck, ETriadGameState.InProgressBlue); solver.RunSimulation(gameStateB, agentRandom, agentRandom); deckScore += (gameStateB.state == ETriadGameState.BlueWins) ? 2 : (gameStateB.state == ETriadGameState.BlueDraw) ? 1 : 0; } return deckScore; } private struct CardScoreData : IComparable { public TriadCard card; public float score; public int CompareTo(CardScoreData other) { return -score.CompareTo(other.score); } public override string ToString() { return card.ToShortCodeString() + ", score: " + score; } } private void ApplyAscentionFilter(List commonScoredList, List> priScoredList) { Func FindCardAscValue = scoredEntry => scoredEntry.score; int maxCardTypes = Enum.GetValues(typeof(ETriadCardType)).Length; int maxLists = priScoredList.Count + 1; List[,] mapCardAscValues = new List[maxCardTypes, maxLists]; for (int idxL = 0; idxL < priScoredList.Count + 1; idxL++) { for (int idxT = 0; idxT < maxCardTypes; idxT++) { mapCardAscValues[idxT, idxL] = new List(); } if (idxL > 0) { foreach (var scoredEntry in priScoredList[idxL - 1]) { mapCardAscValues[(int)scoredEntry.card.Type, idxL].Add(FindCardAscValue(scoredEntry)); } } } foreach (var scoredEntry in commonScoredList) { if (scoredEntry.card.Type != ETriadCardType.None) { mapCardAscValues[(int)scoredEntry.card.Type, 0].Add(FindCardAscValue(scoredEntry)); } } ETriadCardType bestType = ETriadCardType.None; float bestScore = 0; if (debugMode) { Logger.WriteLine("Ascension filter..."); } for (int idxT = 0; idxT < maxCardTypes; idxT++) { if (idxT == (int)ETriadCardType.None) { continue; } float[] typePartialScores = new float[maxLists]; float typeScore = 0; for (int idxL = 0; idxL < maxLists; idxL++) { for (int cardIdx = 0; cardIdx < mapCardAscValues[idxT, idxL].Count; cardIdx++) { typePartialScores[idxL] += mapCardAscValues[idxT, idxL][cardIdx]; } if (mapCardAscValues[idxT, idxL].Count == 0) { typePartialScores[idxL] = 0; } else { typePartialScores[idxL] /= mapCardAscValues[idxT, idxL].Count; } typeScore += typePartialScores[idxL]; } typeScore /= maxLists; if (debugMode) { Logger.WriteLine(" [{0}]: score:{1} ({2})", (ETriadCardType)idxT, typeScore, string.Join(", ", typePartialScores)); } if (bestScore <= 0.0f || typeScore > bestScore) { bestScore = typeScore; bestType = (ETriadCardType)idxT; } } if (bestType != ETriadCardType.None) { if (debugMode) { Logger.WriteLine(" best: {0}", bestType); } Action IncreaseScoreForType = (scoredEntry, cardType) => { if (scoredEntry.card.Type == cardType) { scoredEntry.score += 1000.0f; } }; foreach (var scoredEntry in commonScoredList) { IncreaseScoreForType(scoredEntry, bestType); } foreach (var priList in priScoredList) { foreach (var scoredEntry in priList) { IncreaseScoreForType(scoredEntry, bestType); } } } } private bool FindCardPool(List allCards, List modifiers, List lockedCards) { currentPool = new CardPool(); int maxRarityNum = Enum.GetValues(typeof(ETriadCardRarity)).Length; int priRarityNum = (int)commonRarity + 1; int[] mapAvailRarity = new int[maxRarityNum]; var modifiersCopy = new List(); modifiersCopy.AddRange(modifiers); int reverseModIdx = modifiersCopy.FindIndex(mod => mod.GetType() == typeof(TriadGameModifierReverse)); bool hasReverseMod = reverseModIdx >= 0; int ascentionModIdx = modifiersCopy.FindIndex(mod => mod.GetType() == typeof(TriadGameModifierAscention)); bool hasAscensionMod = ascentionModIdx >= 0; ; int descentionModIdx = modifiersCopy.FindIndex(mod => mod.GetType() == typeof(TriadGameModifierDescention)); bool hasDescentionMod = descentionModIdx >= 0; ; // special cases: // - reverse: don't include any rare slots if there's enough cards in common list // - reverse + ascention: swap for descention rule to additionally penalize picking cards with type // - reverse + descention: swap for ascention rule to select cards of shared type if (hasReverseMod && hasAscensionMod) { hasAscensionMod = false; modifiersCopy.RemoveAt(ascentionModIdx); modifiersCopy.Add(TriadGameModifierDB.Get().mods.Find(mod => mod.GetType() == typeof(TriadGameModifierDescention))); } else if (hasReverseMod && hasDescentionMod) { hasAscensionMod = true; modifiersCopy.RemoveAt(descentionModIdx); modifiersCopy.Add(TriadGameModifierDB.Get().mods.Find(mod => mod.GetType() == typeof(TriadGameModifierAscention))); } // find number of priority lists based on unique rarity limits List priRarityThr = new List(); for (int idxR = priRarityNum; idxR < maxRarityNum; idxR++) { ETriadCardRarity testRarity = (ETriadCardRarity)idxR; if (!hasReverseMod && maxSlotsPerRarity.ContainsKey(testRarity) && maxSlotsPerRarity[testRarity] > 0) { mapAvailRarity[idxR] = maxSlotsPerRarity[testRarity]; mapAvailRarity[idxR - 1] -= maxSlotsPerRarity[testRarity]; priRarityThr.Add(testRarity); } } if (debugMode) { Logger.WriteLine("FindCardPool> priRarityThr:{0}, maxAvail:[{1},{2},{3},{4},{5}], reverse:{6}, ascention:{7}", priRarityThr.Count, mapAvailRarity[0], mapAvailRarity[1], mapAvailRarity[2], mapAvailRarity[3], mapAvailRarity[4], hasReverseMod, hasAscensionMod); } // check rarity of locked cards, eliminate pri list when threshold is matched // when multiple pri rarities are locked, start eliminating from pool above // e.g. 2x 4 star locked => 4 star out, 5 star out currentPool.deckSlotTypes = new int[lockedCards.Count]; int numLockedCards = 0; for (int idx = 0; idx < lockedCards.Count; idx++) { TriadCard card = lockedCards[idx]; if (card != null) { if (card.Rarity > commonRarity) { for (int testR = (int)card.Rarity; testR <= maxRarityNum; testR++) { if (mapAvailRarity[testR] > 0) { mapAvailRarity[testR]--; break; } } } currentPool.deckSlotTypes[idx] = DeckSlotLocked; numLockedCards++; } else { currentPool.deckSlotTypes[idx] = DeckSlotCommon; } } if (debugMode) { Logger.WriteLine(">> adjusted for locking, numLocked:{0}, maxAvail:[{1},{2},{3},{4},{5}]", numLockedCards, mapAvailRarity[0], mapAvailRarity[1], mapAvailRarity[2], mapAvailRarity[3], mapAvailRarity[4]); } if (numLockedCards == lockedCards.Count) { return false; } List commonScoredList = new List(); List> priScoredList = new List>(); for (int idxP = 0; idxP < priRarityThr.Count; idxP++) { priScoredList.Add(new List()); } // reverse priority thresholds, idx:0 becomes strongest card priRarityThr.Reverse(); // assign each owned card to scored lists foreach (TriadCard card in allCards) { if (card == null || !card.IsValid()) { continue; } CardScoreData scoredCard = new CardScoreData() { card = card, score = card.OptimizerScore }; foreach (TriadGameModifier mod in modifiersCopy) { mod.OnScoreCard(card, ref scoredCard.score); } for (int idxP = 0; idxP < priRarityThr.Count; idxP++) { if (card.Rarity <= priRarityThr[idxP]) { priScoredList[idxP].Add(scoredCard); } } if (card.Rarity <= commonRarity) { commonScoredList.Add(scoredCard); } } if (debugMode) { Logger.WriteLine(">> card lists sorted, common:{0}", commonScoredList.Count); } bool isPoolValid = (commonScoredList.Count > 0); if (isPoolValid) { int numPriLists = 0; int deckSlotIdx = isOrderImportant ? 1 : 0; for (int idx = 0; idx < priScoredList.Count; idx++) { int numAvail = mapAvailRarity[(int)priRarityThr[idx]]; if (debugMode) { Logger.WriteLine(" pri list[{0}]:{1}, rarity:{2}, avail:{3}", idx, priScoredList[idx].Count, priRarityThr[idx], numAvail); } if ((numAvail > 0) && (priScoredList[idx].Count > 0)) { // initial deckSlotIdx should be already past only available spot (e.g. all slots but [0] are locked), make sure to wrap around // find fist available Common slot to overwrite with priority list, repeat numAvail times for (int idxAvail = 0; idxAvail < numAvail; idxAvail++) { for (int idxD = 0; idxD < currentPool.deckSlotTypes.Length; idxD++) { if (currentPool.deckSlotTypes[deckSlotIdx] == DeckSlotCommon) { break; } deckSlotIdx++; } currentPool.deckSlotTypes[deckSlotIdx] = numPriLists; } numPriLists++; } else { priScoredList[idx].Clear(); } } // ascension modifier special case: same type across all pools is best // aply after priority lists were trimmed if (hasAscensionMod) { ApplyAscentionFilter(commonScoredList, priScoredList); } if (numPriLists > 0) { currentPool.priorityLists = new TriadCard[numPriLists][]; if (debugMode) { Logger.WriteLine(">> num priority lists:{0}", numPriLists); } int idxP = 0; for (int idxL = 0; idxL < priScoredList.Count; idxL++) { int maxPriorityToUse = Math.Min(numPriorityToBuild, priScoredList[idxL].Count); if (maxPriorityToUse > 0) { currentPool.priorityLists[idxP] = new TriadCard[maxPriorityToUse]; priScoredList[idxL].Sort(); for (int idxC = 0; idxC < maxPriorityToUse; idxC++) { currentPool.priorityLists[idxP][idxC] = priScoredList[idxL][idxC].card; } idxP++; } } } // adjust pool of common cards based on avail common slots // - all common: use requested size // - scale down 20% per every priority list slot int numPriSlots = 0; for (int idx = 0; idx < currentPool.deckSlotTypes.Length; idx++) { numPriSlots += (currentPool.deckSlotTypes[idx] >= 0) ? 1 : 0; } int maxCommonToUse = Math.Min(numCommonToBuild - (numCommonToBuild * numPriSlots * numCommonPctToDropPerPriSlot / 100), commonScoredList.Count); if (debugMode) { Logger.WriteLine(">> adjusting common pool based on priSlots:{0} and drop:{1}% => {2}", numPriSlots, numCommonPctToDropPerPriSlot, maxCommonToUse); } currentPool.commonList = new TriadCard[maxCommonToUse]; commonScoredList.Sort(); for (int idx = 0; idx < currentPool.commonList.Length; idx++) { currentPool.commonList[idx] = commonScoredList[idx].card; } } if (debugMode) { Logger.WriteLine(">> deck slot types:[{0}, {1}, {2}, {3}, {4}]", currentPool.deckSlotTypes[0], currentPool.deckSlotTypes[1], currentPool.deckSlotTypes[2], currentPool.deckSlotTypes[3], currentPool.deckSlotTypes[4]); } return isPoolValid; } private class SlotIterator { public const int numSlots = 5; public TriadCard[][] slotLists = new TriadCard[numSlots][]; private bool[] isSlotCommon = new bool[numSlots]; public struct ItemInfo { public int Idx0; public int Idx1; public int Idx2; public int Idx3; public int Idx4; public TriadCard[] Cards; public ItemInfo(int idx0, int idx1, int idx2, int idx3, int idx4, SlotIterator iterator) { Idx0 = idx0; Idx1 = idx1; Idx2 = idx2; Idx3 = idx3; Idx4 = idx4; Cards = new TriadCard[5] { iterator.slotLists[0][Idx0], iterator.slotLists[1][Idx1], iterator.slotLists[2][Idx2], iterator.slotLists[3][Idx3], iterator.slotLists[4][Idx4] }; } public bool IsValid() { return (Cards[0] != Cards[1]) && (Cards[0] != Cards[2]) && (Cards[0] != Cards[3]) && (Cards[0] != Cards[4]) && (Cards[1] != Cards[2]) && (Cards[1] != Cards[3]) && (Cards[1] != Cards[4]) && (Cards[2] != Cards[3]) && (Cards[2] != Cards[4]) && (Cards[3] != Cards[4]); } } public SlotIterator(CardPool cardPool, List lockedCards) { for (int idx = 0; idx < numSlots; idx++) { slotLists[idx] = (cardPool.deckSlotTypes[idx] == DeckSlotCommon) ? cardPool.commonList : (cardPool.deckSlotTypes[idx] >= 0) ? cardPool.priorityLists[cardPool.deckSlotTypes[idx]] : new TriadCard[1] { lockedCards[idx] }; isSlotCommon[idx] = (cardPool.deckSlotTypes[idx] == DeckSlotCommon); } } private int FindLoopStart(int SlotIdx, int IdxS0, int IdxS1, int IdxS2, int IdxS3) { if (!isSlotCommon[SlotIdx]) { return 0; } if (SlotIdx >= 4 && isSlotCommon[3]) { return IdxS3 + 1; } if (SlotIdx >= 3 && isSlotCommon[2]) { return IdxS2 + 1; } if (SlotIdx >= 2 && isSlotCommon[1]) { return IdxS1 + 1; } if (SlotIdx >= 1 && isSlotCommon[0]) { return IdxS0 + 1; } return 0; } public IEnumerable GetDecks(long skipIdx) { long skipCounter = skipIdx; for (int IdxS0 = 0; IdxS0 < slotLists[0].Length; IdxS0++) { int startS1 = FindLoopStart(1, IdxS0, -1, -1, -1); for (int IdxS1 = startS1; IdxS1 < slotLists[1].Length; IdxS1++) { int startS2 = FindLoopStart(2, IdxS0, IdxS1, -1, -1); for (int IdxS2 = startS2; IdxS2 < slotLists[2].Length; IdxS2++) { int startS3 = FindLoopStart(3, IdxS0, IdxS1, IdxS2, -1); for (int IdxS3 = startS3; IdxS3 < slotLists[3].Length; IdxS3++) { int startS4 = FindLoopStart(4, IdxS0, IdxS1, IdxS2, IdxS3); for (int IdxS4 = startS4; IdxS4 < slotLists[4].Length; IdxS4++) { // i'm lazy. if (skipCounter > 0) { skipCounter--; continue; } yield return new ItemInfo(IdxS0, IdxS1, IdxS2, IdxS3, IdxS4, this); } } } } } } } private void FindDecksScored(TriadGameModifier[] regionMods, List lockedCards) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); if (currentPool.commonList == null && currentPool.priorityLists == null) { stopwatch.Stop(); Logger.WriteLine("Skip deck building, everything was locked"); optimizedDeck = new TriadDeck(lockedCards); return; } object lockOb = new object(); int bestScore = 0; TriadDeck bestDeck = new TriadDeck(PlayerSettingsDB.Get().starterCards); // no more flexible slot count after this point => loop land SlotIterator slotIterator = new SlotIterator(currentPool, lockedCards); long lowestPauseIdx = 0; bool canFinishLoop = false; ParallelOptions options = new ParallelOptions(); if (parallelLoadPct > 0.0f) { options.MaxDegreeOfParallelism = Math.Max(1, (int)(Environment.ProcessorCount * parallelLoadPct)); Logger.WriteLine("MaxDegreeOfParallelism: {0}", options.MaxDegreeOfParallelism); } do { var loopResult = Parallel.ForEach(slotIterator.GetDecks(lowestPauseIdx), options, (deckInfo, state) => { if (isPaused) { state.Break(); } else if (bAbort) { state.Stop(); } if (deckInfo.IsValid()) { int randomSeed = GetRandomSeed(deckInfo.Idx0, deckInfo.Idx1, deckInfo.Idx2, deckInfo.Idx3, deckInfo.Idx4); // TODO: custom permutation lookup { TriadDeck testDeck = new TriadDeck(deckInfo.Cards); int testScore = GetDeckScore(currentSolver, testDeck, randomSeed, 1); if (testScore > bestScore) { lock (lockOb) { bestScore = testScore; bestDeck = testDeck; // score: num games * (2 if win, 1 if draw, 0 if lose) // max score = 100% win = num games * 2 float estWinChance = 1.0f * testScore / (numGamesToPlay * 2); OnFoundDeck.Invoke(testDeck, estWinChance); } } } Interlocked.Increment(ref numTestedDecks); } }); if (isPaused) { loopPauseEvent.WaitOne(); lowestPauseIdx += loopResult.LowestBreakIteration ?? 0; Interlocked.Exchange(ref numTestedDecks, Math.Min(lowestPauseIdx, numPossibleDecks)); } canFinishLoop = bAbort || loopResult.IsCompleted || loopResult.LowestBreakIteration == null; } while (!canFinishLoop); stopwatch.Stop(); Logger.WriteLine("Building list of decks: " + stopwatch.ElapsedMilliseconds + "ms, num:" + numPossibleDecks); optimizedDeck = bestDeck; } public int GetProgress() { if (numPossibleDecks > 0) { string desc = (100 * Interlocked.Read(ref numTestedDecks) / numPossibleDecks).ToString(); int progressPct = int.Parse(desc); return Math.Max(0, Math.Min(100, progressPct)); } return 0; } public string GetNumTestedDesc() { return Interlocked.Read(ref numTestedDecks).ToString("N0", CultureInfo.InvariantCulture); } public string GetNumPossibleDecksDesc() { return numPossibleDecks.ToString("N0", CultureInfo.InvariantCulture); } public int GetSecondsRemaining(int ElapsedMs) { int numSeconds = int.MaxValue; numMsElapsed += ElapsedMs; long numTestedDecksSafe = Interlocked.Read(ref numTestedDecks); long numTestedPerMs = numTestedDecksSafe / numMsElapsed; long numMsPerTest = (numTestedDecksSafe == 0) ? 1 : (numMsElapsed / numTestedDecksSafe); long numTestsRemaning = numPossibleDecks - numTestedDecksSafe; long numSecRemaning = (numTestedPerMs > 0) ? ((numTestsRemaning / numTestedPerMs) / 1000) : ((numTestsRemaning * numMsPerTest) / 1000); string numIntervalsDesc = numSecRemaning.ToString(); int.TryParse(numIntervalsDesc, out numSeconds); return Math.Max(0, numSeconds); } public void SetPaused(bool wantsPaused) { if (wantsPaused && !isPaused) { loopPauseEvent.Reset(); isPaused = true; } else if (!wantsPaused && isPaused) { isPaused = false; loopPauseEvent.Set(); } } private const float optimizerScoreAvgSides = 1.0f; private const float optimizerScoreMaxSides = 0.75f; private const float optimizerScoreRarity = 0.2f; private const float optimizerMaxScore = optimizerScoreAvgSides + optimizerScoreMaxSides + optimizerScoreRarity; public static float GetCardScore(TriadCard card) { // try to guess how good card will perform // - avg of sides // - max of sides // - rarity (should be reflected by sides already) int numberMax = Math.Max(Math.Max(card.Sides[0], card.Sides[1]), Math.Max(card.Sides[2], card.Sides[3])); int numberSum = card.Sides[0] + card.Sides[1] + card.Sides[2] + card.Sides[3]; float numberAvg = numberSum / 4.0f; float cardScore = ((numberAvg / 10.0f) * optimizerScoreAvgSides) + ((numberMax / 10.0f) * optimizerScoreMaxSides) + (((int)card.Rarity / (float)ETriadCardRarity.Legendary) * optimizerScoreRarity); return cardScore / optimizerMaxScore; } } } ================================================ FILE: sources/gamelogic/TriadGameAgent.cs ================================================ using MgAl2O4.Utils; using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace FFTriadBuddy { public abstract class TriadGameAgent { [Flags] public enum DebugFlags { None = 0, AgentInitialize = 0x1, ShowMoveResult = 0x2, ShowMoveStart = 0x4, ShowMoveDetails = 0x8, ShowMoveDetailsRng = 0x10, } public DebugFlags debugFlags; public string agentName = "??"; public virtual void Initialize(TriadGameSolver solver, int sessionSeed) { } public virtual bool IsInitialized() { return true; } public virtual float GetProgress() { return 0.0f; } public virtual void OnSimulationStart() { } public abstract bool FindNextMove(TriadGameSolver solver, TriadGameSimulationState gameState, out int cardIdx, out int boardPos, out SolverResult solverResult); } /// /// Random pick from all possible actions /// public class TriadGameAgentRandom : TriadGameAgent { public static bool UseEqualDistribution = false; private Random randGen; public TriadGameAgentRandom() { } public TriadGameAgentRandom(TriadGameSolver solver, int sessionSeed) { Initialize(solver, sessionSeed); } public override void Initialize(TriadGameSolver solver, int sessionSeed) { randGen = new Random(sessionSeed); agentName = "Random"; } public override bool IsInitialized() { return randGen != null; } public override bool FindNextMove(TriadGameSolver solver, TriadGameSimulationState gameState, out int cardIdx, out int boardPos, out SolverResult solverResult) { #if DEBUG if ((debugFlags & DebugFlags.ShowMoveStart) != DebugFlags.None) { Logger.WriteLine($"FindNextMove, numPlaced:{gameState.numCardsPlaced}"); } #endif // DEBUG cardIdx = -1; boardPos = -1; solverResult = SolverResult.Zero; if (!IsInitialized()) { return false; } if (UseEqualDistribution) { // proper solution, but ends up lowering initial win chance by A LOT solver.FindAvailableActions(gameState, out int availBoardMask, out int numAvailBoard, out int availCardsMask, out int numAvailCards); if (numAvailCards > 0 && numAvailBoard > 0) { cardIdx = PickBitmaskIndex(availCardsMask, numAvailCards); boardPos = PickBitmaskIndex(availBoardMask, numAvailBoard); } } else { // OLD IMPLEMENTATION for comparison // doesn't guarantee equal distribution = opponent simulation is biased => reported win chance is too high // stays for now until i can make CarloScored usable // const int boardPosMax = TriadGameSimulationState.boardSizeSq; if (gameState.numCardsPlaced < TriadGameSimulationState.boardSizeSq) { int testPos = randGen.Next(boardPosMax); for (int passIdx = 0; passIdx < boardPosMax; passIdx++) { testPos = (testPos + 1) % boardPosMax; if (gameState.board[testPos] == null) { boardPos = testPos; break; } } } cardIdx = -1; TriadDeckInstance useDeck = (gameState.state == ETriadGameState.InProgressBlue) ? gameState.deckBlue : gameState.deckRed; if (useDeck.availableCardMask > 0) { int testIdx = randGen.Next(TriadDeckInstance.maxAvailableCards); for (int passIdx = 0; passIdx < TriadDeckInstance.maxAvailableCards; passIdx++) { testIdx = (testIdx + 1) % TriadDeckInstance.maxAvailableCards; if ((useDeck.availableCardMask & (1 << testIdx)) != 0) { cardIdx = testIdx; break; } } } } return (boardPos >= 0) && (cardIdx >= 0); } protected int PickBitmaskIndex(int mask, int numSet) { int stepIdx = randGen.Next(numSet); return PickRandomBitFromMask(mask, stepIdx); } public static int PickRandomBitFromMask(int mask, int randStep) { int bitIdx = 0; int testMask = 1 << bitIdx; while (testMask <= mask) { if ((testMask & mask) != 0) { randStep--; if (randStep < 0) { return bitIdx; } } bitIdx++; testMask <<= 1; } #if DEBUG // more bits set than mask allows? Debugger.Break(); #endif return -1; } } /// /// Base class for agents recursively exploring action graph /// public abstract class TriadGameAgentGraphExplorer : TriadGameAgent { protected float currentProgress = 0; protected int sessionSeed = 0; private Random failsafeRandStream = null; public override float GetProgress() { return currentProgress; } public override void Initialize(TriadGameSolver solver, int sessionSeed) { this.sessionSeed = sessionSeed; } public override bool FindNextMove(TriadGameSolver solver, TriadGameSimulationState gameState, out int cardIdx, out int boardPos, out SolverResult solverResult) { #if DEBUG if ((debugFlags & DebugFlags.ShowMoveStart) != DebugFlags.None) { Logger.WriteLine($"FindNextMove, numPlaced:{gameState.numCardsPlaced}"); } #endif // DEBUG cardIdx = -1; boardPos = -1; bool isFinished = IsFinished(gameState, out solverResult); if (!isFinished && IsInitialized()) { _ = SearchActionSpace(solver, gameState, 0, out cardIdx, out boardPos, out solverResult); } return (cardIdx >= 0) && (boardPos >= 0); } protected bool IsFinished(TriadGameSimulationState gameState, out SolverResult gameResult) { // end game conditions, owner always fixed as blue switch (gameState.state) { case ETriadGameState.BlueWins: gameResult = new SolverResult(1, 0, 1); return true; case ETriadGameState.BlueDraw: gameResult = new SolverResult(0, 1, 1); return true; case ETriadGameState.BlueLost: gameResult = new SolverResult(0, 0, 1); return true; default: break; } gameResult = SolverResult.Zero; return false; } protected virtual SolverResult SearchActionSpace(TriadGameSolver solver, TriadGameSimulationState gameState, int searchLevel, out int bestCardIdx, out int bestBoardPos, out SolverResult bestActionResult) { // don't check finish condition at start! // this is done before caling this function (from FindNextMove / recursive), so it doesn't have to be duplicated in every derrived class bestCardIdx = -1; bestBoardPos = -1; bestActionResult = SolverResult.Zero; // game in progress, explore actions bool isRootLevel = searchLevel == 0; if (isRootLevel) { currentProgress = 0.0f; } float numWinsTotal = 0; float numDrawsTotal = 0; long numGamesTotal = 0; solver.FindAvailableActions(gameState, out int availBoardMask, out int numAvailBoard, out int availCardsMask, out int numAvailCards); if (numAvailCards > 0 && numAvailBoard > 0) { var turnOwner = (gameState.state == ETriadGameState.InProgressBlue) ? ETriadCardOwner.Blue : ETriadCardOwner.Red; int cardProgressCounter = 0; bool hasValidPlacements = false; for (int cardIdx = 0; cardIdx < TriadDeckInstance.maxAvailableCards; cardIdx++) { bool cardNotAvailable = (availCardsMask & (1 << cardIdx)) == 0; if (cardNotAvailable) { continue; } if (isRootLevel) { currentProgress = 1.0f * cardProgressCounter / numAvailCards; cardProgressCounter++; } for (int boardIdx = 0; boardIdx < gameState.board.Length; boardIdx++) { bool boardNotAvailable = (availBoardMask & (1 << boardIdx)) == 0; if (boardNotAvailable) { continue; } var gameStateCopy = new TriadGameSimulationState(gameState); var useDeck = (gameStateCopy.state == ETriadGameState.InProgressBlue) ? gameStateCopy.deckBlue : gameStateCopy.deckRed; bool isPlaced = solver.simulation.PlaceCard(gameStateCopy, cardIdx, useDeck, turnOwner, boardIdx); if (isPlaced) { // check if finished before going deeper bool isFinished = IsFinished(gameStateCopy, out var branchResult); if (!isFinished) { gameStateCopy.forcedCardIdx = -1; branchResult = SearchActionSpace(solver, gameStateCopy, searchLevel + 1, out _, out _, out _); } #if DEBUG if ((debugFlags & DebugFlags.ShowMoveDetails) != DebugFlags.None && isRootLevel) { Logger.WriteLine($" board[{boardIdx}], card[{cardIdx}] = {branchResult}"); } #endif // DEBUG if (branchResult.IsBetterThan(bestActionResult) || !hasValidPlacements) { bestActionResult = branchResult; bestCardIdx = cardIdx; bestBoardPos = boardIdx; } numWinsTotal += branchResult.numWins; numDrawsTotal += branchResult.numDraws; numGamesTotal += branchResult.numGames; hasValidPlacements = true; } } } if (!hasValidPlacements) { // failsafe in case simulation runs into any issues if (failsafeRandStream == null) { failsafeRandStream = new Random(sessionSeed); } bestCardIdx = TriadGameAgentRandom.PickRandomBitFromMask(availCardsMask, failsafeRandStream.Next(numAvailCards)); bestBoardPos = TriadGameAgentRandom.PickRandomBitFromMask(availBoardMask, failsafeRandStream.Next(numAvailBoard)); } #if DEBUG if ((debugFlags & DebugFlags.ShowMoveResult) != DebugFlags.None && isRootLevel) { string namePrefix = string.IsNullOrEmpty(solver.name) ? "" : ("[" + solver.name + "] "); Logger.WriteLine("{0}Solver {11}win:{1:P2} (draw:{2:P2}), blue[{3}], red[{4}], turn:{5}, availBoard:{6} ({7:x}), availCards:{8} ({9}:{10:x})", namePrefix, bestActionResult.winChance, bestActionResult.drawChance, gameState.deckBlue, gameState.deckRed, turnOwner, numAvailBoard, availBoardMask, numAvailCards, gameState.state == ETriadGameState.InProgressBlue ? "B" : "R", availCardsMask, hasValidPlacements ? "[FAILSAFE] " : ""); } #endif // DEBUG } else { #if DEBUG if ((debugFlags & DebugFlags.ShowMoveResult) != DebugFlags.None && isRootLevel) { string namePrefix = string.IsNullOrEmpty(solver.name) ? "" : ("[" + solver.name + "] "); Logger.WriteLine("{0}Can't find move! availBoard:{1} ({2:x}), availCards:{3} ({4}:{5:x})", namePrefix, numAvailBoard, availBoardMask, numAvailCards, gameState.state == ETriadGameState.InProgressBlue ? "B" : "R", availCardsMask); } #endif // DEBUG } // what to do with results depend on current move owner: // Agent's player (search levels: 0, 2, 4, ...) // - result of processing this level is MAX(result branch) // // opponent player (search levels: 1, 3, ...) // - min/max says MIN, but let's go with AVG instead to be more optimistic // - result of processing this level is AVG, use total counters to create chance data bool isOwnerTurn = (searchLevel % 2) == 0; return isOwnerTurn ? bestActionResult : new SolverResult(numWinsTotal, numDrawsTotal, numGamesTotal); } } /// /// Single level MCTS, each available action spins 2000 random games and best one is selected /// public class TriadGameAgentDerpyCarlo : TriadGameAgentGraphExplorer { protected int numWorkers = 2000; protected TriadGameAgentRandom[] workerAgents; public override void Initialize(TriadGameSolver solver, int sessionSeed) { base.Initialize(solver, sessionSeed); agentName = "DerpyCarlo"; // initialize all random streams just once, it's enough for seeing and having unique stream for each worker workerAgents = new TriadGameAgentRandom[numWorkers]; for (int idx = 0; idx < numWorkers; idx++) { workerAgents[idx] = new TriadGameAgentRandom(solver, sessionSeed + idx); } } public override bool IsInitialized() { return workerAgents != null; } protected override SolverResult SearchActionSpace(TriadGameSolver solver, TriadGameSimulationState gameState, int searchLevel, out int bestCardIdx, out int bestBoardPos, out SolverResult bestActionResult) { bool runWorkers = CanRunRandomExploration(solver, gameState, searchLevel); if (runWorkers) { bestCardIdx = -1; bestBoardPos = -1; bestActionResult = FindWinningProbability(solver, gameState); #if DEBUG if ((debugFlags & DebugFlags.ShowMoveDetailsRng) != DebugFlags.None) { Logger.WriteLine($"level:{searchLevel}, numPlaced:{gameState.numCardsPlaced} => random workers:{bestActionResult}"); } #endif // DEBUG return bestActionResult; } var result = base.SearchActionSpace(solver, gameState, searchLevel, out bestCardIdx, out bestBoardPos, out bestActionResult); #if DEBUG if ((debugFlags & DebugFlags.ShowMoveDetails) != DebugFlags.None && searchLevel == 0) { Logger.WriteLine($"level:{searchLevel}, numPlaced:{gameState.numCardsPlaced} => result:{bestActionResult}"); } #endif // DEBUG return result; } protected virtual bool CanRunRandomExploration(TriadGameSolver solver, TriadGameSimulationState gameState, int searchLevel) { return searchLevel > 0; } protected virtual SolverResult FindWinningProbability(TriadGameSolver solver, TriadGameSimulationState gameState) { int numWinningWorkers = 0; int numDrawingWorkers = 0; _ = Parallel.For(0, numWorkers, workerIdx => //for (int workerIdx = 0; workerIdx < solverWorkers; workerIdx++) { var gameStateCopy = new TriadGameSimulationState(gameState); var agent = workerAgents[workerIdx]; solver.RunSimulation(gameStateCopy, agent, agent); if (gameStateCopy.state == ETriadGameState.BlueWins) { _ = Interlocked.Add(ref numWinningWorkers, 1); } else if (gameStateCopy.state == ETriadGameState.BlueDraw) { _ = Interlocked.Add(ref numDrawingWorkers, 1); } }); // return normalized score so it can be compared return new SolverResult(1.0f * numWinningWorkers / numWorkers, 1.0f * numDrawingWorkers / numWorkers, 1); } } /// /// Switches between derpy MCTS and full exploration depending on size of game space /// public class TriadGameAgentCarloTheExplorer : TriadGameAgentDerpyCarlo { // 10k seems to be sweet spot // - 1k: similar time, lower accuracy // - 100k: 8x longer, similar accuracy public const long MaxStatesToExplore = 10 * 1000; private int minPlacedToExplore = 10; private int minPlacedToExploreWithForced = 10; public override void Initialize(TriadGameSolver solver, int sessionSeed) { base.Initialize(solver, sessionSeed); agentName = "CarloTheExplorer"; // cache number of possible states depending on cards placed // 0: (5 * 9) * (5 * 8) * (4 * 7) * (4 * 6) * ... = (5 * 5 * 4 * 4 * 3 * 3 * 2 * 2 * 1) * 9! = (5! * 5!) * 9! // 1: (5 * 8) * (4 * 7) * (4 * 6) * ... = (5 * 4 * 4 * 3 * 3 * 2 * 2 * 1) * 8! = (4! * 5!) * 8! // ... // 6: (2 * 3) * (1 * 2) * (1 * 1) // 7: (1 * 2) * (1 * 1) // 8: (1 * 1) // 9: 0 // // num states = num board positions * num cards, // - board(num placed) => x == 0 ? 0 : x! // - card(num placed) => forced ? 1 : ((x + 2) / 2)! * ((x + 1) / 2)! long numStatesForced = 1; long numStates = 1; const int maxToPlace = TriadGameSimulationState.boardSizeSq; for (int numToPlace = 1; numToPlace <= maxToPlace; numToPlace++) { int numPlaced = maxToPlace - numToPlace; numStatesForced *= numToPlace; if (numStatesForced <= MaxStatesToExplore) { minPlacedToExploreWithForced = numPlaced; } numStates *= numToPlace * ((numToPlace + 2) / 2) * ((numToPlace + 1) / 2); if (numStates <= MaxStatesToExplore) { minPlacedToExplore = numPlaced; } } #if DEBUG if ((debugFlags & DebugFlags.AgentInitialize) != DebugFlags.None) { Logger.WriteLine($"{agentName}: minPlacedToExplore:{minPlacedToExplore}, minPlacedToExploreWithForced:{minPlacedToExploreWithForced}"); } #endif // DEBUG } protected override bool CanRunRandomExploration(TriadGameSolver solver, TriadGameSimulationState gameState, int searchLevel) { int numPlacedThr = (gameState.forcedCardIdx < 0) ? minPlacedToExplore : minPlacedToExploreWithForced; return (searchLevel > 0) && (gameState.numCardsPlaced < numPlacedThr); } } /// /// Aguments random search phase with score of game state to increase diffs between probability of initial steps /// public class TriadGameAgentCarloScored : TriadGameAgentCarloTheExplorer { public const float StateWeight = 0.75f; public const float StateWeightDecay = 0.25f; public const float PriorityDefense = 1.0f; public const float PriorityDeck = 2.0f; public const float PriorityCapture = 3.5f; public override void Initialize(TriadGameSolver solver, int sessionSeed) { base.Initialize(solver, sessionSeed); agentName = "CarloScored"; } protected override SolverResult FindWinningProbability(TriadGameSolver solver, TriadGameSimulationState gameState) { var result = base.FindWinningProbability(solver, gameState); var stateScore = CalculateStateScore(solver, gameState); var useWeight = Math.Max(0.0f, StateWeight - ((gameState.deckBlue.numPlaced - 1) * StateWeightDecay)); var numWinsModified = ((result.numWins / result.numGames) * (1.0f - useWeight)) + (stateScore * useWeight); return new SolverResult(Math.Min(1.0f, numWinsModified), result.numDraws / result.numGames, 1); } public float CalculateStateScore(TriadGameSolver solver, TriadGameSimulationState gameState) { var (blueDefenseScore, blueCaptureScore) = CalculateBoardScore(solver, gameState); var deckScore = CalculateBlueDeckScore(solver, gameState); #if DEBUG if ((debugFlags & DebugFlags.ShowMoveDetailsRng) != DebugFlags.None) { Logger.WriteLine($"stateScore => def:{blueDefenseScore}, capture:{blueCaptureScore}, deck:{deckScore}"); } #endif // DEBUG return ((blueDefenseScore * PriorityDefense) + (blueCaptureScore * PriorityCapture) + (deckScore * PriorityDeck)) / (PriorityDefense + PriorityDeck + PriorityCapture); } private (float, float) CalculateBoardScore(TriadGameSolver solver, TriadGameSimulationState gameState) { // for each blue card: // for each side: // find all numbers that can capture it // normalize count of capturing numbers // normalize card capturing value // inverse => blue cards defensive value // // pct of blue in all cards => capture score float capturingSum = 0.0f; int numBlueCards = 0; for (int idx = 0; idx < gameState.board.Length; idx++) { var cardInst = gameState.board[idx]; if (cardInst == null) { continue; } if (cardInst.owner == ETriadCardOwner.Blue) { int[] neis = TriadGameSimulation.cachedNeis[idx]; int numCapturingValues = 0; int numValidSides = 0; for (int side = 0; side < 4; side++) { if ((neis[side] >= 0) && (gameState.board[neis[side]] == null)) { int cardNumber = cardInst.GetNumber((ETriadGameSide)side); int numCaptures = 0; for (int testValue = 1; testValue <= 10; testValue++) { bool canCapture = CanBeCapturedWith(solver.simulation, cardNumber, testValue); numCaptures += canCapture ? 1 : 0; } //Logger.WriteLine($"[{idx}].side:{side} card:{cardNumber} <- captures:{numCaptures}"); numValidSides++; numCapturingValues += numCaptures; } } capturingSum += (numValidSides > 0) ? (numCapturingValues / (numValidSides * 10.0f)) : 0.0f; numBlueCards++; } } float defenseScore = (numBlueCards > 0) ? (1.0f - (capturingSum / numBlueCards)) : 0.0f; float captureScore = Math.Min(1.0f, numBlueCards / 5.0f); return (defenseScore, captureScore); } private float CalculateBlueDeckScore(TriadGameSolver solver, TriadGameSimulationState gameState) { float blueCardScore = 0.0f; int numScoredBlueCards = 0; for (int idx = 0; idx < TriadDeckInstance.maxAvailableCards; idx++) { if ((gameState.deckBlue.availableCardMask & (1 << idx)) != 0) { var testCard = gameState.deckBlue.GetCard(idx); float cardScore = testCard.OptimizerScore; foreach (TriadGameModifier mod in solver.simulation.modifiers) { mod.OnScoreCard(testCard, ref cardScore); } blueCardScore += cardScore; numScoredBlueCards++; } } return (numScoredBlueCards > 0) ? (blueCardScore / numScoredBlueCards) : 0.0f; } private bool CanBeCapturedWith(TriadGameSimulation simulation, int defendingNum, int capturingNum) { if ((simulation.modFeatures & TriadGameModifier.EFeature.CaptureWeights) != 0) { bool isReverseActive = (simulation.modFeatures & TriadGameModifier.EFeature.CaptureMath) != 0; foreach (TriadGameModifier mod in simulation.modifiers) { mod.OnCheckCaptureCardWeights(null, -1, -1, isReverseActive, ref capturingNum, ref defendingNum); } } bool isCaptured = (capturingNum > defendingNum); if ((simulation.modFeatures & TriadGameModifier.EFeature.CaptureMath) != 0) { foreach (TriadGameModifier mod in simulation.modifiers) { mod.OnCheckCaptureCardMath(null, -1, -1, capturingNum, defendingNum, ref isCaptured); } } return isCaptured; } } } ================================================ FILE: sources/gamelogic/TriadGameModifier.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Reflection; namespace FFTriadBuddy { [Flags] public enum ETriadGameSpecialMod { None = 0, SelectVisible3 = 0x1, SelectVisible5 = 0x2, RandomizeRule = 0x4, RandomizeBlueDeck = 0x8, SwapCards = 0x10, BlueCardSelection = 0x20, IgnoreOwnedCheck = 0x40, } public class TriadGameModifier : IComparable { [Flags] public enum EFeature { None = 0, CardPlaced = 1, CaptureNei = 2, CaptureWeights = 4, CaptureMath = 8, PostCapture = 16, AllPlaced = 32, FilterNext = 64, } protected string RuleName; protected LocString LocRuleName; protected bool bAllowCombo = false; protected bool bIsDeckOrderImportant = false; protected bool bHasLastRedReminder = false; protected ETriadGameSpecialMod SpecialMod = ETriadGameSpecialMod.None; protected EFeature Features = EFeature.None; public virtual string GetCodeName() { return RuleName; } public virtual string GetLocalizedName() { return LocRuleName.GetLocalized(); } public int GetLocalizationId() { return LocRuleName.Id; } public virtual bool AllowsCombo() { return bAllowCombo; } public virtual bool IsDeckOrderImportant() { return bIsDeckOrderImportant; } public virtual ETriadGameSpecialMod GetSpecialRules() { return SpecialMod; } public virtual EFeature GetFeatures() { return Features; } public virtual bool HasLastRedReminder() { return bHasLastRedReminder; } public override string ToString() { return GetCodeName(); } public virtual void OnCardPlaced(TriadGameSimulationState gameData, int boardPos) { } public virtual void OnCheckCaptureNeis(TriadGameSimulationState gameData, int boardPos, int[] neiPos, List captureList) { } public virtual void OnCheckCaptureCardWeights(TriadGameSimulationState gameData, int boardPos, int neiPos, bool isReverseActive, ref int cardNum, ref int neiNum) { } public virtual void OnCheckCaptureCardMath(TriadGameSimulationState gameData, int boardPos, int neiPos, int cardNum, int neiNum, ref bool isCaptured) { } public virtual void OnPostCaptures(TriadGameSimulationState gameData, int boardPos) { } public virtual void OnScreenUpdate(TriadGameSimulationState gameData) { } public virtual void OnAllCardsPlaced(TriadGameSimulationState gameData) { } public virtual void OnFilterNextCards(TriadGameSimulationState gameData, ref int allowedCardsMask) { } public virtual void OnMatchInit() { } public virtual void OnScoreCard(TriadCard card, ref float score) { } public int CompareTo(TriadGameModifier otherMod) { if (otherMod != null) { string locStrA = GetLocalizedName(); string locStrB = otherMod.GetLocalizedName(); return locStrA.CompareTo(locStrB); } return 0; } public int CompareTo(object obj) { return CompareTo((TriadGameModifier)obj); } public virtual TriadGameModifier Clone() { return (TriadGameModifier)this.MemberwiseClone(); } public override bool Equals(object obj) { var otherMod = obj as TriadGameModifier; return (otherMod != null) && (GetLocalizationId() == otherMod.GetLocalizationId()); } public override int GetHashCode() { return GetLocalizationId(); } } public class TriadGameModifierNone : TriadGameModifier { public TriadGameModifierNone() { RuleName = "None"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 0); } // no special logic } public class TriadGameModifierRoulette : TriadGameModifier { protected TriadGameModifier RuleInst; public TriadGameModifierRoulette() { RuleName = "Roulette"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 1); SpecialMod = ETriadGameSpecialMod.RandomizeRule; } public override string GetCodeName() { return base.GetCodeName() + (RuleInst != null ? (" (" + RuleInst.GetCodeName() + ")") : ""); } public override string GetLocalizedName() { return base.GetLocalizedName() + (RuleInst != null ? (" (" + RuleInst.GetLocalizedName() + ")") : ""); } public override bool AllowsCombo() { return (RuleInst != null) ? RuleInst.AllowsCombo() : base.AllowsCombo(); } public override bool IsDeckOrderImportant() { return (RuleInst != null) ? RuleInst.IsDeckOrderImportant() : base.IsDeckOrderImportant(); } public override ETriadGameSpecialMod GetSpecialRules() { return base.GetSpecialRules() | ((RuleInst != null) ? RuleInst.GetSpecialRules() : ETriadGameSpecialMod.None); } public override EFeature GetFeatures() { return (RuleInst != null) ? RuleInst.GetFeatures() : EFeature.None; } public override bool HasLastRedReminder() { return (RuleInst != null) ? RuleInst.HasLastRedReminder() : base.HasLastRedReminder(); } public override void OnCardPlaced(TriadGameSimulationState gameData, int boardPos) { if (RuleInst != null) { RuleInst.OnCardPlaced(gameData, boardPos); } } public override void OnCheckCaptureNeis(TriadGameSimulationState gameData, int boardPos, int[] neiPos, List captureList) { if (RuleInst != null) { RuleInst.OnCheckCaptureNeis(gameData, boardPos, neiPos, captureList); } } public override void OnCheckCaptureCardWeights(TriadGameSimulationState gameData, int boardPos, int neiPos, bool isReverseActive, ref int cardNum, ref int neiNum) { if (RuleInst != null) { RuleInst.OnCheckCaptureCardWeights(gameData, boardPos, neiPos, isReverseActive, ref cardNum, ref neiNum); } } public override void OnCheckCaptureCardMath(TriadGameSimulationState gameData, int boardPos, int neiPos, int cardNum, int neiNum, ref bool isCaptured) { if (RuleInst != null) { RuleInst.OnCheckCaptureCardMath(gameData, boardPos, neiPos, cardNum, neiNum, ref isCaptured); } } public override void OnPostCaptures(TriadGameSimulationState gameData, int boardPos) { if (RuleInst != null) { RuleInst.OnPostCaptures(gameData, boardPos); } } public override void OnAllCardsPlaced(TriadGameSimulationState gameData) { if (RuleInst != null) { RuleInst.OnAllCardsPlaced(gameData); } } public override void OnFilterNextCards(TriadGameSimulationState gameData, ref int allowedCardsMask) { if (RuleInst != null) { RuleInst.OnFilterNextCards(gameData, ref allowedCardsMask); } } public override void OnMatchInit() { SetRuleInstance(null); } public void SetRuleInstance(TriadGameModifier RuleInstance) { RuleInst = RuleInstance; } } public class TriadGameModifierAllOpen : TriadGameModifier { public TriadGameModifierAllOpen() { RuleName = "All Open"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 2); SpecialMod = ETriadGameSpecialMod.SelectVisible5; } // shared with three open public static void StaticMakeKnown(TriadGameSimulationState gameData, List redIndices) { const int deckSize = 5; TriadDeckInstanceManual deckRedEx = gameData.deckRed as TriadDeckInstanceManual; if (deckRedEx != null && redIndices.Count <= deckSize) { if (gameData.bDebugRules) { Logger.WriteLine(">> Open:{0}! red indices:{1}", redIndices.Count, string.Join(", ", redIndices)); } TriadDeck redDeckVisible = new TriadDeck(deckRedEx.deck.knownCards, deckRedEx.deck.unknownCardPool); for (int idx = 0; idx < redIndices.Count; idx++) { int cardIdx = redIndices[idx]; if (cardIdx < deckRedEx.deck.knownCards.Count) { // already known, ignore } else { int idxU = cardIdx - deckRedEx.deck.knownCards.Count; var cardOb = deckRedEx.deck.unknownCardPool[idxU]; redDeckVisible.knownCards.Add(cardOb); redDeckVisible.unknownCardPool.Remove(cardOb); } } // safety for impossible state for (int idx = 0; (idx < redDeckVisible.knownCards.Count) && (redDeckVisible.knownCards.Count > deckSize); idx++) { var cardOb = redDeckVisible.knownCards[idx]; int orgIdx = deckRedEx.GetCardIndex(cardOb); if (!redIndices.Contains(orgIdx)) { redDeckVisible.knownCards.RemoveAt(idx); idx--; } } gameData.deckRed = new TriadDeckInstanceManual(redDeckVisible); } } } public class TriadGameModifierThreeOpen : TriadGameModifier { public TriadGameModifierThreeOpen() { RuleName = "Three Open"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 3); SpecialMod = ETriadGameSpecialMod.SelectVisible3; } // no special logic } public class TriadGameModifierSuddenDeath : TriadGameModifier { public TriadGameModifierSuddenDeath() { RuleName = "Sudden Death"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 4); bHasLastRedReminder = true; Features = EFeature.AllPlaced; } public override void OnAllCardsPlaced(TriadGameSimulationState gameData) { if (gameData.state == ETriadGameState.BlueDraw && gameData.numRestarts < 3) { // TODO: don't follow this more than once when simulating in solver? // can get stuck in pretty long loops // implement this rule only for manual mode, screen captures get everything automatically TriadDeckInstanceManual deckBlueEx = gameData.deckBlue as TriadDeckInstanceManual; TriadDeckInstanceManual deckRedEx = gameData.deckRed as TriadDeckInstanceManual; if (deckBlueEx != null && deckRedEx != null) { List blueCards = new List(); List redCards = new List(); List redUnknownCards = new List(); string redCardsDebug = ""; for (int Idx = 0; Idx < gameData.board.Length; Idx++) { if (gameData.board[Idx].owner == ETriadCardOwner.Blue) { blueCards.Add(gameData.board[Idx].card); } else { redCards.Add(gameData.board[Idx].card); } gameData.board[Idx] = null; } if (deckBlueEx.numPlaced < deckRedEx.numPlaced) { // blue has cards on hand, all known for (int Idx = 0; Idx < deckBlueEx.deck.knownCards.Count; Idx++) { bool bIsAvailable = !deckBlueEx.IsPlaced(Idx); if (bIsAvailable) { blueCards.Add(deckBlueEx.deck.knownCards[Idx]); break; } } gameData.state = ETriadGameState.InProgressBlue; } else { // red has cards on hand, check known vs unknown for (int Idx = 0; Idx < deckRedEx.deck.knownCards.Count; Idx++) { bool bIsAvailable = !deckRedEx.IsPlaced(Idx); if (bIsAvailable) { redCards.Add(deckRedEx.deck.knownCards[Idx]); redCardsDebug += deckRedEx.deck.knownCards[Idx].Name.GetCodeName() + ":K, "; break; } } if (redCards.Count < blueCards.Count) { for (int Idx = 0; Idx < deckRedEx.deck.unknownCardPool.Count; Idx++) { int cardIdx = Idx + deckRedEx.deck.knownCards.Count; bool bIsAvailable = !deckRedEx.IsPlaced(cardIdx); if (bIsAvailable) { redUnknownCards.Add(deckRedEx.deck.unknownCardPool[Idx]); redCardsDebug += deckRedEx.deck.unknownCardPool[Idx].Name.GetCodeName() + ":U, "; } } } gameData.state = ETriadGameState.InProgressRed; } gameData.deckBlue = new TriadDeckInstanceManual(new TriadDeck(blueCards)); gameData.deckRed = new TriadDeckInstanceManual(new TriadDeck(redCards, redUnknownCards)); gameData.numCardsPlaced = 0; gameData.numRestarts++; for (int Idx = 0; Idx < gameData.typeMods.Length; Idx++) { gameData.typeMods[Idx] = 0; } if (gameData.bDebugRules) { redCardsDebug = (redCardsDebug.Length > 0) ? redCardsDebug.Remove(redCardsDebug.Length - 2, 2) : "(board only)"; ETriadCardOwner nextTurnOwner = (gameData.state == ETriadGameState.InProgressBlue) ? ETriadCardOwner.Blue : ETriadCardOwner.Red; Logger.WriteLine(">> " + RuleName + "! next turn:" + nextTurnOwner + ", red:" + redCardsDebug); } } } } } public class TriadGameModifierReverse : TriadGameModifier { public TriadGameModifierReverse() { RuleName = "Reverse"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 5); Features = EFeature.CaptureMath; } public override void OnCheckCaptureCardMath(TriadGameSimulationState gameData, int boardPos, int neiPos, int cardNum, int neiNum, ref bool isCaptured) { isCaptured = cardNum < neiNum; } public override void OnScoreCard(TriadCard card, ref float score) { const float MaxSum = 40.0f; int numberSum = card.Sides[0] + card.Sides[1] + card.Sides[2] + card.Sides[3]; score = 1.0f - (numberSum / MaxSum); } } public class TriadGameModifierFallenAce : TriadGameModifier { public TriadGameModifierFallenAce() { RuleName = "Fallen Ace"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 6); Features = EFeature.CaptureWeights; } public override void OnCheckCaptureCardWeights(TriadGameSimulationState gameData, int boardPos, int neiPos, bool isReverseActive, ref int cardNum, ref int neiNum) { // note: check if cardNum at [boardPos] can capture neiNum at [neiPos] // cardNum:1 vs neiNum:A => override weights to force capture // cardNum:A vs neiNum:1 => capture, no need to change weights // // due to asymetry, it needs to know about active reverse rule and swap sides if (isReverseActive) { if ((cardNum == 10) && (neiNum == 1)) { cardNum = 0; } } else { if ((cardNum == 1) && (neiNum == 10)) { neiNum = 0; } } } } public class TriadGameModifierSame : TriadGameModifier { public TriadGameModifierSame() { RuleName = "Same"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 7); bAllowCombo = true; Features = EFeature.CaptureNei | EFeature.CardPlaced; } public override void OnCheckCaptureNeis(TriadGameSimulationState gameData, int boardPos, int[] neiPos, List captureList) { TriadCardInstance checkCard = gameData.board[boardPos]; int numSame = 0; int neiCaptureMask = 0; for (int sideIdx = 0; sideIdx < 4; sideIdx++) { int testNeiPos = neiPos[sideIdx]; if (testNeiPos >= 0 && gameData.board[testNeiPos] != null) { TriadCardInstance neiCard = gameData.board[testNeiPos]; int numPos = checkCard.GetNumber((ETriadGameSide)sideIdx); int numOther = neiCard.GetOppositeNumber((ETriadGameSide)sideIdx); if (numPos == numOther) { numSame++; if (neiCard.owner != checkCard.owner) { neiCaptureMask |= (1 << sideIdx); } } } } if (numSame >= 2) { for (int sideIdx = 0; sideIdx < 4; sideIdx++) { int testNeiPos = neiPos[sideIdx]; if ((neiCaptureMask & (1 << sideIdx)) != 0) { TriadCardInstance neiCard = gameData.board[testNeiPos]; neiCard.owner = checkCard.owner; captureList.Add(testNeiPos); if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + testNeiPos + "] " + neiCard.card.Name.GetCodeName() + " => " + neiCard.owner); } } } } } } public class TriadGameModifierPlus : TriadGameModifier { public TriadGameModifierPlus() { RuleName = "Plus"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 8); bAllowCombo = true; Features = EFeature.CaptureNei | EFeature.CardPlaced; } public override void OnCheckCaptureNeis(TriadGameSimulationState gameData, int boardPos, int[] neiPos, List captureList) { TriadCardInstance checkCard = gameData.board[boardPos]; for (int sideIdx = 0; sideIdx < 4; sideIdx++) { int testNeiPos = neiPos[sideIdx]; if (testNeiPos >= 0 && gameData.board[testNeiPos] != null) { TriadCardInstance neiCard = gameData.board[testNeiPos]; if (checkCard.owner != neiCard.owner) { int numPosPattern = checkCard.GetNumber((ETriadGameSide)sideIdx); int numOtherPattern = neiCard.GetOppositeNumber((ETriadGameSide)sideIdx); int sumPattern = numPosPattern + numOtherPattern; bool bIsCaptured = false; for (int vsSideIdx = 0; vsSideIdx < 4; vsSideIdx++) { int vsNeiPos = neiPos[vsSideIdx]; if (vsNeiPos >= 0 && sideIdx != vsSideIdx && gameData.board[vsNeiPos] != null) { TriadCardInstance vsCard = gameData.board[vsNeiPos]; int numPosVs = checkCard.GetNumber((ETriadGameSide)vsSideIdx); int numOtherVs = vsCard.GetOppositeNumber((ETriadGameSide)vsSideIdx); int sumVs = numPosVs + numOtherVs; if (sumPattern == sumVs) { bIsCaptured = true; if (vsCard.owner != checkCard.owner) { vsCard.owner = checkCard.owner; captureList.Add(vsNeiPos); if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + vsNeiPos + "] " + vsCard.card.Name.GetCodeName() + " => " + vsCard.owner); } } } } } if (bIsCaptured) { neiCard.owner = checkCard.owner; captureList.Add(testNeiPos); if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + testNeiPos + "] " + neiCard.card.Name.GetCodeName() + " => " + neiCard.owner); } } } } } } } public class TriadGameModifierAscention : TriadGameModifier { public TriadGameModifierAscention() { RuleName = "Ascension"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 9); Features = EFeature.CardPlaced | EFeature.PostCapture; } public override void OnCardPlaced(TriadGameSimulationState gameData, int boardPos) { TriadCardInstance checkCard = gameData.board[boardPos]; if (checkCard.card.Type != ETriadCardType.None) { int scoreMod = gameData.typeMods[(int)checkCard.card.Type]; if (scoreMod != 0) { checkCard.scoreModifier = scoreMod; if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + boardPos + "] " + checkCard.card.Name.GetCodeName() + " is: " + ((scoreMod > 0) ? "+" : "") + scoreMod); } } } } public override void OnPostCaptures(TriadGameSimulationState gameData, int boardPos) { TriadCardInstance checkCard = gameData.board[boardPos]; if (checkCard.card.Type != ETriadCardType.None) { int scoreMod = checkCard.scoreModifier + 1; gameData.typeMods[(int)checkCard.card.Type] = scoreMod; for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance otherCard = gameData.board[Idx]; if ((otherCard != null) && (checkCard.card.Type == otherCard.card.Type)) { otherCard.scoreModifier = scoreMod; if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + Idx + "] " + otherCard.card.Name.GetCodeName() + " is: " + ((scoreMod > 0) ? "+" : "") + scoreMod); } } } } } public override void OnScreenUpdate(TriadGameSimulationState gameData) { for (int Idx = 0; Idx < gameData.typeMods.Length; Idx++) { gameData.typeMods[Idx] = 0; } for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance checkCard = gameData.board[Idx]; if (checkCard != null && checkCard.card.Type != ETriadCardType.None) { gameData.typeMods[(int)checkCard.card.Type] += 1; } } for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance checkCard = gameData.board[Idx]; if (checkCard != null && checkCard.card.Type != ETriadCardType.None) { checkCard.scoreModifier = gameData.typeMods[(int)checkCard.card.Type]; } } } public override void OnScoreCard(TriadCard card, ref float score) { const float ScoreMult = 0.8f; score *= ScoreMult; bool bHasType = card.Type != ETriadCardType.None; if (bHasType) { score += (1.0f - ScoreMult); } } } public class TriadGameModifierDescention : TriadGameModifier { public TriadGameModifierDescention() { RuleName = "Descension"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 10); Features = EFeature.CardPlaced | EFeature.PostCapture; } public override void OnCardPlaced(TriadGameSimulationState gameData, int boardPos) { TriadCardInstance checkCard = gameData.board[boardPos]; if (checkCard.card.Type != ETriadCardType.None) { int scoreMod = gameData.typeMods[(int)checkCard.card.Type]; if (scoreMod != 0) { checkCard.scoreModifier = scoreMod; if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + boardPos + "] " + checkCard.card.Name.GetCodeName() + " is: " + ((scoreMod > 0) ? "+" : "") + scoreMod); } } } } public override void OnPostCaptures(TriadGameSimulationState gameData, int boardPos) { TriadCardInstance checkCard = gameData.board[boardPos]; if (checkCard.card.Type != ETriadCardType.None) { int scoreMod = checkCard.scoreModifier - 1; gameData.typeMods[(int)checkCard.card.Type] = scoreMod; for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance otherCard = gameData.board[Idx]; if ((otherCard != null) && (checkCard.card.Type == otherCard.card.Type)) { otherCard.scoreModifier = scoreMod; if (gameData.bDebugRules) { Logger.WriteLine(">> " + RuleName + "! [" + Idx + "] " + otherCard.card.Name.GetCodeName() + " is: " + ((scoreMod > 0) ? "+" : "") + scoreMod); } } } } } public override void OnScreenUpdate(TriadGameSimulationState gameData) { for (int Idx = 0; Idx < gameData.typeMods.Length; Idx++) { gameData.typeMods[Idx] = 0; } for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance checkCard = gameData.board[Idx]; if (checkCard != null && checkCard.card.Type != ETriadCardType.None) { gameData.typeMods[(int)checkCard.card.Type] -= 1; } } for (int Idx = 0; Idx < gameData.board.Length; Idx++) { TriadCardInstance checkCard = gameData.board[Idx]; if (checkCard != null && checkCard.card.Type != ETriadCardType.None) { checkCard.scoreModifier = gameData.typeMods[(int)checkCard.card.Type]; } } } public override void OnScoreCard(TriadCard card, ref float score) { const float ScoreMult = 0.5f; score *= ScoreMult; bool bNoType = card.Type == ETriadCardType.None; if (bNoType) { score += (1.0f - ScoreMult); } } } public class TriadGameModifierOrder : TriadGameModifier { public TriadGameModifierOrder() { RuleName = "Order"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 11); bIsDeckOrderImportant = true; Features = EFeature.FilterNext; } public override void OnFilterNextCards(TriadGameSimulationState gameData, ref int allowedCardsMask) { if ((gameData.state == ETriadGameState.InProgressBlue) && (allowedCardsMask != 0)) { int firstBlueIdx = gameData.deckBlue.GetFirstAvailableCardFast(); allowedCardsMask = (firstBlueIdx < 0) ? 0 : (1 << firstBlueIdx); if (gameData.bDebugRules) { TriadCard firstBlueCard = gameData.deckBlue.GetCard(firstBlueIdx); Logger.WriteLine(">> " + RuleName + "! next card: " + (firstBlueCard != null ? firstBlueCard.Name.GetCodeName() : "none")); } } } } public class TriadGameModifierChaos : TriadGameModifier { public TriadGameModifierChaos() { RuleName = "Chaos"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 12); SpecialMod = ETriadGameSpecialMod.BlueCardSelection; } // special logic, covered by GUI } public class TriadGameModifierSwap : TriadGameModifier { public TriadGameModifierSwap() { RuleName = "Swap"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 13); SpecialMod = ETriadGameSpecialMod.SwapCards; } // special logic, covered by GUI public static void StaticSwapCards(TriadGameSimulationState gameData, TriadCard swapFromBlue, int blueSlotIdx, TriadCard swapFromRed, int redSlotIdx) { // implement this rule only for manual mode, screen captures get everything automatically TriadDeckInstanceManual deckBlueEx = gameData.deckBlue as TriadDeckInstanceManual; TriadDeckInstanceManual deckRedEx = gameData.deckRed as TriadDeckInstanceManual; if (deckBlueEx != null && deckRedEx != null) { bool bIsRedFromKnown = redSlotIdx < deckRedEx.deck.knownCards.Count; if (gameData.bDebugRules) { TriadGameModifierSwap DummyOb = new TriadGameModifierSwap(); Logger.WriteLine(">> " + DummyOb.RuleName + "! blue[" + blueSlotIdx + "]:" + swapFromBlue.Name.GetCodeName() + " <-> red[" + redSlotIdx + (bIsRedFromKnown ? "" : ":Opt") + "]:" + swapFromRed.Name.GetCodeName()); } TriadDeck blueDeckSwapped = new TriadDeck(deckBlueEx.deck.knownCards, deckBlueEx.deck.unknownCardPool); TriadDeck redDeckSwapped = new TriadDeck(deckRedEx.deck.knownCards, deckRedEx.deck.unknownCardPool); // ignore order in red deck redDeckSwapped.knownCards.Add(swapFromBlue); redDeckSwapped.knownCards.Remove(swapFromRed); redDeckSwapped.unknownCardPool.Remove(swapFromRed); // preserve order in blue deck blueDeckSwapped.knownCards[blueSlotIdx] = swapFromRed; gameData.deckBlue = new TriadDeckInstanceManual(blueDeckSwapped); gameData.deckRed = new TriadDeckInstanceManual(redDeckSwapped); } } } public class TriadGameModifierRandom : TriadGameModifier { public TriadGameModifierRandom() { RuleName = "Random"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 14); SpecialMod = ETriadGameSpecialMod.RandomizeBlueDeck; } // special logic, covered by GUI public static void StaticRandomized(TriadGameSimulationState gameData) { if (gameData.bDebugRules) { TriadGameModifierRandom DummyOb = new TriadGameModifierRandom(); Logger.WriteLine(">> " + DummyOb.RuleName + "! blue deck:" + gameData.deckBlue); } } } public class TriadGameModifierDraft : TriadGameModifier { public TriadGameModifierDraft() { RuleName = "Draft"; LocRuleName = LocalizationDB.Get().FindOrAddLocString(ELocStringType.RuleName, 15); SpecialMod = ETriadGameSpecialMod.IgnoreOwnedCheck; } // no special logic } public class TriadGameModifierDB { public List mods; private static TriadGameModifierDB instance = new TriadGameModifierDB(); public static TriadGameModifierDB Get() { return instance; } public TriadGameModifierDB() { mods = new List(); foreach (Type type in Assembly.GetAssembly(typeof(TriadGameModifier)).GetTypes()) { if (type.IsSubclassOf(typeof(TriadGameModifier))) { TriadGameModifier modOb = (TriadGameModifier)Activator.CreateInstance(type); mods.Add(modOb); } } mods.Sort((a, b) => (a.GetLocalizationId().CompareTo(b.GetLocalizationId()))); for (int idx = 0; idx < mods.Count; idx++) { if (mods[idx].GetLocalizationId() != idx) { Logger.WriteLine("FAILED to initialize modifiers!"); break; } } } } } ================================================ FILE: sources/gamelogic/TriadGameScreenMemory.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Linq; namespace FFTriadBuddy { public class TriadGameScreenMemory { [Flags] public enum EUpdateFlags { None = 0, Modifiers = 1, Board = 2, RedDeck = 4, BlueDeck = 8, SwapWarning = 16, SwapHints = 32, } public TriadGameSimulationState gameState; public TriadGameSolver gameSolver; public TriadDeckInstanceScreen deckBlue; public TriadDeckInstanceScreen deckRed; private List blueDeckHistory; private TriadCard[] playerDeckPattern; private TriadNpc lastScanNpc; private bool bHasSwapRule; private bool bHasRestartRule; private bool bHasOpenRule; public int swappedBlueCardIdx; public bool logScan; public TriadGameScreenMemory() { gameSolver = new TriadGameSolver(); gameState = new TriadGameSimulationState(); deckBlue = new TriadDeckInstanceScreen(); deckRed = new TriadDeckInstanceScreen(); blueDeckHistory = new List(); bHasSwapRule = false; swappedBlueCardIdx = -1; lastScanNpc = null; logScan = true; } public EUpdateFlags OnNewScan(ScannerTriad.GameState screenGame, TriadNpc selectedNpc) { EUpdateFlags updateFlags = EUpdateFlags.None; if (screenGame == null) { return updateFlags; } // check if game from screenshot can be continuation of cached one // is current state a continuation of last one? // ideally each blue turn is a capture until game resets = any card disappears from board // guess work for adding sense of persistence to screen decks bool bContinuesPrevState = (deckRed.deck == selectedNpc.Deck) && (lastScanNpc == selectedNpc); if (bContinuesPrevState) { for (int Idx = 0; Idx < gameState.board.Length; Idx++) { bool bWasNull = gameState.board[Idx] == null; bool bIsNull = screenGame.board[Idx] == null; if (!bWasNull && bIsNull) { bContinuesPrevState = false; if (logScan) { Logger.WriteLine("Can't continue previous state: board[" + Idx + "] disappeared "); } } } } else { if (logScan) { Logger.WriteLine("Can't continue previous state: npc changed"); } } bool bModsChanged = (gameSolver.simulation.modifiers.Count != screenGame.mods.Count) || !gameSolver.simulation.modifiers.All(screenGame.mods.Contains); if (bModsChanged) { bHasSwapRule = false; bHasRestartRule = false; bHasOpenRule = false; gameSolver.simulation.modifiers.Clear(); gameSolver.simulation.modifiers.AddRange(screenGame.mods); gameSolver.simulation.specialRules = ETriadGameSpecialMod.None; gameSolver.simulation.modFeatures = TriadGameModifier.EFeature.None; foreach (TriadGameModifier mod in gameSolver.simulation.modifiers) { gameSolver.simulation.modFeatures |= mod.GetFeatures(); // swap rule is bad for screenshot based analysis, no good way of telling what is out of place if (mod is TriadGameModifierSwap) { bHasSwapRule = true; } else if (mod is TriadGameModifierSuddenDeath) { bHasRestartRule = true; } else if (mod is TriadGameModifierAllOpen) { bHasOpenRule = true; } } updateFlags |= EUpdateFlags.Modifiers; bContinuesPrevState = false; if (logScan) { Logger.WriteLine("Can't continue previous state: modifiers changed"); } deckRed.SetSwappedCard(null, -1); } // wipe blue deck history when playing with new npc (or region modifiers have changed) bool bRemoveBlueHistory = bModsChanged || (lastScanNpc != selectedNpc); if (bRemoveBlueHistory) { blueDeckHistory.Clear(); if (bHasSwapRule && logScan) { Logger.WriteLine("Blue deck history cleared"); } } bool bRedDeckChanged = (lastScanNpc != selectedNpc) || !IsDeckMatching(deckRed, screenGame.redDeck) || (deckRed.deck != selectedNpc.Deck); if (bRedDeckChanged) { updateFlags |= EUpdateFlags.RedDeck; deckRed.deck = selectedNpc.Deck; lastScanNpc = selectedNpc; // needs to happen before any changed to board (gameState) UpdateAvailableRedCards(deckRed, screenGame.redDeck, screenGame.blueDeck, screenGame.board, deckBlue.cards, gameState.board, bContinuesPrevState); } bool bBlueDeckChanged = !IsDeckMatching(deckBlue, screenGame.blueDeck); if (bBlueDeckChanged) { updateFlags |= EUpdateFlags.BlueDeck; deckBlue.UpdateAvailableCards(screenGame.blueDeck); } gameState.state = ETriadGameState.InProgressBlue; gameState.deckBlue = deckBlue; gameState.deckRed = deckRed; gameState.numCardsPlaced = 0; gameState.forcedCardIdx = deckBlue.GetCardIndex(screenGame.forcedBlueCard); bool bBoardChanged = false; for (int Idx = 0; Idx < gameState.board.Length; Idx++) { bool bWasNull = gameState.board[Idx] == null; bool bIsNull = screenGame.board[Idx] == null; if (bWasNull && !bIsNull) { bBoardChanged = true; gameState.board[Idx] = new TriadCardInstance(screenGame.board[Idx], screenGame.boardOwner[Idx]); if (logScan) { Logger.WriteLine(" board update: [" + Idx + "] " + gameState.board[Idx].owner + ": " + gameState.board[Idx].card.Name.GetCodeName()); } } else if (!bWasNull && bIsNull) { bBoardChanged = true; gameState.board[Idx] = null; } else if (!bWasNull && !bIsNull) { if (gameState.board[Idx].owner != screenGame.boardOwner[Idx] || gameState.board[Idx].card != screenGame.board[Idx]) { bBoardChanged = true; gameState.board[Idx] = new TriadCardInstance(screenGame.board[Idx], screenGame.boardOwner[Idx]); } } gameState.numCardsPlaced += (gameState.board[Idx] != null) ? 1 : 0; } if (bBoardChanged) { updateFlags |= EUpdateFlags.Board; foreach (TriadGameModifier mod in gameSolver.simulation.modifiers) { mod.OnScreenUpdate(gameState); } } // start of game, do additional checks when swap rule is active if (bHasSwapRule && gameState.numCardsPlaced <= 1) { updateFlags |= DetectSwapOnGameStart(); } if (logScan) { Logger.WriteLine("OnNewScan> board:" + (bBoardChanged ? "changed" : "same") + ", blue:" + (bBlueDeckChanged ? "changed" : "same") + ", red:" + (bRedDeckChanged ? "changed" : "same") + ", mods:" + (bModsChanged ? "changed" : "same") + ", continuePrev:" + bContinuesPrevState + " => " + ((updateFlags != EUpdateFlags.None) ? ("UPDATE[" + updateFlags + "]") : "skip")); } return updateFlags; } private bool IsDeckMatching(TriadDeckInstanceScreen deckInstance, TriadCard[] cards) { bool bIsMatching = false; if ((deckInstance != null) && (cards != null) && (deckInstance.cards.Length >= cards.Length)) { bIsMatching = true; for (int Idx = 0; Idx < cards.Length; Idx++) { bIsMatching = bIsMatching && (cards[Idx] == deckInstance.cards[Idx]); } } return bIsMatching; } private void UpdateAvailableRedCards(TriadDeckInstanceScreen redDeck, TriadCard[] screenCardsRed, TriadCard[] screenCardsBlue, TriadCard[] screenBoard, TriadCard[] prevCardsBlue, TriadCardInstance[] prevBoard, bool bContinuePrevState) { bool bDebugMode = false; int hiddenCardId = TriadCardDB.Get().hiddenCard.Id; int numVisibleCards = deckRed.cards.Length; redDeck.numPlaced = 0; if (!bContinuePrevState) { redDeck.numUnknownPlaced = 0; } int maxUnknownToUse = redDeck.cards.Length - redDeck.deck.knownCards.Count; int firstUnknownPoolIdx = redDeck.cards.Length + redDeck.deck.knownCards.Count; if (redDeck.deck.unknownCardPool.Count > 0) { redDeck.unknownPoolMask = ((1 << redDeck.deck.unknownCardPool.Count) - 1) << firstUnknownPoolIdx; for (int Idx = 0; Idx < screenCardsRed.Length; Idx++) { if ((screenCardsRed[Idx] != null) && (screenCardsRed[Idx].Id != hiddenCardId) && redDeck.deck.unknownCardPool.Contains(screenCardsRed[Idx])) { redDeck.unknownPoolMask |= (1 << Idx); } } } int allDeckAvailableMask = ((1 << (redDeck.deck.knownCards.Count + redDeck.deck.unknownCardPool.Count)) - 1) << numVisibleCards; bool bCanCompareWithPrevData = (screenCardsRed.Length == redDeck.cards.Length) && (screenCardsBlue.Length == prevCardsBlue.Length) && (screenBoard.Length == prevBoard.Length); if (bCanCompareWithPrevData && !bContinuePrevState) { // special case: 1st turn int numCardsOnBoard = 0; for (int Idx = 0; Idx < screenBoard.Length; Idx++) { if (screenBoard[Idx] != null) { numCardsOnBoard++; } } if (numCardsOnBoard <= 1) { bCanCompareWithPrevData = true; prevBoard = new TriadCardInstance[screenBoard.Length]; prevCardsBlue = new TriadCard[numVisibleCards]; deckRed.cards = new TriadCard[numVisibleCards]; deckRed.availableCardMask = allDeckAvailableMask; deckRed.numPlaced = 0; deckRed.numUnknownPlaced = 0; } else { bCanCompareWithPrevData = false; } } if (bDebugMode) { Logger.WriteLine("Red deck update, diff mode check... " + "bContinuePrevState:" + bContinuePrevState + ", cards(screen:" + screenCardsRed.Length + ", prev:" + deckRed.cards.Length + ")=" + ((screenCardsRed.Length == deckRed.cards.Length) ? "ok" : "nope") + ", other(screen:" + screenCardsBlue.Length + ", prev:" + prevCardsBlue.Length + ")=" + ((screenCardsBlue.Length == prevCardsBlue.Length) ? "ok" : "nope") + ", board(screen:" + screenBoard.Length + ", prev:" + prevBoard.Length + ")=" + ((screenBoard.Length == prevBoard.Length) ? "ok" : "nope")); } if (bCanCompareWithPrevData) { // create diffs, hopefully prev state comes from last turn and is just 2 cards away List usedCardsIndices = new List(); List usedCardsOther = new List(); int numKnownOnHand = 0; int numUnknownOnHand = 0; int numHidden = 0; int numOnHand = 0; for (int Idx = 0; Idx < deckRed.cards.Length; Idx++) { if (screenCardsRed[Idx] == null) { TriadCard prevCard = deckRed.cards[Idx]; if ((prevCard != null) && (prevCard.Id != hiddenCardId)) { if (bDebugMode) { Logger.WriteLine(" card[" + Idx + "]:" + prevCard.Name.GetCodeName() + " => mark as used, disappeared from prev state"); } usedCardsIndices.Add(Idx); } deckRed.availableCardMask &= ~(1 << Idx); deckRed.numPlaced++; } else { if (screenCardsRed[Idx].Id != hiddenCardId) { bool bIsUnknown = (deckRed.unknownPoolMask & (1 << Idx)) != 0; numUnknownOnHand += bIsUnknown ? 1 : 0; numKnownOnHand += bIsUnknown ? 0 : 1; numOnHand++; deckRed.availableCardMask |= (1 << Idx); int knownCardIdx = deckRed.deck.knownCards.IndexOf(screenCardsRed[Idx]); int unknownCardIdx = deckRed.deck.unknownCardPool.IndexOf(screenCardsRed[Idx]); if (knownCardIdx >= 0) { deckRed.availableCardMask &= ~(1 << (knownCardIdx + deckRed.cards.Length)); } else if (unknownCardIdx >= 0) { deckRed.availableCardMask &= ~(1 << (unknownCardIdx + deckRed.cards.Length + deckRed.deck.knownCards.Count)); } if (bDebugMode) { TriadCard cardOb = screenCardsRed[Idx]; Logger.WriteLine(" card[" + Idx + "]:" + (cardOb != null ? cardOb.Name.GetCodeName() : "??") + " => numUnknown:" + numUnknownOnHand + ", numKnown:" + numKnownOnHand + ", numHidden:" + numHidden); } } else { numHidden++; } } } for (int Idx = 0; Idx < prevCardsBlue.Length; Idx++) { if ((prevCardsBlue[Idx] != null) && (screenCardsBlue[Idx] == null)) { usedCardsOther.Add(prevCardsBlue[Idx]); if (bDebugMode) { Logger.WriteLine(" blue[" + Idx + "]:" + prevCardsBlue[Idx].Name.GetCodeName() + " => mark as used"); } } } for (int Idx = 0; Idx < prevBoard.Length; Idx++) { TriadCard testCard = screenBoard[Idx]; if ((prevBoard[Idx] == null || prevBoard[Idx].card == null) && (testCard != null)) { int testCardIdx = deckRed.GetCardIndex(testCard); if (!usedCardsOther.Contains(testCard) && (testCardIdx >= 0)) { usedCardsIndices.Add(testCardIdx); if (bDebugMode) { Logger.WriteLine(" card[" + testCardIdx + "]:" + testCard.Name.GetCodeName() + " => mark as used, appeared on board[" + Idx + "], not used by blue"); } } } } Array.Copy(screenCardsRed, deckRed.cards, 5); for (int Idx = 0; Idx < usedCardsIndices.Count; Idx++) { int cardMask = 1 << usedCardsIndices[Idx]; deckRed.availableCardMask &= ~cardMask; bool bIsUnknownPool = (deckRed.unknownPoolMask & cardMask) != 0; if (bIsUnknownPool) { deckRed.numUnknownPlaced++; } if (bDebugMode) { TriadCard cardOb = deckRed.GetCard(usedCardsIndices[Idx]); Logger.WriteLine(" card[" + usedCardsIndices[Idx] + "]:" + (cardOb != null ? cardOb.Name.GetCodeName() : "??") + " => used"); } } if ((numHidden == 0) && ((numOnHand + deckRed.numPlaced) == numVisibleCards)) { deckRed.availableCardMask &= (1 << numVisibleCards) - 1; if (bDebugMode) { Logger.WriteLine(" all cards are on hand and visible"); } } else if ((deckRed.numUnknownPlaced + numUnknownOnHand) >= maxUnknownToUse || ((numKnownOnHand >= (numVisibleCards - maxUnknownToUse)) && (numHidden == 0))) { deckRed.availableCardMask &= (1 << (numVisibleCards + deckRed.deck.knownCards.Count)) - 1; if (bDebugMode) { Logger.WriteLine(" removing all unknown cards, numUnknownPlaced:" + deckRed.numUnknownPlaced + ", numUnknownOnHand:" + numUnknownOnHand + ", numKnownOnHand:" + numKnownOnHand + ", numHidden:" + numHidden + ", maxUnknownToUse:" + maxUnknownToUse); } } } else { // TriadDeckInstanceScreen is mostly stateless (created from scratch on screen capture) // this makes guessing which cards were placed hard, especially when there's no good // history data to compare with. // Ignore board data here, cards could be placed by blue and are still available for red deck deckRed.UpdateAvailableCards(screenCardsRed); deckRed.availableCardMask = allDeckAvailableMask; } if (bDebugMode) { redDeck.LogAvailableCards("Red deck"); } } public void UpdatePlayerDeck(TriadDeck playerDeck) { playerDeckPattern = playerDeck.knownCards.ToArray(); } private bool FindSwappedCard(TriadCard[] screenCards, TriadCard[] expectedCards, TriadDeckInstanceScreen otherDeck, out int swappedCardIdx, out int swappedOtherIdx, out TriadCard swappedCard) { swappedCardIdx = -1; swappedOtherIdx = -1; swappedCard = null; TriadCard swappedBlueCard = null; int numDiffs = 0; int numPotentialSwaps = 0; for (int Idx = 0; Idx < screenCards.Length; Idx++) { if ((screenCards[Idx] != expectedCards[Idx]) && (screenCards[Idx] != null)) { numDiffs++; swappedCardIdx = Idx; swappedOtherIdx = otherDeck.GetCardIndex(screenCards[Idx]); swappedBlueCard = screenCards[Idx]; swappedCard = expectedCards[Idx]; Logger.WriteLine("FindSwappedCard[" + Idx + "]: screen:" + screenCards[Idx].Name.GetCodeName() + ", expected:" + expectedCards[Idx].Name.GetCodeName() + ", redIdxScreen:" + swappedOtherIdx); if (swappedOtherIdx >= 0) { numPotentialSwaps++; } } } bool bHasSwapped = (numDiffs == 1) && (numPotentialSwaps == 1); Logger.WriteLine("FindSwappedCard: blue[" + swappedCardIdx + "]:" + (swappedBlueCard != null ? swappedBlueCard.Name.GetCodeName() : "??") + " <=> red[" + swappedOtherIdx + "]:" + (swappedCard != null ? swappedCard.Name.GetCodeName() : "??") + ", diffs:" + numDiffs + ", potentialSwaps:" + numPotentialSwaps + " => " + (bHasSwapped ? "SWAP" : "ignore")); return bHasSwapped; } private bool FindSwappedCardVisible(TriadCard[] screenCards, TriadCardInstance[] board, TriadDeckInstanceScreen otherDeck, out int swappedCardIdx, out int swappedOtherIdx, out TriadCard swappedCard) { swappedCardIdx = -1; swappedOtherIdx = -1; swappedCard = null; int numDiffs = 0; int numOnHand = 0; int hiddenCardId = TriadCardDB.Get().hiddenCard.Id; for (int Idx = 0; Idx < otherDeck.cards.Length; Idx++) { if (otherDeck.cards[Idx] != null && otherDeck.cards[Idx].Id != hiddenCardId) { // find in source deck, not in instance int cardIdx = otherDeck.deck.GetCardIndex(otherDeck.cards[Idx]); if (cardIdx < 0) { swappedOtherIdx = Idx; swappedCard = otherDeck.cards[Idx]; for (int ScreenIdx = 0; ScreenIdx < screenCards.Length; ScreenIdx++) { cardIdx = otherDeck.deck.GetCardIndex(screenCards[ScreenIdx]); if (cardIdx >= 0) { swappedCardIdx = ScreenIdx; numDiffs++; } } } } numOnHand += (otherDeck.cards[Idx] != null) ? 1 : 0; } bool bBoardMode = false; if (numOnHand < screenCards.Length) { for (int Idx = 0; Idx < board.Length; Idx++) { if (board[Idx] != null && board[Idx].owner == ETriadCardOwner.Red) { // find in source deck, not in instance int cardIdx = otherDeck.deck.GetCardIndex(board[Idx].card); if (cardIdx < 0) { swappedCard = board[Idx].card; swappedOtherIdx = 100; // something way outside, it's not going to be used directly as card was already placed for (int ScreenIdx = 0; ScreenIdx < screenCards.Length; ScreenIdx++) { cardIdx = otherDeck.deck.GetCardIndex(screenCards[ScreenIdx]); if (cardIdx >= 0) { swappedCardIdx = ScreenIdx; bBoardMode = true; numDiffs++; } } } } } } bool bHasSwapped = (numDiffs == 1); Logger.WriteLine("FindSwappedCardVisible: blue[" + swappedCardIdx + "]:" + (swappedCardIdx >= 0 ? screenCards[swappedCardIdx].Name.GetCodeName() : "??") + " <=> red[" + swappedOtherIdx + "]:" + (swappedCard != null ? swappedCard.Name.GetCodeName() : "??") + ", boardMode:" + bBoardMode + ", diffs:" + numDiffs + " => " + (bHasSwapped ? "SWAP" : "ignore")); return bHasSwapped; } private TriadCard[] FindCommonCards(List deckHistory) { TriadCard[] result = null; if (deckHistory.Count > 1) { result = new TriadCard[deckHistory[0].Length]; for (int SlotIdx = 0; SlotIdx < result.Length; SlotIdx++) { Dictionary slotCounter = new Dictionary(); TriadCard bestSlotCard = null; int bestSlotCount = 0; for (int HistoryIdx = 0; HistoryIdx < deckHistory.Count; HistoryIdx++) { TriadCard testCard = deckHistory[HistoryIdx][SlotIdx]; if (slotCounter.ContainsKey(testCard)) { slotCounter[testCard] += 1; } else { slotCounter.Add(testCard, 1); } if (slotCounter[testCard] > bestSlotCount) { bestSlotCount = slotCounter[testCard]; bestSlotCard = testCard; } } Logger.WriteLine("FindCommonCards[" + SlotIdx + "]: " + bestSlotCard.Name.GetCodeName() + " x" + bestSlotCount + (bestSlotCount < 2 ? " => not enough to decide!" : "")); if (bestSlotCount >= 2) { result[SlotIdx] = bestSlotCard; } else { result = null; break; } } } return result; } private EUpdateFlags DetectSwapOnGameStart() { EUpdateFlags updateFlags = EUpdateFlags.None; deckRed.SetSwappedCard(null, -1); for (int Idx = 0; Idx < deckBlue.cards.Length; Idx++) { if (deckBlue.cards[Idx] == null) { Logger.WriteLine("DetectSwapOnGameStart: found empty blue card, skipping"); return updateFlags; } } bool bDetectedSuddenDeath = bHasRestartRule && IsSuddenDeathRestart(deckRed); if (bDetectedSuddenDeath) { Logger.WriteLine(">> ignore swap checks"); return updateFlags; } // store initial blue deck { if (blueDeckHistory.Count > 10) { blueDeckHistory.RemoveAt(0); } TriadCard[] copyCards = new TriadCard[deckBlue.cards.Length]; Array.Copy(deckBlue.cards, copyCards, copyCards.Length); blueDeckHistory.Add(copyCards); Logger.WriteLine("Storing blue deck at[" + blueDeckHistory.Count + "]: " + deckBlue); } int blueSwappedCardIdx = -1; int redSwappedCardIdx = -1; TriadCard blueSwappedCard = null; bool bHasSwappedCard = FindSwappedCardVisible(deckBlue.cards, gameState.board, deckRed, out blueSwappedCardIdx, out redSwappedCardIdx, out blueSwappedCard); if (!bHasSwappedCard) { bHasSwappedCard = FindSwappedCard(deckBlue.cards, playerDeckPattern, deckRed, out blueSwappedCardIdx, out redSwappedCardIdx, out blueSwappedCard); if (!bHasSwappedCard) { TriadCard[] commonCards = FindCommonCards(blueDeckHistory); if (commonCards != null) { bHasSwappedCard = FindSwappedCard(deckBlue.cards, commonCards, deckRed, out blueSwappedCardIdx, out redSwappedCardIdx, out blueSwappedCard); } } } if (bHasSwappedCard) { // deck blue doesn't need updates, it already has all cards visible // deck red needs to know which card is not longer available and which one is new deckRed.SetSwappedCard(blueSwappedCard, redSwappedCardIdx); swappedBlueCardIdx = blueSwappedCardIdx; updateFlags |= EUpdateFlags.SwapHints; } else { swappedBlueCardIdx = -1; updateFlags |= EUpdateFlags.SwapWarning; } return updateFlags; } private bool IsSuddenDeathRestart(TriadDeckInstanceScreen deck) { // sudden death: all red cards visible, not matching npc deck at all int numMismatchedCards = 0; int numVisibleCards = 0; int hiddenCardId = TriadCardDB.Get().hiddenCard.Id; for (int Idx = 0; Idx < deck.cards.Length; Idx++) { int npcCardIdx = deck.deck.GetCardIndex(deck.cards[Idx]); if (npcCardIdx < 0) { numMismatchedCards++; } if (deck.cards[Idx] != null && deck.cards[Idx].Id != hiddenCardId) { numVisibleCards++; } } bool bHasOpenAndMismatched = (numVisibleCards >= 4 && numMismatchedCards > 1); bool bHasOpenAndShouldnt = (numVisibleCards >= 4 && !bHasOpenRule); Logger.WriteLine("IsSuddenDeathRestart? numMismatchedCards:" + numMismatchedCards + ", numVisibleCards:" + numVisibleCards); return bHasOpenAndMismatched || bHasOpenAndShouldnt; } } } ================================================ FILE: sources/gamelogic/TriadGameSimulation.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; namespace FFTriadBuddy { public enum ETriadGameState { InProgressBlue, InProgressRed, BlueWins, BlueDraw, BlueLost, } public class TriadGameSimulationState { public TriadCardInstance[] board; public TriadDeckInstance deckBlue; public TriadDeckInstance deckRed; public ETriadGameState state; public ETriadGameSpecialMod resolvedSpecial; public int[] typeMods; public int numCardsPlaced; public int numRestarts; public int forcedCardIdx; public bool bDebugRules; public const int boardSize = 3; public const int boardSizeSq = boardSize * boardSize; public TriadGameSimulationState() { board = new TriadCardInstance[boardSizeSq]; typeMods = new int[Enum.GetNames(typeof(ETriadCardType)).Length]; state = ETriadGameState.InProgressBlue; resolvedSpecial = ETriadGameSpecialMod.None; numCardsPlaced = 0; numRestarts = 0; forcedCardIdx = -1; bDebugRules = false; for (int Idx = 0; Idx < typeMods.Length; Idx++) { typeMods[Idx] = 0; } } public TriadGameSimulationState(TriadGameSimulationState copyFrom) { board = new TriadCardInstance[copyFrom.board.Length]; for (int Idx = 0; Idx < board.Length; Idx++) { board[Idx] = (copyFrom.board[Idx] == null) ? null : new TriadCardInstance(copyFrom.board[Idx]); } typeMods = new int[copyFrom.typeMods.Length]; for (int Idx = 0; Idx < typeMods.Length; Idx++) { typeMods[Idx] = copyFrom.typeMods[Idx]; } deckBlue = copyFrom.deckBlue.CreateCopy(); deckRed = copyFrom.deckRed.CreateCopy(); state = copyFrom.state; numCardsPlaced = copyFrom.numCardsPlaced; numRestarts = copyFrom.numRestarts; resolvedSpecial = copyFrom.resolvedSpecial; // bDebugRules not copied, only first step needs it } } public class TriadGameSimulation { public List modifiers = new List(); public ETriadGameSpecialMod specialRules; public TriadGameModifier.EFeature modFeatures = TriadGameModifier.EFeature.None; public static int[][] cachedNeis = new int[9][]; public TriadGameSimulationState StartGame(TriadDeck deckBlue, TriadDeck deckRed, ETriadGameState state) { foreach (var mod in modifiers) { mod.OnMatchInit(); } return new TriadGameSimulationState() { state = state, deckBlue = new TriadDeckInstanceManual(deckBlue), deckRed = new TriadDeckInstanceManual(deckRed) }; } public void Initialize(IEnumerable modsA, IEnumerable modsB = null) { modifiers.Clear(); if (modsA != null) { foreach (var mod in modsA) { TriadGameModifier modCopy = (TriadGameModifier)Activator.CreateInstance(mod.GetType()); modifiers.Add(modCopy); } } if (modsB != null) { foreach (var mod in modsB) { TriadGameModifier modCopy = (TriadGameModifier)Activator.CreateInstance(mod.GetType()); modifiers.Add(modCopy); } } UpdateSpecialRules(); } public void UpdateSpecialRules() { specialRules = ETriadGameSpecialMod.None; modFeatures = TriadGameModifier.EFeature.None; foreach (TriadGameModifier mod in modifiers) { specialRules |= mod.GetSpecialRules(); modFeatures |= mod.GetFeatures(); } } public bool HasSpecialRule(ETriadGameSpecialMod specialRule) { return (specialRules & specialRule) != ETriadGameSpecialMod.None; } public bool PlaceCard(TriadGameSimulationState gameState, int cardIdx, TriadDeckInstance cardDeck, ETriadCardOwner owner, int boardPos) { bool bResult = false; bool bIsAllowedOwner = ((owner == ETriadCardOwner.Blue) && (gameState.state == ETriadGameState.InProgressBlue)) || ((owner == ETriadCardOwner.Red) && (gameState.state == ETriadGameState.InProgressRed)); TriadCard card = cardDeck.GetCard(cardIdx); if (bIsAllowedOwner && (boardPos >= 0) && (gameState.board[boardPos] == null) && (card != null)) { gameState.board[boardPos] = new TriadCardInstance(card, owner); gameState.numCardsPlaced++; if (owner == ETriadCardOwner.Blue) { gameState.deckBlue.OnCardPlacedFast(cardIdx); gameState.state = ETriadGameState.InProgressRed; } else { gameState.deckRed.OnCardPlacedFast(cardIdx); gameState.state = ETriadGameState.InProgressBlue; } // verify owner bResult = (owner == ETriadCardOwner.Red) || !HasSpecialRule(ETriadGameSpecialMod.IgnoreOwnedCheck); bool bAllowCombo = false; if ((modFeatures & TriadGameModifier.EFeature.CardPlaced) != 0) { foreach (TriadGameModifier mod in modifiers) { mod.OnCardPlaced(gameState, boardPos); bAllowCombo = bAllowCombo || mod.AllowsCombo(); } } List comboList = new List(); int comboCounter = 0; CheckCaptures(gameState, boardPos, comboList, comboCounter); while (bAllowCombo && comboList.Count > 0) { if (gameState.bDebugRules) { Logger.WriteLine(">> combo step: {0}", string.Join(",", comboList)); } List nextCombo = new List(); comboCounter++; foreach (int pos in comboList) { CheckCaptures(gameState, pos, nextCombo, comboCounter); } comboList = nextCombo; } if ((modFeatures & TriadGameModifier.EFeature.PostCapture) != 0) { foreach (TriadGameModifier mod in modifiers) { mod.OnPostCaptures(gameState, boardPos); } } if (gameState.numCardsPlaced == gameState.board.Length) { OnAllCardsPlaced(gameState); } } return bResult; } public bool PlaceCard(TriadGameSimulationState gameState, TriadCard card, ETriadCardOwner owner, int boardPos) { TriadDeckInstance useDeck = (owner == ETriadCardOwner.Blue) ? gameState.deckBlue : gameState.deckRed; int cardIdx = useDeck.GetCardIndex(card); return PlaceCard(gameState, cardIdx, useDeck, owner, boardPos); } public static int GetBoardPos(int x, int y) { return x + (y * TriadGameSimulationState.boardSize); } public static void GetBoardXY(int pos, out int x, out int y) { x = pos % TriadGameSimulationState.boardSize; y = pos / TriadGameSimulationState.boardSize; } public static int[] GetNeighbors(TriadGameSimulationState gameState, int boardPos) { int boardPosX = 0; int boardPosY = 0; GetBoardXY(boardPos, out boardPosX, out boardPosY); int[] resultNeis = new int[4]; resultNeis[(int)ETriadGameSide.Up] = (boardPosY > 0) ? GetBoardPos(boardPosX, boardPosY - 1) : -1; resultNeis[(int)ETriadGameSide.Down] = (boardPosY < (TriadGameSimulationState.boardSize - 1)) ? GetBoardPos(boardPosX, boardPosY + 1) : -1; resultNeis[(int)ETriadGameSide.Right] = (boardPosX > 0) ? GetBoardPos(boardPosX - 1, boardPosY) : -1; resultNeis[(int)ETriadGameSide.Left] = (boardPosX < (TriadGameSimulationState.boardSize - 1)) ? GetBoardPos(boardPosX + 1, boardPosY) : -1; return resultNeis; } private void CheckCaptures(TriadGameSimulationState gameState, int boardPos, List comboList, int comboCounter) { // combo: // - modifiers are active only in intial placement // - ...except for reverse, this one stays active in chains too... // - only card captured via modifiers can initiate combo (same, plus) // - type modifiers (ascention, descention) values are baked in card and influence combo // - can't proc another plus/same as a result of combo int[] neis = cachedNeis[boardPos]; bool allowMods = comboCounter == 0; if (allowMods && (modFeatures & TriadGameModifier.EFeature.CaptureNei) != 0) { foreach (TriadGameModifier mod in modifiers) { mod.OnCheckCaptureNeis(gameState, boardPos, neis, comboList); } } // CaptureMath: used only by reverse rule bool isReverseActive = allowMods && ((modFeatures & TriadGameModifier.EFeature.CaptureMath) != 0); TriadCardInstance checkCard = gameState.board[boardPos]; for (int sideIdx = 0; sideIdx < 4; sideIdx++) { int neiPos = neis[sideIdx]; if (neiPos >= 0 && gameState.board[neiPos] != null) { TriadCardInstance neiCard = gameState.board[neiPos]; if (checkCard.owner != neiCard.owner) { int numPos = checkCard.GetNumber((ETriadGameSide)sideIdx); int numOther = neiCard.GetOppositeNumber((ETriadGameSide)sideIdx); if (allowMods && (modFeatures & TriadGameModifier.EFeature.CaptureWeights) != 0) { // CaptureWeights: use only by fallen ace = asymetric rule, needs to know about active reverse foreach (TriadGameModifier mod in modifiers) { mod.OnCheckCaptureCardWeights(gameState, boardPos, neiPos, isReverseActive, ref numPos, ref numOther); } } bool bIsCaptured = (numPos > numOther); // special case: always allow CaptureMath (= reverse mod) during combo chain, see comments above { foreach (TriadGameModifier mod in modifiers) { mod.OnCheckCaptureCardMath(gameState, boardPos, neiPos, numPos, numOther, ref bIsCaptured); } } if (bIsCaptured) { neiCard.owner = checkCard.owner; if (comboCounter > 0) { comboList.Add(neiPos); } if (gameState.bDebugRules) { Logger.WriteLine(">> " + (comboCounter > 0 ? "combo!" : "") + " [" + neiPos + "] " + neiCard.card.Name.GetCodeName() + " => " + neiCard.owner); } } } } } } private void OnAllCardsPlaced(TriadGameSimulationState gameState) { int numBlue = (gameState.deckBlue.availableCardMask != 0) ? 1 : 0; foreach (TriadCardInstance card in gameState.board) { if (card.owner == ETriadCardOwner.Blue) { numBlue++; } } int numBlueToWin = (gameState.board.Length / 2) + 1; gameState.state = (numBlue > numBlueToWin) ? ETriadGameState.BlueWins : (numBlue == numBlueToWin) ? ETriadGameState.BlueDraw : ETriadGameState.BlueLost; if (gameState.bDebugRules) { TriadCard availBlueCard = gameState.deckBlue.GetFirstAvailableCard(); Logger.WriteLine(">> blue:" + numBlue + " (in deck:" + ((availBlueCard != null) ? availBlueCard.Name.GetCodeName() : "none") + "), required:" + numBlueToWin + " => " + gameState.state); } if ((modFeatures & TriadGameModifier.EFeature.AllPlaced) != 0) { foreach (TriadGameModifier mod in modifiers) { mod.OnAllCardsPlaced(gameState); } } } public static void StaticInitialize() { for (int idxPos = 0; idxPos < 9; idxPos++) { cachedNeis[idxPos] = GetNeighbors(null, idxPos); } } } } ================================================ FILE: sources/gamelogic/TriadGameSolver.cs ================================================ using System.Collections.Generic; namespace FFTriadBuddy { public struct SolverResult { public float numWins; public float numDraws; public long numGames; public float winChance; public float drawChance; public ETriadGameState expectedResult; public float score; public static SolverResult Zero = new SolverResult(0, 0, 0); public SolverResult(float numWins, float numDraws, long numGames) { this.numWins = numWins; this.numDraws = numDraws; this.numGames = numGames; winChance = (numGames <= 0) ? 0.0f : (numWins / numGames); drawChance = (numGames <= 0) ? 0.0f : (numDraws / numGames); if (winChance < 0.25f && drawChance < 0.25f) { score = winChance / 10.0f; expectedResult = ETriadGameState.BlueLost; } else if (winChance < drawChance) { score = drawChance; expectedResult = ETriadGameState.BlueDraw; } else { score = winChance + 10.0f; expectedResult = ETriadGameState.BlueWins; } } public bool IsBetterThan(SolverResult other) { return score > other.score; } public override string ToString() { return $"{expectedResult}, score:{score}, win:{winChance:P0} ({numWins:0.##}/{numGames}), draw:{drawChance:P0} ({numDraws:0.##}/{numGames})"; } } public class TriadGameSolver { public TriadGameSimulation simulation = new TriadGameSimulation(); public TriadGameAgent agent = new TriadGameAgentCarloTheExplorer(); public string name; public TriadGameSolver() { agent.Initialize(this, 0); } public void InitializeSimulation(IEnumerable modsA, IEnumerable modsB) => simulation.Initialize(modsA, modsB); public void InitializeSimulation(IEnumerable mods) => simulation.Initialize(mods, null); public TriadGameSimulationState StartSimulation(TriadDeck deckBlue, TriadDeck deckRed, ETriadGameState state) { agent.OnSimulationStart(); return simulation.StartGame(deckBlue, deckRed, state); } public bool HasSimulationRule(ETriadGameSpecialMod specialRule) => simulation.HasSpecialRule(specialRule); public float GetAgentProgress() => agent.GetProgress(); public bool FindNextMove(TriadGameSimulationState gameState, out int cardIdx, out int boardPos, out SolverResult solverResult) => agent.FindNextMove(this, gameState, out cardIdx, out boardPos, out solverResult); public void RunSimulation(TriadGameSimulationState gameState, TriadGameAgent agentBlue, TriadGameAgent agentRed) { bool keepPlaying = true; while (keepPlaying) { if (gameState.state == ETriadGameState.InProgressBlue) { keepPlaying = agentBlue.FindNextMove(this, gameState, out int cardIdx, out int boardPos, out _); if (keepPlaying) { keepPlaying = simulation.PlaceCard(gameState, cardIdx, gameState.deckBlue, ETriadCardOwner.Blue, boardPos); } } else if (gameState.state == ETriadGameState.InProgressRed) { keepPlaying = agentRed.FindNextMove(this, gameState, out int cardIdx, out int boardPos, out _); if (keepPlaying) { keepPlaying = simulation.PlaceCard(gameState, cardIdx, gameState.deckRed, ETriadCardOwner.Red, boardPos); } } else { keepPlaying = false; } } } public void FindAvailableActions(TriadGameSimulationState gameState, out int availBoardMask, out int availCardsMask) { // prepare available board data availBoardMask = 0; for (int Idx = 0; Idx < gameState.board.Length; Idx++) { if (gameState.board[Idx] == null) { availBoardMask |= (1 << Idx); } } // prepare available cards data availCardsMask = (gameState.forcedCardIdx >= 0) ? (1 << gameState.forcedCardIdx) : (gameState.state == ETriadGameState.InProgressBlue) ? gameState.deckBlue.availableCardMask : gameState.deckRed.availableCardMask; if ((simulation.modFeatures & TriadGameModifier.EFeature.FilterNext) != 0) { foreach (var mod in simulation.modifiers) { mod.OnFilterNextCards(gameState, ref availCardsMask); } } } public void FindAvailableActions(TriadGameSimulationState gameState, out int availBoardMask, out int numAvailBoard, out int availCardsMask, out int numAvailCards) { FindAvailableActions(gameState, out availBoardMask, out availCardsMask); numAvailBoard = CountSetBits(availBoardMask); numAvailCards = CountSetBits(availCardsMask); int CountSetBits(int value) { value = value - ((value >> 1) & 0x55555555); value = (value & 0x33333333) + ((value >> 2) & 0x33333333); return (((value + (value >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24; } } } } ================================================ FILE: sources/gamelogic/tests/TriadGameScreenTests.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; namespace FFTriadBuddy { public class TriadGameScreenTests { private static Dictionary mapValidationRules; private class VerifyMove { private ETriadCardOwner[] expectedState; public TriadCard card; public ETriadCardOwner owner; public int boardPos; public int cardIdx; public void Load(JsonParser.ObjectValue configOb) { string ownerStr = configOb["player"] as JsonParser.StringValue; owner = (ownerStr == "blue") ? ETriadCardOwner.Blue : (ownerStr == "red") ? ETriadCardOwner.Red : ETriadCardOwner.Unknown; boardPos = configOb["pos"] as JsonParser.IntValue; cardIdx = configOb["cardIdx"] as JsonParser.IntValue; if (configOb.entries.ContainsKey("board")) { string boardCode = configOb["board"] as JsonParser.StringValue; boardCode = boardCode.Replace(" ", ""); expectedState = new ETriadCardOwner[9]; for (int idx = 0; idx < expectedState.Length; idx++) { expectedState[idx] = (boardCode[idx] == 'R') ? ETriadCardOwner.Red : (boardCode[idx] == 'B') ? ETriadCardOwner.Blue : ETriadCardOwner.Unknown; } } var cardName = configOb["card"] as JsonParser.StringValue; if (cardName != null) { card = TriadCardDB.Get().Find(cardName); } else { var cardSides = configOb["card"] as JsonParser.ArrayValue; int numU = cardSides[0] as JsonParser.IntValue; int numL = cardSides[1] as JsonParser.IntValue; int numD = cardSides[2] as JsonParser.IntValue; int numR = cardSides[3] as JsonParser.IntValue; card = TriadCardDB.Get().Find(numU, numL, numD, numR); } } public bool VerifyState(TriadGameSimulationState gameState, bool debugMode) { if (expectedState != null) { for (int idx = 0; idx < expectedState.Length; idx++) { if (gameState.board[idx].owner != expectedState[idx]) { if (debugMode) { string expectedCode = ""; string currentCode = ""; Func GetOwnerCode = (owner) => (owner == ETriadCardOwner.Blue) ? 'B' : (owner == ETriadCardOwner.Red) ? 'R' : '.'; for (int codeIdx = 0; codeIdx < 9; codeIdx++) { if (codeIdx == 3 || codeIdx == 6) { expectedCode += ' '; currentCode += ' '; } expectedCode += GetOwnerCode(gameState.board[codeIdx].owner); currentCode += GetOwnerCode(expectedState[codeIdx]); } Logger.WriteLine("Failed, mismatch at [{0}]! Expected:{1}, got{2}", idx, expectedCode, currentCode); } return false; } } } return true; } } private static void CopyGameStateToScreen(TriadGameSimulationState testGameData, ScannerTriad.GameState screenGame) { for (int idx = 0; idx < 9; idx++) { screenGame.board[idx] = testGameData.board[idx] != null ? testGameData.board[idx].card : null; screenGame.boardOwner[idx] = testGameData.board[idx] != null ? testGameData.board[idx].owner : ETriadCardOwner.Unknown; } for (int idx = 0; idx < 5; idx++) { screenGame.blueDeck[idx] = testGameData.deckBlue.GetCard(idx); if (testGameData.deckRed.IsPlaced(idx)) { screenGame.redDeck[idx] = null; } } } public static void RunTest(string configPath, bool debugMode) { string testName = System.IO.Path.GetFileNameWithoutExtension(configPath); string configText = System.IO.File.ReadAllText(configPath); JsonParser.ObjectValue rootOb = JsonParser.ParseJson(configText); if (rootOb["type"] != "Screen") { return; } ScannerTriad.VerifyConfig configData = new ScannerTriad.VerifyConfig(); configData.Load(rootOb); // setup npc & modifiers TriadNpc testNpc = TriadNpcDB.Get().Find(configData.npc); if (testNpc == null) { string exceptionMsg = string.Format("Test {0} failed! Can't find npc: {1}", testName, configData.npc); throw new Exception(exceptionMsg); } ScannerTriad.GameState screenGame = new ScannerTriad.GameState(); if (mapValidationRules == null) { mapValidationRules = new Dictionary(); foreach (TriadGameModifier mod in ImageHashDB.Get().modObjects) { mapValidationRules.Add(mod.GetCodeName(), mod); } } foreach (string modName in configData.rules) { screenGame.mods.Add(mapValidationRules[modName]); } Func ConvertToTriadCard = configCard => { if (configCard.state == ScannerTriad.ECardState.None) { return null; } if (configCard.state == ScannerTriad.ECardState.Hidden) { return TriadCardDB.Get().hiddenCard; } TriadCard matchingCard = !string.IsNullOrEmpty(configCard.name) ? TriadCardDB.Get().Find(configCard.name) : TriadCardDB.Get().Find(configCard.sides[0], configCard.sides[1], configCard.sides[2], configCard.sides[3]); if (matchingCard == null) { string exceptionMsg = string.Format("Test {0} failed! Can't match validation card: '{1}' [{2},{3},{4},{5}]", testName, configCard.name, configCard.sides[0], configCard.sides[1], configCard.sides[2], configCard.sides[3]); throw new Exception(exceptionMsg); } return matchingCard; }; bool needsLockedBlue = false; for (int idx = 0; idx < 5; idx++) { screenGame.blueDeck[idx] = ConvertToTriadCard(configData.deckBlue[idx]); screenGame.redDeck[idx] = ConvertToTriadCard(configData.deckRed[idx]); if (configData.deckBlue[idx].state == ScannerTriad.ECardState.Locked) { needsLockedBlue = true; } } if (needsLockedBlue) { for (int idx = 0; idx < 5; idx++) { if (configData.deckBlue[idx].state == ScannerTriad.ECardState.Visible) { screenGame.forcedBlueCard = screenGame.blueDeck[idx]; break; } } } for (int idx = 0; idx < 9; idx++) { screenGame.board[idx] = ConvertToTriadCard(configData.board[idx]); screenGame.boardOwner[idx] = configData.board[idx].state == ScannerTriad.ECardState.PlacedBlue ? ETriadCardOwner.Blue : configData.board[idx].state == ScannerTriad.ECardState.PlacedRed ? ETriadCardOwner.Red : ETriadCardOwner.Unknown; } TriadGameScreenMemory screenMemory = new TriadGameScreenMemory { logScan = false }; if (!rootOb.entries.ContainsKey("moves")) { screenMemory.OnNewScan(screenGame, testNpc); bool hasMove = screenMemory.gameSolver.FindNextMove(screenMemory.gameState, out var dummyCardIdx, out var dummyBoardPos, out var bestChance); if (!hasMove || (bestChance.expectedResult == ETriadGameState.BlueLost && bestChance.winChance <= 0.0f && bestChance.drawChance <= 0.0f)) { string exceptionMsg = string.Format("Test {0} failed! Can't find move!", testName); throw new Exception(exceptionMsg); } } else { debugMode = true; TriadGameSimulation testSession = new TriadGameSimulation(); testSession.Initialize(screenGame.mods, null); TriadGameSimulationState testGameState = new TriadGameSimulationState() { bDebugRules = debugMode }; testGameState.deckBlue = new TriadDeckInstanceManual(new TriadDeck(screenGame.blueDeck)); testGameState.deckRed = new TriadDeckInstanceManual(testNpc.Deck); bool shouldForceBlueSelection = true; JsonParser.ArrayValue moveArr = rootOb.entries["moves"] as JsonParser.ArrayValue; for (int idx = 0; idx < moveArr.entries.Count; idx++) { var move = new VerifyMove(); move.Load(moveArr.entries[idx] as JsonParser.ObjectValue); if (debugMode) { Logger.WriteLine("move[{0}]: [{1}] {2}: {3}", idx, move.boardPos, move.owner, move.card); } if (move.owner == ETriadCardOwner.Blue) { CopyGameStateToScreen(testGameState, screenGame); if (shouldForceBlueSelection) { screenGame.forcedBlueCard = screenGame.blueDeck[move.cardIdx]; } screenMemory.OnNewScan(screenGame, testNpc); screenMemory.gameSolver.FindNextMove(screenMemory.gameState, out int solverCardIdx, out int solverBoardPos, out SolverResult bestChance); if (debugMode) { var solverTriadCard = testGameState.deckBlue.GetCard(solverCardIdx); Logger.WriteLine("solver: {0} -> board[{1}], chance: {2}", solverTriadCard.Name.GetCodeName(), solverBoardPos, bestChance.expectedResult); if (solverBoardPos != move.boardPos) { Logger.WriteLine(" >> MISMATCH!"); } } } testGameState.state = move.owner == ETriadCardOwner.Blue ? ETriadGameState.InProgressBlue : ETriadGameState.InProgressRed; testSession.PlaceCard(testGameState, move.card, move.owner, move.boardPos); } } } } } ================================================ FILE: sources/gamelogic/tests/TriadGameTests.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Diagnostics; namespace FFTriadBuddy { #if DEBUG public class TriadGameTests { private static Dictionary mapValidationRules; private class VerifyMove { private ETriadCardOwner[] expectedState; public TriadCard card; public ETriadCardOwner owner; public int boardPos; public void Load(JsonParser.ObjectValue configOb) { string ownerStr = configOb["player"] as JsonParser.StringValue; owner = (ownerStr == "blue") ? ETriadCardOwner.Blue : (ownerStr == "red") ? ETriadCardOwner.Red : ETriadCardOwner.Unknown; boardPos = configOb["pos"] as JsonParser.IntValue; if (configOb.entries.ContainsKey("board")) { string boardCode = configOb["board"] as JsonParser.StringValue; boardCode = boardCode.Replace(" ", ""); expectedState = new ETriadCardOwner[9]; for (int idx = 0; idx < expectedState.Length; idx++) { expectedState[idx] = (boardCode[idx] == 'R') ? ETriadCardOwner.Red : (boardCode[idx] == 'B') ? ETriadCardOwner.Blue : ETriadCardOwner.Unknown; } } var cardName = configOb["card"] as JsonParser.StringValue; if (cardName != null) { card = TriadCardDB.Get().Find(cardName); } else { var cardSides = configOb["card"] as JsonParser.ArrayValue; int numU = cardSides[0] as JsonParser.IntValue; int numL = cardSides[1] as JsonParser.IntValue; int numD = cardSides[2] as JsonParser.IntValue; int numR = cardSides[3] as JsonParser.IntValue; card = TriadCardDB.Get().Find(numU, numL, numD, numR); } } public bool VerifyState(TriadGameSimulationState gameState, bool debugMode) { if (expectedState != null) { for (int idx = 0; idx < expectedState.Length; idx++) { if (gameState.board[idx] == null && expectedState[idx] == ETriadCardOwner.Unknown) { continue; } if (gameState.board[idx].owner != expectedState[idx]) { if (debugMode) { string expectedCode = ""; string currentCode = ""; Func GetOwnerCode = (owner) => (owner == ETriadCardOwner.Blue) ? 'B' : (owner == ETriadCardOwner.Red) ? 'R' : '.'; for (int codeIdx = 0; codeIdx < 9; codeIdx++) { if (codeIdx == 3 || codeIdx == 6) { expectedCode += ' '; currentCode += ' '; } expectedCode += GetOwnerCode(gameState.board[codeIdx]?.owner ?? ETriadCardOwner.Unknown); currentCode += GetOwnerCode(expectedState[codeIdx]); } Logger.WriteLine("Failed, mismatch at [{0}]! Expected:{1}, got{2}", idx, expectedCode, currentCode); } return false; } } } return true; } } public static void RunTest(string configPath, bool debugMode) { string testName = System.IO.Path.GetFileNameWithoutExtension(configPath); string configText = System.IO.File.ReadAllText(configPath); JsonParser.ObjectValue configOb = JsonParser.ParseJson(configText); if (configOb["type"] != "Solver") { return; } // intial state ScannerTriad.VerifyConfig configData = new ScannerTriad.VerifyConfig(); configData.Load(configOb); if (mapValidationRules == null) { mapValidationRules = new Dictionary(); foreach (TriadGameModifier mod in ImageHashDB.Get().modObjects) { mapValidationRules.Add(mod.GetCodeName(), mod); } } List configMods = new List(); foreach (string modName in configData.rules) { configMods.Add(mapValidationRules[modName]); } TriadGameSimulation testSession = new TriadGameSimulation(); testSession.Initialize(configMods); TriadGameSimulationState testGameData = new TriadGameSimulationState() { bDebugRules = debugMode }; if (configData.board.Length > 0) { Func ConvertToTriadCard = configCard => { if (configCard.state == ScannerTriad.ECardState.None) { return null; } if (configCard.state == ScannerTriad.ECardState.Hidden) { return TriadCardDB.Get().hiddenCard; } TriadCard matchingCard = !string.IsNullOrEmpty(configCard.name) ? TriadCardDB.Get().Find(configCard.name) : TriadCardDB.Get().Find(configCard.sides[0], configCard.sides[1], configCard.sides[2], configCard.sides[3]); if (matchingCard == null) { string exceptionMsg = string.Format("Test {0} failed! Can't match validation card: '{1}' [{2},{3},{4},{5}]", testName, configCard.name, configCard.sides[0], configCard.sides[1], configCard.sides[2], configCard.sides[3]); throw new Exception(exceptionMsg); } return matchingCard; }; for (int idx = 0; idx < configData.board.Length; idx++) { var configState = configData.board[idx].state; if (configState != ScannerTriad.ECardState.None) { testGameData.board[idx] = new TriadCardInstance(ConvertToTriadCard(configData.board[idx]), (configState == ScannerTriad.ECardState.PlacedBlue) ? ETriadCardOwner.Blue : (configState == ScannerTriad.ECardState.PlacedRed) ? ETriadCardOwner.Red : ETriadCardOwner.Unknown); } } } var deckRed = new TriadDeck(); var deckBlue = new TriadDeck(); testGameData.deckBlue = new TriadDeckInstanceManual(deckBlue); testGameData.deckRed = new TriadDeckInstanceManual(deckRed); JsonParser.ArrayValue moveArr = configOb.entries["moves"] as JsonParser.ArrayValue; for (int idx = 0; idx < moveArr.entries.Count; idx++) { var move = new VerifyMove(); move.Load(moveArr.entries[idx] as JsonParser.ObjectValue); var useDeck = (move.owner == ETriadCardOwner.Blue) ? deckBlue : deckRed; useDeck.knownCards.Add(move.card); if (idx == 0) { testGameData.state = (move.owner == ETriadCardOwner.Blue) ? ETriadGameState.InProgressBlue : ETriadGameState.InProgressRed; } if (debugMode) { Logger.WriteLine("move[{0}]: [{1}] {2}: {3}", idx, move.boardPos, move.owner, move.card); } bool result = testSession.PlaceCard(testGameData, move.card, move.owner, move.boardPos); if (!result) { string exceptionMsg = string.Format("Test {0} failed! Can't place card!", testName); throw new Exception(exceptionMsg); } result = move.VerifyState(testGameData, debugMode); if (!result) { string exceptionMsg = string.Format("Test {0} failed! Finished with bad state!", testName); throw new Exception(exceptionMsg); } } } public static void RunSolverStressTest() { int numIterations = 1000 * 1000; Logger.WriteLine("Solver speed testing start, numIterations:" + numIterations); Stopwatch timer = new Stopwatch(); timer.Start(); TriadDeck testDeck = new TriadDeck(new int[] { 10, 20, 30, 40, 50 }); TriadNpc testNpc = TriadNpcDB.Get().Find("Garima"); var solver = new TriadGameSolver(); solver.InitializeSimulation(testNpc.Rules); var agent = new TriadGameAgentRandom(solver, 0); for (int Idx = 0; Idx < numIterations; Idx++) { var gameState = solver.StartSimulation(testDeck, testNpc.Deck, ETriadGameState.InProgressBlue); solver.RunSimulation(gameState, agent, agent); } timer.Stop(); Logger.WriteLine("Solver speed testing finished, time taken:" + timer.ElapsedMilliseconds + "ms"); if (Debugger.IsAttached) { Debugger.Break(); } } private static int[][] BuildDeckPermutations() { var permutationList = new int[120][]; int ListIdx = 0; for (int IdxP0 = 0; IdxP0 < 5; IdxP0++) { for (int IdxP1 = 0; IdxP1 < 5; IdxP1++) { if (IdxP1 == IdxP0) { continue; } for (int IdxP2 = 0; IdxP2 < 5; IdxP2++) { if (IdxP2 == IdxP0 || IdxP2 == IdxP1) { continue; } for (int IdxP3 = 0; IdxP3 < 5; IdxP3++) { if (IdxP3 == IdxP0 || IdxP3 == IdxP1 || IdxP3 == IdxP2) { continue; } for (int IdxP4 = 0; IdxP4 < 5; IdxP4++) { if (IdxP4 == IdxP0 || IdxP4 == IdxP1 || IdxP4 == IdxP2 || IdxP4 == IdxP3) { continue; } permutationList[ListIdx] = new int[5] { IdxP0, IdxP1, IdxP2, IdxP3, IdxP4 }; ListIdx++; } } } } } return permutationList; } private static int[][] deckPermutations; private static int[] PickRandomPermutation(Random rand) { if (deckPermutations == null) { deckPermutations = BuildDeckPermutations(); } return deckPermutations[rand.Next(deckPermutations.Length)]; } class SolverAccTestInfo { public TriadGameAgent agentBlue; public TriadGameAgent agentRed; public int numWins = 0; public int numControlled = 0; public float elapsedSeconds = 0.0f; public List predictionSteps; } private static void PlayTestGame(SolverAccTestInfo testInfo, TriadGameSolver solver, TriadGameSimulationState gameState, Random sessionRand) { int[] blueDeckOrder = solver.HasSimulationRule(ETriadGameSpecialMod.BlueCardSelection) ? PickRandomPermutation(sessionRand) : null; bool keepPlaying = true; while (keepPlaying) { if (gameState.state == ETriadGameState.InProgressBlue) { gameState.forcedCardIdx = (blueDeckOrder != null) ? blueDeckOrder[gameState.deckBlue.numPlaced] : -1; keepPlaying = testInfo.agentBlue.FindNextMove(solver, gameState, out int cardIdx, out int boardPos, out var blueResult); if (keepPlaying) { testInfo.predictionSteps[gameState.deckBlue.numPlaced] += blueResult.winChance; keepPlaying = solver.simulation.PlaceCard(gameState, cardIdx, gameState.deckBlue, ETriadCardOwner.Blue, boardPos); } } else if (gameState.state == ETriadGameState.InProgressRed) { gameState.forcedCardIdx = -1; keepPlaying = testInfo.agentRed.FindNextMove(solver, gameState, out int cardIdx, out int boardPos, out var dummyResult); if (keepPlaying) { keepPlaying = solver.simulation.PlaceCard(gameState, cardIdx, gameState.deckRed, ETriadCardOwner.Red, boardPos); } } else { keepPlaying = false; } } int numBlue = (gameState.deckBlue.availableCardMask != 0) ? 1 : 0; foreach (TriadCardInstance card in gameState.board) { numBlue += (card != null && card.owner == ETriadCardOwner.Blue) ? 1 : 0; } testInfo.numControlled += numBlue; testInfo.numWins += (gameState.state == ETriadGameState.BlueWins) ? 1 : 0; } public static void RunSolverAccuracyTests() { int numIterations = 200; var deckPermutations = BuildDeckPermutations(); Logger.WriteLine("Solver accuracy testing start, numIterations:" + numIterations); var deckRand = new Random(20); var deckCards = new int[5] { 61, 248, 113, 191, 87 }; var cardDB = TriadCardDB.Get(); int idx = 0; while (idx < deckCards.Length) { int cardIdx = deckRand.Next(cardDB.cards.Count); if (cardDB.cards[cardIdx].IsValid()) { deckCards[idx] = cardIdx; idx++; } } TriadDeck testDeck = new TriadDeck(deckCards); //TriadNpc testNpc = TriadNpcDB.Get().Find("Garima"); //TriadNpc testNpc = TriadNpcDB.Get().Find("Swift"); TriadNpc testNpc = TriadNpcDB.Get().Find("Aurifort of the Three Clubs"); var solver = new TriadGameSolver(); solver.InitializeSimulation(testNpc.Rules); var playerAgents = new List(); playerAgents.Add(new TriadGameAgentDerpyCarlo()); playerAgents.Add(new TriadGameAgentCarloTheExplorer()); playerAgents.Add(new TriadGameAgentCarloScored()); // single iteration: in depth testing for single agent if (numIterations == 1) { playerAgents.Clear(); playerAgents.Add(new TriadGameAgentCarloScored()); playerAgents[0].debugFlags = TriadGameAgent.DebugFlags.AgentInitialize | TriadGameAgent.DebugFlags.ShowMoveStart | TriadGameAgent.DebugFlags.ShowMoveDetails; } var testResults = new List(); foreach (var agent in playerAgents) { var testInfo = new SolverAccTestInfo() { agentBlue = agent, agentRed = new TriadGameAgentRandom() }; testInfo.agentBlue.Initialize(solver, 0); testInfo.agentRed.Initialize(solver, 0); testInfo.predictionSteps = new List(); for (idx = 0; idx < 5; idx++) { testInfo.predictionSteps.Add(0.0f); } testResults.Add(testInfo); } TriadGameAgentRandom.UseEqualDistribution = true; var iterCounter = 0; foreach (var testInfo in testResults) { var sessionRand = new Random(0); Stopwatch timer = new Stopwatch(); timer.Start(); for (int Idx = 0; Idx < numIterations; Idx++) { iterCounter++; if (iterCounter % 20 == 0) { Logger.WriteLine(">> {0}/{1}", iterCounter, numIterations * testResults.Count); } var initialState = solver.StartSimulation(testDeck, testNpc.Deck, ETriadGameState.InProgressRed); PlayTestGame(testInfo, solver, initialState, sessionRand); } timer.Stop(); testInfo.elapsedSeconds = timer.ElapsedMilliseconds / 1000.0f; } Logger.WriteLine("Solver accuracy testing finished"); foreach (var testInfo in testResults) { string predictionDesc = ""; for (idx = 0; idx < testInfo.predictionSteps.Count - 1; idx++) { if (predictionDesc.Length > 0) { predictionDesc += ", "; } predictionDesc += $"{(testInfo.predictionSteps[idx] / numIterations):P0}"; } Logger.WriteLine("[{0}] score:{1:P2}, control:{2:0.##}, time taken:{3}s, predictions:{4}", testInfo.agentBlue.agentName, (float)testInfo.numWins / numIterations, (float)testInfo.numControlled / numIterations, testInfo.elapsedSeconds, predictionDesc); } if (Debugger.IsAttached) { Debugger.Break(); } } private static TriadNpc PickRandomNpc(Random rand) { var npcList = TriadNpcDB.Get().npcs; TriadNpc npcOb = null; while (npcOb == null) { npcOb = npcList[rand.Next(npcList.Count)]; } return npcOb; } private static Dictionary> mapCardRarities; private static TriadCard PickRandomCard(Random rand, int rarity) { if (mapCardRarities == null) { mapCardRarities = new Dictionary>(); for (int idx = 0; idx < 5; idx++) { mapCardRarities.Add(idx, new List()); } var cardList = TriadCardDB.Get().cards; foreach (var card in cardList) { if (card != null && card.IsValid()) { mapCardRarities[(int)card.Rarity].Add(card); } } } var rarityList = mapCardRarities[rarity]; return rarityList[rand.Next(rarityList.Count)]; } private static TriadDeck PickRandomDeck(Random rand) { int[] rarityCount = new int[5]; int rarityType = rand.Next(10); if (rarityType < 2) { rarityCount[0] = 4; rarityCount[1] = 1; } else if (rarityType < 5) { rarityCount[0] = 1; rarityCount[1] = 2; rarityCount[2] = 1; rarityCount[3] = 1; rarityCount[4] = 0; } else { rarityCount[2] = 3; rarityCount[3] = 1; rarityCount[4] = 1; } var listCards = new List(); for (int idxRarity = 0; idxRarity < rarityCount.Length; idxRarity++) { int numAdded = 0; while (numAdded < rarityCount[idxRarity]) { var cardOb = PickRandomCard(rand, idxRarity); if (!listCards.Contains(cardOb)) { listCards.Add(cardOb); numAdded++; } } } return new TriadDeck(listCards); } public static void GenerateAccuracyTrainingData() { int numGames = 100; int numSamples = 200; int seed = 0; TriadGameAgentRandom.UseEqualDistribution = true; var rand = new Random(seed); var testLines = new List(); testLines.Add("seed,deck0,deck1,deck2,deck3,deck4,npc,winChance"); for (int idxSample = 0; idxSample < numSamples; idxSample++) { Logger.WriteLine($">> {idxSample + 1}/{numSamples}"); var sessionSeed = rand.Next(); var sessionRand = new Random(sessionSeed); var testNpc = PickRandomNpc(sessionRand); var testDeck = PickRandomDeck(sessionRand); var solver = new TriadGameSolver(); solver.InitializeSimulation(testNpc.Rules); var testInfo = new SolverAccTestInfo() { agentBlue = new TriadGameAgentCarloTheExplorer(), agentRed = new TriadGameAgentRandom() }; testInfo.agentBlue.Initialize(solver, 0); testInfo.agentRed.Initialize(solver, 0); testInfo.predictionSteps = new List(); for (int idx = 0; idx < 5; idx++) { testInfo.predictionSteps.Add(0.0f); } for (int idxGame = 0; idxGame < numGames; idxGame++) { var initialState = solver.StartSimulation(testDeck, testNpc.Deck, ETriadGameState.InProgressRed); PlayTestGame(testInfo, solver, initialState, sessionRand); } var winChance = 1.0f * testInfo.numWins / numGames; testLines.Add($"{sessionSeed},{testDeck.knownCards[0].Id},{testDeck.knownCards[1].Id},{testDeck.knownCards[2].Id},{testDeck.knownCards[3].Id},{testDeck.knownCards[4].Id},{testNpc.Id},{winChance}"); } System.IO.File.WriteAllLines("predictionDump.csv", testLines); } } #endif // DEBUG } ================================================ FILE: sources/googleapi/GoogleClientMissingIdentifiers.cs ================================================ using System; #warning "API keys not available, cloud storage access will be disabled" namespace MgAl2O4.GoogleAPI { public class GoogleClientIdentifiers { public static GoogleOAuth2.ClientIdentifier Keys = null; } } ================================================ FILE: sources/googleapi/GoogleDriveService.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; namespace MgAl2O4.GoogleAPI { // Google API package is doing awesome job, but.... it adds lots of dll dependecies // I want to keep program as dll free as it gets, so it means: REWRITE ALL THE THINGS! // (jk, I like doing stuff myself and learning how it works under the hood too much) public class GoogleDriveService { private static readonly string FilesApi = "https://www.googleapis.com/drive/v3/files"; private static readonly string UploadApi = "https://www.googleapis.com/upload/drive/v3/files"; public enum EState { NoErrors, NotInitialized, NotAuthorized, AuthInProgress, ApiFailure, } private readonly GoogleOAuth2.ClientIdentifier clientId; private GoogleOAuth2.Token authToken; private EState currentState; private string lastApiResponse; private Dictionary mapFileIds; private class Reply { public bool bIsSuccessful; public string contentBody; public HttpWebResponse response; } public GoogleDriveService(GoogleOAuth2.ClientIdentifier clientIdentifier, GoogleOAuth2.Token savedToken) { clientId = clientIdentifier; authToken = savedToken; mapFileIds = new Dictionary(); currentState = ((savedToken == null) || !savedToken.IsValidForRefresh()) ? EState.NotAuthorized : EState.NotInitialized; lastApiResponse = null; } public GoogleOAuth2.Token GetAuthToken() { return authToken; } public EState GetState() { return currentState; } public string GetLastApiResponse() { return lastApiResponse; } public int GetFileCount() { return mapFileIds.Count; } public async Task InitFileList() { string requestUri = FilesApi + "?list" + "&spaces=appDataFolder" + "&q=" + Uri.EscapeDataString("trashed=false"); bool bHasValidResponse = false; Reply reply = await HandleRequest("GET", requestUri); if (reply.bIsSuccessful) { JsonParser.ObjectValue jsonOb = JsonParser.ParseJson(reply.contentBody); if (jsonOb != null) { JsonParser.ArrayValue fileArr = (JsonParser.ArrayValue)jsonOb["files"]; foreach (JsonParser.Value entry in fileArr.entries) { JsonParser.ObjectValue entryOb = (JsonParser.ObjectValue)entry; string mapKey = entryOb["name"]; mapFileIds.Remove(mapKey); mapFileIds.Add(mapKey, entryOb["id"]); } bHasValidResponse = true; } else { // null response is still valid (no settings saved yet) bHasValidResponse = true; } } UpdateCurrentState(bHasValidResponse); } public async Task UploadTextFile(string fileName, string fileContent) { bool bResult = false; if (!mapFileIds.ContainsKey(fileName)) { string uploadRequestUri = UploadApi + "?uploadType=multipart"; string uploadMeta = "{\"name\":\"" + fileName + "\",parents:[\"appDataFolder\"]}"; Reply reply = await HandleRequest("POST", uploadRequestUri, uploadMeta, fileContent); if (reply.bIsSuccessful) { JsonParser.ObjectValue jsonOb = JsonParser.ParseJson(reply.contentBody); if (jsonOb != null) { string fileId = jsonOb["id"]; mapFileIds.Remove(fileName); mapFileIds.Add(fileName, fileId); bResult = true; } } } else { string patchRequestUri = UploadApi + "/" + mapFileIds[fileName] + "?uploadType=media"; Reply reply = await HandleRequest("PATCH", patchRequestUri, fileContent); bResult = reply.bIsSuccessful; } UpdateCurrentState(bResult); return bResult; } public async Task DownloadTextFile(string fileName) { if (!mapFileIds.ContainsKey(fileName)) { return null; } string getRequestUri = FilesApi + "/" + mapFileIds[fileName] + "?alt=media"; Reply reply = await HandleRequest("GET", getRequestUri); if (reply.bIsSuccessful) { UpdateCurrentState(true); return reply.contentBody; } UpdateCurrentState(false); return null; } private void UpdateCurrentState(bool bHasValidApiResponse) { if (currentState <= EState.NotInitialized && !bHasValidApiResponse) { currentState = EState.ApiFailure; } } private async Task HandleRequest(string method, string requestUri, params string[] requestBody) { Reply result = new Reply(); result.bIsSuccessful = false; currentState = EState.AuthInProgress; GoogleOAuth2.Token activeToken = await GoogleOAuth2.GetAuthorizationToken(clientId, authToken); if (activeToken != null && activeToken.IsValidForAuth()) { authToken = activeToken; currentState = EState.NoErrors; HttpWebRequest request = WebRequest.CreateHttp(requestUri); request.Method = method; request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + authToken.accessToken); SetRequestContent(request, requestBody); WebResponse rawResponse = null; try { rawResponse = await request.GetResponseAsync(); } catch (WebException ex) { lastApiResponse = ex.Message; } catch (Exception ex) { lastApiResponse = "Exception: " + ex; } HttpWebResponse response = (HttpWebResponse)rawResponse; result.response = response; if (response != null) { lastApiResponse = response.StatusDescription; if (response.StatusCode == HttpStatusCode.OK) { string responseBody = null; if (response.ContentLength > 0) { byte[] contentBytes = new byte[response.ContentLength]; Stream contentStream = response.GetResponseStream(); contentStream.Read(contentBytes, 0, contentBytes.Length); contentStream.Close(); responseBody = Encoding.UTF8.GetString(contentBytes); } result.bIsSuccessful = true; result.contentBody = responseBody; } } } else { currentState = EState.NotAuthorized; } return result; } private void SetRequestContent(HttpWebRequest request, string[] parts) { byte[] contentBytes = null; if (parts.Length == 0) { request.ContentType = "application/json; charset=UTF-8"; } else if (parts.Length == 1) { bool bJsonRequest = (parts[0].Length > 1) && (parts[0][0] == '{'); request.ContentType = (bJsonRequest ? "application/json" : "text/plain") + "; charset=UTF-8"; contentBytes = Encoding.UTF8.GetBytes(parts[0]); } else { string boundaryStr = "SPLITMEHERE"; request.ContentType = "multipart/related; boundary=" + boundaryStr; MemoryStream memoryStream = new MemoryStream(); foreach (string str in parts) { bool bJsonRequest = (str.Length > 1) && (str[0] == '{'); string header = (memoryStream.Position == 0 ? "" : "\r\n") + "--" + boundaryStr + "\r\nContent-Type: " + (bJsonRequest ? "application/json" : "text/plain") + "; charset=UTF-8\r\n\r\n"; byte[] headerBytes = Encoding.UTF8.GetBytes(header); memoryStream.Write(headerBytes, 0, headerBytes.Length); byte[] partBytes = Encoding.UTF8.GetBytes(str); memoryStream.Write(partBytes, 0, partBytes.Length); } string footer = "\r\n--" + boundaryStr + "--"; byte[] footerBytes = Encoding.UTF8.GetBytes(footer); memoryStream.Write(footerBytes, 0, footerBytes.Length); contentBytes = new byte[memoryStream.Length]; memoryStream.Position = 0; memoryStream.Read(contentBytes, 0, contentBytes.Length); memoryStream.Close(); } request.ContentLength = (contentBytes != null) ? contentBytes.Length : 0; if (contentBytes != null) { Stream contentStream = request.GetRequestStream(); contentStream.Write(contentBytes, 0, contentBytes.Length); contentStream.Close(); } } } } ================================================ FILE: sources/googleapi/GoogleOAuth2.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace MgAl2O4.GoogleAPI { // Google API package is doing awesome job, but.... it adds lots of dll dependecies // I want to keep program as dll free as it gets, so it means: REWRITE ALL THE THINGS! // (jk, I like doing stuff myself and learning how it works under the hood too much) // // source of API and params: Google OAuth 2.0 Playground public class GoogleOAuth2 { public class Token { public DateTime expirationDate; public string accessToken; public string refreshToken; public bool IsValidForAuth() { return !string.IsNullOrEmpty(accessToken) && (expirationDate != null) && (DateTime.Now.CompareTo(expirationDate) < 0); } public bool IsValidForRefresh() { return !string.IsNullOrEmpty(refreshToken); } public override string ToString() { return "AccessToken:" + accessToken + ", RefreshToken:" + refreshToken + ", ExpirationDate:" + expirationDate; } } public class ClientIdentifier { public override string ToString() { return "ID:" + GetID() + ", Secret:" + GetSecret(); } public virtual string GetID() { return ""; } public virtual string GetSecret() { return ""; } } private static readonly string RequestAccessApi = "https://accounts.google.com/o/oauth2/v2/auth"; private static readonly string TokenApi = "https://www.googleapis.com/oauth2/v4/token"; //private static readonly string AccessScopeOwnedFiles = "https://www.googleapis.com/auth/drive.file"; private static readonly string AccessScopeAppSettings = "https://www.googleapis.com/auth/drive.appdata"; private static HttpListener httpListener = new HttpListener(); public static void KillPendingAuthorization() { if (httpListener != null && httpListener.IsListening) { httpListener.Close(); httpListener = null; } } public static async Task GetAuthorizationToken(ClientIdentifier clientIdentifier, Token savedTokenData) { if (clientIdentifier == null) { return null; } if (savedTokenData != null) { if (savedTokenData.IsValidForAuth()) { return savedTokenData; } else if (savedTokenData.IsValidForRefresh()) { Token refreshedToken = await RefreshToken(clientIdentifier, savedTokenData); return refreshedToken; } } KillPendingAuthorization(); Token newToken = await RequestToken(clientIdentifier); return newToken; } private static async Task RequestToken(ClientIdentifier clientIdentifier) { // prepare listen server for receiving token data if (!HttpListener.IsSupported || clientIdentifier == null) { return null; } int listenPort = FindListenPort(); string authListenUri = "http://localhost:" + listenPort + "/auth_response/"; httpListener = new HttpListener(); httpListener.Prefixes.Add(authListenUri); httpListener.Start(); // send authorization request: open in default browser string authRequestUri = RequestAccessApi + "?redirect_uri=" + Uri.EscapeDataString(authListenUri) + "&prompt=consent" + "&response_type=code" + "&client_id=" + clientIdentifier.GetID() + "&scope=" + Uri.EscapeDataString(AccessScopeAppSettings) + "&access_type=offline"; Process.Start(authRequestUri); // wait for reponse string authorizationCode = null; { HttpListenerContext listenContext = null; try { listenContext = await Task.Factory.FromAsync(httpListener.BeginGetContext(null, null), httpListener.EndGetContext); } catch (Exception) { listenContext = null; } if (listenContext != null) { Uri requestUrl = listenContext.Request.Url; ILookup queryLookup = requestUrl.Query.TrimStart('?') .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) .Select(k => k.Split('=')) .Where(k => k.Length == 2) .ToLookup(a => a[0], a => Uri.UnescapeDataString(a[1]), StringComparer.OrdinalIgnoreCase); authorizationCode = queryLookup["code"].FirstOrDefault(); string responseString = "Google account verificationReceived verification code. You may now close this window."; byte[] buffer = Encoding.UTF8.GetBytes(responseString); HttpListenerResponse listenResponse = listenContext.Response; listenResponse.ContentLength64 = buffer.Length; listenResponse.OutputStream.Write(buffer, 0, buffer.Length); listenResponse.OutputStream.Close(); } } // send token grant request: no user ui needed Token resultToken = null; if (!string.IsNullOrEmpty(authorizationCode)) { HttpContent tokenGrantContent = new FormUrlEncodedContent(new[] { new KeyValuePair("code", authorizationCode), new KeyValuePair("redirect_uri", authListenUri), new KeyValuePair("client_id", clientIdentifier.GetID()), new KeyValuePair("client_secret", clientIdentifier.GetSecret()), new KeyValuePair("scope", AccessScopeAppSettings), new KeyValuePair("grant_type", "authorization_code"), }); tokenGrantContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); try { HttpClient client = new HttpClient(); HttpResponseMessage tokenGrantResponse = await client.PostAsync(TokenApi, tokenGrantContent); if (tokenGrantResponse.IsSuccessStatusCode) { string replyJson = await tokenGrantResponse.Content.ReadAsStringAsync(); resultToken = CreateToken(replyJson); } } catch (Exception) { } } if (httpListener != null && httpListener.IsListening) { httpListener.Close(); httpListener = null; } return resultToken; } private static async Task RefreshToken(ClientIdentifier clientIdentifier, Token tokenData) { Token resultToken = null; HttpContent requestContent = new FormUrlEncodedContent(new[] { new KeyValuePair("client_secret", clientIdentifier.GetSecret()), new KeyValuePair("grant_type", "refresh_token"), new KeyValuePair("refresh_token", tokenData.refreshToken), new KeyValuePair("client_id", clientIdentifier.GetID()), }); requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); try { HttpClient client = new HttpClient(); HttpResponseMessage response = await client.PostAsync(TokenApi, requestContent); if (response.IsSuccessStatusCode) { string replyJson = await response.Content.ReadAsStringAsync(); resultToken = CreateToken(replyJson); resultToken.refreshToken = tokenData.refreshToken; } } catch (Exception) { } return resultToken; } private static Token CreateToken(string jsonStr) { JsonParser.ObjectValue jsonOb = JsonParser.ParseJson(jsonStr); int validForSec = (JsonParser.IntValue)jsonOb["expires_in"]; return new Token { accessToken = jsonOb["access_token"], refreshToken = jsonOb["refresh_token", JsonParser.StringValue.Empty], expirationDate = DateTime.Now.AddSeconds(validForSec) }; } private static int FindListenPort() { TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0); tcpListener.Start(); int port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; tcpListener.Stop(); return port; } } } ================================================ FILE: sources/loc/strings.cs ================================================ namespace FFTriadBuddy.loc { public class strings { // keep default values in for editor public static string AdjustForm_CancelButton = "Cancel"; public static string AdjustForm_CardDown = "D:"; public static string AdjustForm_CardLeft = "L:"; public static string AdjustForm_CardList = "Adjusted:"; public static string AdjustForm_CardRight = "R:"; public static string AdjustForm_CardStatus = "Status:"; public static string AdjustForm_CardUp = "U:"; public static string AdjustForm_Current = "Current:"; public static string AdjustForm_Distance = "Distance:"; public static string AdjustForm_Dynamic_CardState_None = "None"; public static string AdjustForm_Dynamic_CardState_Hidden = "Hidden"; public static string AdjustForm_Dynamic_CardState_Locked = "Locked"; public static string AdjustForm_Dynamic_CardState_Visible = "Visible"; public static string AdjustForm_Dynamic_CardState_PlacedRed = "Placed: Red"; public static string AdjustForm_Dynamic_CardState_PlacedBlue = "Placed: Blue"; public static string AdjustForm_Dynamic_Digit_Default = "auto"; public static string AdjustForm_Dynamic_Digit_Override = "custom"; public static string AdjustForm_Dynamic_Distance_Classifier = "<= Classifier based"; public static string AdjustForm_Dynamic_Distance_DefaultHint = "<= Lower is more accurate"; public static string AdjustForm_Dynamic_Distance_Exact = "<= Exact match"; public static string AdjustForm_Dynamic_Distance_NotAvail = "N/A"; public static string AdjustForm_Dynamic_UnknownOwner = "unknown"; public static string AdjustForm_HashList = "Detect:"; public static string AdjustForm_SaveButton = "Save"; public static string AdjustForm_Title = "Adjust detection"; public static string App_Title = "FF Triad Buddy"; public static string DeckCtrl_CtxMenu_LockForOptimization = "Lock card for deck optimization"; public static string DeckCtrl_CtxMenu_PickCard = "Pick card:"; public static string DeckCtrl_CtxMenu_UseOnlyOwned = "Use only owned cards (click to change)"; public static string FavDeckCtrl_Edit = "Edit"; public static string FavDeckForm_AddButton = "Add"; public static string FavDeckForm_Dynamic_AutoName = "Fav #{0}"; public static string FavDeckForm_Dynamic_RemoveMsg = "Favorite deck will be removed, do you want to continue?"; public static string FavDeckForm_Dynamic_UpdateButton = "Update"; public static string FavDeckForm_Info = "Favorite deck, click card for options:"; public static string FavDeckForm_Name = "Short name:"; public static string FavDeckForm_RemoveButton = "Remove"; public static string FavDeckForm_Title = "Edit favorite deck"; public static string Game_ProcessName_DX11 = "ffxiv_dx11"; public static string Game_ProcessName_DX9 = "ffxiv"; public static string Game_WindowNamePrefix = "FINAL FANTASY"; public static string LocalSaves_Export = "Export settings"; public static string LocalSaves_Import = "Import settings"; public static string LocalSaves_ShowBackupFolder = "Show backups"; public static string LocalSaves_Title = "Local saves"; public static string MainForm_Cards_IconsTitle = "Icons"; public static string MainForm_Cards_ListTitle = "List"; public static string MainForm_Cards_List_ColumnId = "#"; public static string MainForm_Cards_List_ColumnName = "Name"; public static string MainForm_Cards_List_ColumnOwned = "Owned"; public static string MainForm_Cards_List_ColumnPower = "Power"; public static string MainForm_Cards_List_ColumnRarity = "Rarity"; public static string MainForm_Cards_List_ColumnType = "Type"; public static string MainForm_Cards_NumOwned = "Number of owned cards:"; public static string MainForm_Cards_Title = "Cards"; public static string MainForm_CtxMenu_CardInfo_FindOnline = "Find online"; public static string MainForm_CtxMenu_CardInfo_NpcReward = "Npc reward:"; public static string MainForm_CtxMenu_FindCard = "Find card:"; public static string MainForm_CtxMenu_FindNpc = "Find npc:"; public static string MainForm_CtxMenu_Learn_Adjust = "Adjust..."; public static string MainForm_CtxMenu_Learn_Delete = "Delete"; public static string MainForm_CtxMenu_SelectNpc_Rewards = "Rewards (click to add/remove)"; public static string MainForm_CtxMenu_SelectNpc_Select = "Select npc to play"; public static string MainForm_Dynamic_CardOwnedColumn = "Yes"; public static string MainForm_Dynamic_DeckState_HasDuplicates = "Found duplicate cards!"; public static string MainForm_Dynamic_DeckState_MissingCards = "Missing cards!"; public static string MainForm_Dynamic_DeckState_TooMany4Star = "More than two 4+ star!"; public static string MainForm_Dynamic_DeckState_TooMany5Star = "Only one 5 star allowed!"; public static string MainForm_Dynamic_NpcCompletedColumn = "Yes"; public static string MainForm_Dynamic_RuleListEmpty = "(none)"; public static string MainForm_Dynamic_Screenhot_Status_NoErrors = "Active"; public static string MainForm_Dynamic_Screenshot_BoardRow_Bottom = "bot"; public static string MainForm_Dynamic_Screenshot_BoardRow_Middle = "mid"; public static string MainForm_Dynamic_Screenshot_BoardRow_Top = "top"; public static string MainForm_Dynamic_Screenshot_CardLocation_BlueDeck = "Blue {0}"; public static string MainForm_Dynamic_Screenshot_CardLocation_Board = "Board {0} {1}"; public static string MainForm_Dynamic_Screenshot_CardLocation_RedDeck = "Red {0}"; public static string MainForm_Dynamic_Screenshot_CardNotDetected = "not detected!"; public static string MainForm_Dynamic_Screenshot_HashType_Card = "Card"; public static string MainForm_Dynamic_Screenshot_HashType_Number = "Number"; public static string MainForm_Dynamic_Screenshot_HashType_Rule = "Rule"; public static string MainForm_Dynamic_Screenshot_MatchType_Auto = "Auto"; public static string MainForm_Dynamic_Screenshot_MatchType_Exact = "Exact"; public static string MainForm_Dynamic_Screenshot_MatchType_Similar = "Similar"; public static string MainForm_Dynamic_Screenshot_SelectDetectionMatch = "(select match)"; public static string MainForm_Dynamic_Screenshot_Status_Disabled = "Disabled"; public static string MainForm_Dynamic_Screenshot_Status_FailedCardMatching = "Failed to recognize some of cards! Game state won't be accurate"; public static string MainForm_Dynamic_Screenshot_Status_MissingGridOrCards = "Can't find board! Try resetting UI position, turning off reshade, etc..."; public static string MainForm_Dynamic_Screenshot_Status_UnknownHash = "Can't recognize pattern! See details below"; public static string MainForm_Dynamic_Setup_CloudSave_Loaded = "Cloud save applied"; public static string MainForm_Dynamic_Setup_CloudStatus_ApiFailure = "API call failed"; public static string MainForm_Dynamic_Setup_CloudStatus_AuthInProgress = "authorizing..."; public static string MainForm_Dynamic_Setup_CloudStatus_Disabled = "disabled, local only"; public static string MainForm_Dynamic_Setup_CloudStatus_NoDatabase = "database failure"; public static string MainForm_Dynamic_Setup_CloudStatus_NoErrors = "active"; public static string MainForm_Dynamic_Setup_CloudStatus_NotAuthorized = "Auth required"; public static string MainForm_Dynamic_Setup_CloudStatus_NotInitialized = "scanning..."; public static string MainForm_Dynamic_Setup_CloudStatus_Synced = "synchronized"; public static string MainForm_Dynamic_Setup_CloudStatus_Uploaded = "stored"; public static string MainForm_Dynamic_Setup_OptimizerProgressAborted = "aborted at {0:P0}"; public static string MainForm_Dynamic_Simulate_BlueStartButton = "Blue starts"; public static string MainForm_Dynamic_Simulate_ChanceIsDraw = "(DRAW)"; public static string MainForm_Dynamic_Simulate_ChangeBlueHint = "Click to change card"; public static string MainForm_Dynamic_Simulate_EndGame_BlueDraw = "draw"; public static string MainForm_Dynamic_Simulate_EndGame_BlueLost = "lost"; public static string MainForm_Dynamic_Simulate_EndGame_BlueWin = "yay!"; public static string MainForm_Dynamic_Simulate_LastCardHint = "[Place last red card to trigger modifier]"; public static string MainForm_Dynamic_Simulate_RedStartButton = "Red start: drag & drop card on board"; public static string MainForm_Dynamic_Simulate_SwapRuleButton = "Select one card from each deck to swap"; public static string MainForm_Info_HomePage = "Project home page:"; public static string MainForm_Info_BugReports = "Bug reports:"; public static string MainForm_Info_Localization = "UI localization:"; public static string MainForm_Info_Title = "Info & Settings"; public static string MainForm_Info_TranslatorLove = "Thanks to everyone who contributed their translations! You're awesome!"; public static string MainForm_Info_TranslatorNeeded = "( volunteers still needed ;) )"; public static string MainForm_Npcs_List_ColumnCompleted = "Completed"; public static string MainForm_Npcs_List_ColumnLocation = "Location"; public static string MainForm_Npcs_List_ColumnName = "Name"; public static string MainForm_Npcs_List_ColumnPower = "Power"; public static string MainForm_Npcs_List_ColumnReward = "Reward"; public static string MainForm_Npcs_List_ColumnRules = "Rules"; public static string MainForm_Npcs_NumKnown = "Number of npcs with cards:"; public static string MainForm_Npcs_Title = "Npcs"; public static string MainForm_Screenshot_CurrentState = "Current state:"; public static string MainForm_Screenshot_History_CardsColumnDetection = "Last detection"; public static string MainForm_Screenshot_History_CardsColumnSides = "Numbers [U-L-D-R]"; public static string MainForm_Screenshot_History_CardsColumnType = "Type"; public static string MainForm_Screenshot_History_HashColumnDetection = "Last detection"; public static string MainForm_Screenshot_History_HashColumnType = "Type"; public static string MainForm_Screenshot_InfoLines = "Rules:\n" + "- game must be in Windowed or Borderless Windowed mode\n" + "- UI: set scale to 100%, use dark scheme\n" + "- npc must be selected\n" + "- capture during Blue turn, after all card animations finished(!!!)\n" + "\nGamepad: press both trigger buttons to capture\n" + "\nWorks with Mini Cactpot too :D"; public static string MainForm_Screenshot_Learn_DetectList = "Detect:"; public static string MainForm_Screenshot_Learn_DiscardAllButton = "Discard All"; public static string MainForm_Screenshot_Learn_DiscardAllInfo = "<< Resets scanner state"; public static string MainForm_Screenshot_Learn_PendingPlural = "(there are {0} more pending)"; public static string MainForm_Screenshot_Learn_PendingSingular = "(there is 1 more pending)"; public static string MainForm_Screenshot_Learn_SaveButton = "Save"; public static string MainForm_Screenshot_Learn_SourceImage = "Source image:"; public static string MainForm_Screenshot_Learn_Type = "Type:"; public static string MainForm_Screenshot_ListHint = "^ Right click on lists for options"; public static string MainForm_Screenshot_RemovePatternsButton = "Remove local patterns"; public static string MainForm_Screenshot_RemovePatternsTitle = "Use to clear all custom patterns and start from scratch:"; public static string MainForm_Screenshot_Title = "Play: Screenshot"; public static string MainForm_Setup_Cloud_AuthButton = "Login:"; public static string MainForm_Setup_Cloud_Desc = "Save in Google:"; public static string MainForm_Setup_Deck_OptimizeAbortButton = "Abort"; public static string MainForm_Setup_Deck_OptimizeStartButton = "Optimize deck for NPC"; public static string MainForm_Setup_Deck_Title = "Active deck, click card for options:"; public static string MainForm_Setup_Fav_AddSlotButton = "Add favorite deck slot"; public static string MainForm_Setup_NPC = "NPC:"; public static string MainForm_Setup_NPC_DeckPower = "Deck power:"; public static string MainForm_Setup_NPC_Location = "Location:"; public static string MainForm_Setup_NPC_Rules = "Rules:"; public static string MainForm_Setup_NPC_WinChance = "Win chance:"; public static string MainForm_Setup_OptimizerStats_NumOwned = "Num owned cards:"; public static string MainForm_Setup_OptimizerStats_NumPossible = "Num possible decks:"; public static string MainForm_Setup_OptimizerStats_NumTested = "Num tested decks:"; public static string MainForm_Setup_OptimizerStats_Progress = "Progress:"; public static string MainForm_Setup_OptimizerStats_TimeLeft = "Time left:"; public static string MainForm_Setup_OptimizeStats_Title = "Optimization stats:"; public static string MainForm_Setup_RulesToggle = "Toggle npc rules, disable for tournament"; public static string MainForm_Setup_Rules_Region1 = "Region rule 1:"; public static string MainForm_Setup_Rules_Region2 = "Region rule 2:"; public static string MainForm_Setup_Rules_Tournament = "Tournament:"; public static string MainForm_Setup_Rules_TournamentRules = "Rules:"; public static string MainForm_Setup_Title = "Setup"; public static string MainForm_Simulate_Debug_ForceCached = "Force using cached image"; public static string MainForm_Simulate_Debug_Info = "Take game screenshot: DEBUG mode"; public static string MainForm_Simulate_Game_ApplyRuleButton = "Apply rule"; public static string MainForm_Simulate_Game_KnownCards = "Known cards:"; public static string MainForm_Simulate_Game_ListHint = "[Drag & drop cards on board]"; public static string MainForm_Simulate_Game_SkipRuleButton = "Skip"; public static string MainForm_Simulate_Game_SpecialRule = "Special Rule >>"; public static string MainForm_Simulate_Game_UnknownCards = "Unknown cards remaining: {0}"; public static string MainForm_Simulate_Open_Hint = "Select visible cards:"; public static string MainForm_Simulate_Random_Info = "Adjust randomized deck :("; public static string MainForm_Simulate_ResetButton = "Reset"; public static string MainForm_Simulate_Roulette_Rule1 = "Pick roulette rule #1"; public static string MainForm_Simulate_Roulette_Rule2 = "Pick roulette rule #2"; public static string MainForm_Simulate_Roulette_Rule3 = "Pick roulette rule #3"; public static string MainForm_Simulate_Roulette_Rule4 = "Pick roulette rule #4"; public static string MainForm_Simulate_RuleList = "Current rules:"; public static string MainForm_Simulate_Title = "Play: Simulate"; public static string MainForm_Simulate_UndoRedMoveButton = "Undo last Red move"; public static string MainForm_Simulate_WinChance = "Win chance:"; public static string MainForm_UpdateNotify = "New version downloaded, please restart program to finish update. Click here to hide."; public static string OverlayForm_Capture_AutoScan = "Auto scan"; public static string OverlayForm_Capture_Button = "Capture"; public static string OverlayForm_Capture_Details = "Show details"; public static string OverlayForm_Capture_Status = "Status"; public static string OverlayForm_CardInfo_Swapped = "SWAPPED"; public static string OverlayForm_DeckInfo_Mismatch = "Blue deck is not matching Setup page! Solver needs to observe a few games to guess correct one."; public static string OverlayForm_Details_Npc = "NPC: {0}"; public static string OverlayForm_Details_RedDeck = "Red deck details:"; public static string OverlayForm_Details_RedInfo = "< Potential red cards, not including visible ones below:"; public static string OverlayForm_Details_RedPlacedAll = "All placed: {0}"; public static string OverlayForm_Details_RedPlacedVariable = "Var placed: {0}"; public static string OverlayForm_Details_Rules = "Rules: {0}"; public static string OverlayForm_Details_ScanId = "Scan Id: {0}"; public static string OverlayForm_Dynamic_NpcUnknown = "unknown"; public static string OverlayForm_Dynamic_RulesWaiting = "npc changed, waiting for scan..."; public static string OverlayForm_Dynamic_Status_ActiveTurn = "Ready (active turn!)"; public static string OverlayForm_Dynamic_Status_AutoScanMouseOverBoard = "Move cursor away from scan zone!"; public static string OverlayForm_Dynamic_Status_CactpotReady = "Mini cactpot: Ready"; public static string OverlayForm_Dynamic_Status_FailedCardMatching = "Unknown cards! Check Play:Screenshot for details"; public static string OverlayForm_Dynamic_Status_MissingCards = "Can't find blue deck"; public static string OverlayForm_Dynamic_Status_MissingGameProcess = "Game is not running"; public static string OverlayForm_Dynamic_Status_MissingGameWindow = "Can't find game window"; public static string OverlayForm_Dynamic_Status_MissingGrid = "Can't find board"; public static string OverlayForm_Dynamic_Status_NoInputImage = "Can't retrieve image to analyze"; public static string OverlayForm_Dynamic_Status_NoScannerMatch = "Can't find minigame window"; public static string OverlayForm_Dynamic_Status_Ready = "Ready"; public static string OverlayForm_Dynamic_Status_ScannerErrors = "Failed to recognize minigame"; public static string OverlayForm_Dynamic_Status_UnknownHash = "Unknown pattern! Check Play:Screenshot for details"; public static string OverlayForm_Dynamic_Status_WaitingForTurn = "Waiting for blue turn"; public static string Settings_AlwaysOnTop = "Always on top"; public static string Settings_AlwaysSmallIcons = "Always use small icons"; public static string Settings_DisableHardwareAcceleration = "Disable hardware acceleration"; public static string Settings_FontSize = "Font size"; public static string Settings_MarkerDurationCard = "Marker duration: card"; public static string Settings_MarkerDurationSwap = "Marker duration: swap"; public static string Settings_MarkerDurationCactpot = "Marker duration: cactpot"; public static string Settings_SkipOptionalSimulateRules = "Skip optional rules in simulate"; public static string Settings_Title = "Settings"; } } ================================================ FILE: sources/loc/strings.de.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Abbrechen D: L: Angepasst: R: Status: U: Aktuell: Entfernung: Ausgeblendet Gesperrt Nichts Platziert: Blau Platziert: Rot Sichtbar automatisch Benutzerdefiniert <= Klassifikatorbasiert <= Niedriger ist genau <= Exakte Übereinstimmung k.A. unbekannt Erkennen: Speichern Erkennung anpassen FF Triad Buddy Karte für Deck-Optimierung sperren Wähle Karte: Nur eigene Karten verwenden (zum Ändern klicken) Bearbeiten Hinzufügen Fav #{0} Favoriten Deck wird entfernt, möchtest du fortfahren? Akutalisieren Bevorzugtes Deck, für Optionen Karte anklicken: Kurzname: Entfernen Favoriten bearbeiten ffxiv_dx11 ffxiv FINALE FANTASIE Einstellungen exportieren Einstellungen importieren Backups anzeigen Lokale Speicherung Symbole Liste # Name Im Besitz Power Seltenheit Typ Anzahl der eigenen Karten: Karten Online finden NPC Belohnung: Karte finden: NPC finden: Anpassen... Löschen Belohnungen (Zum Hinzufügen/Entfernen klicken) Wähle NPC zum Starten Ja Doppelte Karten gefunden! Fehlende Karten! Mehr als zwei 4+ Sterne! Nur ein 5 Sterne erlaubt! Ja (keine) Aktiv bot mid top Blau {0} Brett {0} {1} Rot {0} nicht erkannt! Karte Nummer Regel Automatisch Exakt Ähnlich (Match auswählen) Deaktiviert Einige Karten konnten nicht erkannt werden! Der Spielstatus ist nicht korrekt Kann das Board nicht finden! Versuche die UI-Position zurückzusetzen, deaktiviere das reshade, etc... Muster kann nicht erkannt werden! Siehe Details unten Cloudspeicherung angewendet API-Zugriff fehlgeschlagen autorisieren... deaktiviert, nur lokal Datenbankfehler aktiv Auth erforderlich Scannen... Synchronisiert gespeichert abgebrochen um {0:P0} Blau beginnt (ZIEH) Klicken, um die Karte zu ändern unentschieden verloren Juhu! [Legen Sie die letzte rote Karte, um den Modifikator auszulösen] Rot startet: Karte per Drag & Drop auf das Board Wählen Sie eine Karte von jedem Deck zum Tausch Fehlermeldungen: Projekt Homepage: UI Lokalisierung: Info & Einstellungen Vielen Dank an alle, die ihre Übersetzungen beigesteuert haben! Ihr seid großartig! ( Freiwillige werden noch gesucht ;) ) Abgeschlossen Standort Name Power Belohnung Regeln Anzahl der NPCs mit Karten: NPCs Aktueller Status Letzte Erfassung Nummern [U-L-D-R] Typ Letzte Erfassung Typ Regeln: - Das Spiel muss im Fenstermodus oder im randlosen Fenstermodus sein. - UI: Skalierung auf 100% setzen, dunkles Schema verwenden - NPC muss ausgewählt sein - Aufnahme während des blauen Zuges, nachdem alle Kartenanimationen abgeschlossen sind (!!!) Gamepad: Drücken Sie beide Trigger-Tasten zum Erfassen Funktioniert auch mit Mini Cactpot :D Ermittelt: Alle Verwerfen << Setzt den Status des Scanners zurück (es sind {0} weitere ausstehend) (weitere 1 steht noch aus) Speichern Quellbild: Typ: ^ Rechtsklicken Sie auf Listen für Optionen Lokale Muster entfernen Verwenden Sie diese Option, um alle benutzerdefinierten Muster zu löschen und von vorne zu beginnen: Spielen: Screenshot Login: In Google speichern: Abbrechen Deck für NPC optimieren Aktives Deck, für Optionen Karte anklicken: Favoriten Deck-Slot hinzufügen NPC: Deck Power: Standort: Regeln: Gewinnchance: Anzahl der Karten im Besitz: Anzahl möglicher Decks: Anzahl getesteter Decks: Fortschritt: Verbleibende Zeit: Optimierungsstatus: NPC-Regeln ein-/ausschalten für Turnier Regionalregel 1: Regionalregel 2: Turnier: Regeln: Konfiguration Erzwinge mit zwischengespeichertem Bild Machen Sie einen Screenshot vom Spiel: DEBUG-Modus Regel anwenden Bekannte Karten: [Karten auf das Board ziehen und ablegen] Überspringen Sonderregel >> Unbekannte Karten verbleibend: {0} Sichtbare Karten auswählen: Zufälliges Deck anpassen :( Zurücksetzen Wähle Roulette Regel #1 aus Wähle Roulette Regel #2 aus Wähle Roulette Regel #3 aus Wählen Sie Roulette-Regel #4 Aktuelle Regeln: Spielen: Simulieren Letzten roten Zug rückgängig machen Gewinnchance: Neue Version heruntergeladen, bitte starten Sie das Programm neu, um das Update abzuschließen. Klicken Sie hier, um es auszublenden. Automatischer Scan Erfassen Details anzeigen Status GETAUSCHT Das blaue Deck passt nicht zur Setup-Seite! Der Solver muss ein paar Spiele beobachten, um das richtige Spiel zu erraten. NPC: {0} Details zum roten Deck < Mögliche rote Karten, ohne die unten sichtbaren: Alle platziert: {0} Var platziert: {0} Regeln: {0} Scan-ID: {0} unbekannt NPC geändert, warten auf Scan... Bereit (aktiver Zug!) Bewegen Sie den Cursor aus dem Scanbereich heraus! Mini Cactpot: Bereit Unbekannte Karten! Überprüfe Play:Screenshot für Details Blaues Deck kann nicht gefunden werden Spiel läuft nicht! Spielfenster nicht gefunden Board nicht gefunden Bild kann nicht zur Analyse abgerufen werden Minigame Fenster nicht gefunden Bereit Minispiel konnte nicht erkannt werden Unbekanntes Muster! Siehe Play:Screenshot für Details Warten auf blauen Zug Immer im Vordergrund Immer kleine Symbole verwenden Hardwarebeschleunigung deaktivieren Schriftgröße Markierungsdauer: cactpot Markierungsdauer: Karte Markierungsdauer: Swap Optionale Regeln in Simulation überspringen Einstellungen ================================================ FILE: sources/loc/strings.es.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Cancelar D: L: Ajustado: R: Estado: U: Actual: Distancia: Oculto Bloqueado Ninguno Colocado: Azul Colocado: Rojo Visible auto personalizado <= Basado en la clasificación <= El inferior es más preciso <= Partida exacta N/A desconocido Detectar: Guardar Ajuste de detección FF Triad Buddy Bloquear carta para optimizar el mazo Elegir carta: Usar sólo cartas propias (clic para cambiar) Editar Agregar Fav #{0} Se eliminará el mazo favorito, ¿quieres continuar? Actualizar Mazo favorito, haz clic en una carta para opciones: Nombre corto: Eliminar Editar mazo favorito ffxiv_dx11 ffxiv FINAL FANTASY Exportar configuración Importar configuración Mostrar backups Guardados locales Íconos Lista # Nombre En propiedad Poder Rareza Tipo Número de cartas poseídas: Cartas Buscar en línea Recompensa de PNJ: Buscar carta: Buscar PNJ: Ajustar... Eliminar Recompensas (haga clic para añadir/eliminar) Selecciona este PNJ para reproducir Si ¡Se han encontrado cartas duplicadas! ¡Faltan cartas! ¡Más de dos 4+ estrellas! ¡Sólo se permite una 5 estrellas! Si (ninguno) Activo interior central superior Azul {0} Tablero {0} {1} Rojo {0} ¡no detectado! Carta Número Regla Auto Exacto Similar (seleccionar partida) Desactivado ¡Error al reconocer algunas cartas! El estado del juego no será preciso ¡No se puede encontrar el tablero! Intenta restablecer la posición de la interfaz de usuario, apagando el rehade, etc... ¡No se puede reconocer el patrón! Ver los detalles a continuación Guardado en la nube aplicado Falló la llamada API autorizando... desactivado, solo local error de base de datos activo Se requiere autenticación escaneando... sincronizado almacenado abortado en {0:P0} Empieza azul (EMPATE) Haga clic para cambiar la carta empate derrota ¡Hurra! [Coloca la última carta roja para activar el modificador] Empieza rojo: arrastre y suelte la carta en el tablero Selecciona una carta de cada mazo para intercambiar Informes de errores: Página principal del proyecto: Localización de la IU: Info y Preferencias ¡Gracias a todos los que han contribuido con sus traducciones! ¡Sois increíbles! (todavía se necesitan voluntarios ;) ) Completado Ubicación Nombre Poder Recompensa Reglas Número de npcs con cartas: Npcs Estado actual: Última detección Números [U-L-D-R] Tipo Última detección Tipo Reglas: - el juego debe estar en modo ventana o ventana sin bordes - UI: establecer escala a 100%, usa interfaz oscuro - Se debe de seleccionar al PNJ - Haz captura durante el turno de Azul, después de que hayan terminado todas las animaciones de las cartas (! !) Mando: presione ambos gatillos para hacer captura Funciona también con Mini Cactpot :D Detectar: Descartar todo << Reinicia el estado del escáner (hay {0} más pendientes) (hay 1 más pendiente) Guardar Fuente de la imagen: Tipo: ^ Clic derecho en las listas para opciones Eliminar patrones locales Utilizar para borrar todos los patrones personalizados y empezar desde el principio: Reproducción: Captura de pantalla Conectar: Guardar en Google: Abortar Optimizar mazo para PNJ Mazo activo, clic en una carta para opciones: Añadir ranura de mazo favorito PNJ: Poder del mazo: Ubicación: Reglas: Probabilidad de ganar: Número de cartas poseídas: Número de mazos posibles: Número de mazos probados: Progreso: Tiempo restante: Estadísticas de optimización: Cambiar reglas de PNJ, desactivar para torneos Regla 1 de Región: Regla 2 de Región: Torneo: Reglas: Configurar Forzar el uso de imagen en caché Capturar pantalla de juego: modo DEBUG Aplicar regla Cartas conocidas: [Arrastra y suelta cartas en el tablero] Omitir Regla Especial >> Cartas desconocidas restantes: {0} Selecciona las cartas visibles: Ajustar mazo randomizado :( Restablecer Escoge la regla #1 de ruleta Escoge la regla #2 de ruleta Escoge la regla #3 de ruleta Escoge la regla #4 de ruleta Reglas actuales: Reproducción: Simular Deshacer el último movimiento de Rojo Probabilidad de ganar: Nueva versión descargada, por favor reinicie el programa para finalizar la actualización. Haga clic aquí para ocultar. Escaneo automático Capturar Mostrar detalles Estado Intercambiado ¡El mazo azul no coincide con la página de configuración! El solucionador necesita observar algunas partidas para generar uno correcto. PNJ: {0} Detalles del mazo de Rojo: < Cartas rojas posibles, sin incluir las visibles debajo: Todas colocadas: {0} Var colocado: {0} Reglas: {0} Id del escaneo: {0} desconocido PNJ cambiado, esperando a escanear... Listo (Activar turno!) ¡Mueve el cursor lejos de la zona de escaneo! Mini cactpot: Listo ¡Cartas desconocidas! Revisa Reproducción: Captura de pantalla para más detalles No se puede encontrar el mazo azul El juego no se está ejecutando No se puede encontrar la ventana del juego No se puede encontrar el tablero No se puede recuperar la imagen para analizar No se puede encontrar la ventana del minijuego Listo Error al reconocer el minijuego ¡Patrón desconocido! Revisa Reproducción: Captura de pantalla para más detalles Esperando al turno de Azul Siempre visible Usar siempre iconos pequeños Desactivar aceleración por hardware Tamaño de la fuente Duración del marcador: cactpot Duración del marcador: carta Duración del marcador: intercambio Omitir reglas opcionales en simulado Configuración ================================================ FILE: sources/loc/strings.fr.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Annuler D: L: Ajusté : D: État : H: Actuel : Distance : Caché Verrouillé Aucun Placé : Bleu Placé : Rouge Visible automatique Personnaliser <= Classificateur basé <= Le plus bas est plus précis <= Correspondance exacte N/A inconnu Détecter : Sauvegarder Ajuster la détection FF Triad Buddy Verrouiller les cartes pour l'optimisation du deck Choisir une carte: Utiliser uniquement les cartes détenues (cliquez pour modifier) Modifier Ajouter Fav #{0} Le deck favori sera supprimé, voulez-vous continuer ? Mettre à jour Deck favori, cliquez sur une carte pour les options : Nom abrégé: Retirer Modifier le deck favori ffxiv_dx11 ffxiv FINAL FANTASY Exporter réglages Importer réglages Afficher les sauvegardes Sauvegardes locales Icônes Liste # Nom Possédé Puissance Rareté Type Nombre de cartes possédées: Cartes Trouver en ligne Récompense du PNJ: Trouver une carte: Trouver un PNJ: Ajuster... Supprimer Récompenses (cliquez pour ajouter/retirer) Sélectionner un PNJ pour jouer Oui Cartes dupliquées trouvées ! Cartes manquantes ! Plus de deux 4 étoiles ! Une seule 5 étoiles est autorisée ! Oui (aucun) Actif bas mil haut Bleu {0} Plateau {0} {1} Rouge {0} non détectée! Carte Numéro Règle Auto Exact Similaire (choisir la correspondance) Désactivé Impossible de reconnaître certaines cartes! L'état du jeu ne sera pas précis Plateau introuvable ! Essayez de réinitialiser la position de l'IU, désactiver reshade, etc... Impossible de reconnaître le modèle! Voir les détails ci-dessous Sauvegarde Cloud appliquée Échec de l'appel API autorisation... désactivé, local uniquement échec de la base de données actif Authentification requise analyse... synchronisé stocké abandonné à {0:P0} Bleu débute (TIRAGE) Cliquez pour changer de carte tirage perdu youpi ! [Placer la dernière carte rouge pour déclencher le modificateur] Début rouge: glisser-déposer la carte sur le plateau Sélectionnez une carte de chaque deck à échanger Rapports de bugs: Page d'accueil du projet: Traduction de l'IU: Infos & Réglages Merci à tous ceux qui ont contribué à leurs traductions ! Vous êtes géniaux! ( bénévoles encore nécessaires ;) ) Complété Lieu Nom Puissance Récompense Règles Nombre de PNJs avec cartes: PNJs État actuel: Dernière détection Numéros [U-L-D-R] Type Dernière détection Type Règles : - le jeu doit être en mode Fenêtré ou Fenêtre sans bordure - IU : définir l'échelle à 100%, utiliser le schéma sombre - PNJ doit être sélectionné - capturer pendant le tour bleu, après que toutes les animations de carte soient terminées (!!!) Gamepad : appuyez sur les deux boutons de déclenchement pour capturer Fonctionne également avec le Mini Cactpot :D Détecter: Tout abandonner << État du scanner de réinitialisation (il y a {0} autres en attente) (il y a 1 autre en attente) Sauvegarder Image source: Type: ^ Clic droit sur les listes pour les options Supprimer les modèles locaux Utiliser pour effacer tous les modèles personnalisés et partir de zéro: Lecture: Capture d'écran Connexion: Enregistrer sur Google: Abandonner Optimiser le deck pour le PNJ Deck actif, cliquez sur carte pour les options: Ajouter un emplacement de deck favori PNJ: Puissance du deck: Lieu: Règles: Chance de victoire: Nombre de cartes détenues: Nombre de decks possibles: Nombre de decks testés: Progrès: Temps restant: Statistiques d'optimisation: Modifier les règles PNJ, désactiver pour le tournoi Règle de région 1: Règle de région 2: Tournoi: Règles: Configuration Forcer l'utilisation de l'image en cache Capture d'écran du jeu: mode DEBUG Appliquer une règle Cartes connues: [Glisser et déposer des cartes sur le plateau] Passer Règle Spéciale >> Cartes inconnues restantes: {0} Sélectionnez les cartes visibles: Ajuster le deck randomisé :( Réinitialiser Choisir la règle de la roulette #1 Choisir la règle de la roulette #2 Choisir la règle de la roulette #3 Choisir la règle de la roulette #4 Règles actuelles: Lire: Simuler Annuler le dernier coup Rouge Chance de victoire: Nouvelle version téléchargée, veuillez redémarrer le programme pour terminer la mise à jour. Cliquez ici pour cacher. Balayage auto Capturer Afficher les détails État ÉCHANGÉ Le paquet bleu ne correspond pas à la page de configuration! Le solveur doit observer quelques jeux pour deviner la bonne. PNJ : {0} Détails du deck Rouge : < Cartes rouges potentielles, sans inclure celles visibles ci-dessous: Tous placés: {0} Variable placée: {0} Règles: {0} ID du scan: {0} inconnu PNJ a changé, en attente d'analyse... Prêt (tour actif!) Éloignez le curseur de la zone de scan! Mini cactpot: Prêt Cartes inconnues! Vérifiez la capture d'écran pour plus de détails Deck bleu introuvable Le jeu n'est pas en cours d'exécution Impossible de trouver la fenêtre de jeu Plateau introuvable Impossible de récupérer l'image à analyser Impossible de trouver la fenêtre du mini-jeu Prêt Impossible de reconnaître le mini-jeu Modèle inconnu! Vérifiez la lecture:Capture d'écran pour plus de détails En attente du tour bleu Toujours au premier plan Toujours utiliser de petites icônes Désactiver l'accélération matérielle Taille de police Durée du marqueur: cactpot Durée du marqueur: carte Durée du marqueur: échange Ignorer les règles optionnelles dans la simulation Réglages ================================================ FILE: sources/loc/strings.ja.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 キャンセル 下: 左: 調整後: 右: ステータス: 上: 現在: 距離: 隠し ロック中 なし 配置済み: 青 配置済み: 赤 可視 自動 カスタム <= 分類器による <= 低い方が正確です <= 完全一致 該当なし 不明 検出: 保存する 検出の調整 FF トリプルトライアド バディ デッキの最適化にカードをロックする カードを選択: 所有しているカードのみを使用(クリックで変更) 編集 追加 お気に入り #{0} お気に入りのデッキは削除されます。続行しますか? 変更 お気に入りのデッキ。カードをクリックしてオプションが開けます: 略名: 削除 お気に入りのデッキを編集 ffxiv_dx11 ffxiv FINAL FANTASY 設定をエクスポート 設定をインポート バックアップを表示 ローカルセーブ アイコン 一覧表示 # 名前 所有済み 数字 レアリティ タイプ 所有カードの枚数: カード オンラインで検索 NPC報酬: カードを検索: NPCを検索: 調整… 削除 報酬(クリックで追加/削除) 対戦NPCを選択 はい 重複したカードが見つかりました! カードがありません! ★4以上のカードが最大2枚編成可能! ★5のカードが1枚のみ編成可能! はい (なし) 有効 自分 {0} ボード {0} {1} 相手 {0} 検出されませんでした! カード 数字 ルール 自動 完全一致 類似 (一致を選択) 無効 一部のカードを検出できませんでした!対戦状態は正しくない場合があります。 ボードを検出できません!UI位置のリセットや、Reshadeを無効にするなどを試してください... パターンを検出できません!詳細は以下を参照してください。 クラウドデータが適用されました API アクセスに失敗しました 認証中... 無効、ローカルのみ データベースにエラー 有効 認証が必要です 検出中… 同期しました 保存済み {0:P0} で中止されました 自分が先攻 (引き分け) クリックしてカードを変更 引き分け 敗北 やった! [相手のカードを配置してルールを適用する] 相手先攻: カードをボードへドラッグ&ドロップ 各デッキからカードを1枚選択して交換する バグ報告: ホームページ: UI のローカライズ: 情報と設定 翻訳に貢献してくれた皆さん、ありがとうございます!皆さん素晴らしいです! (ボランティア募集中 ;) ) 完了 位置 名前 数字 報酬 ルール カードを獲得できるNPC数: NPC 現在の状態: 最新の検出 数字 [上-左-下-右] タイプ 最新の検出 タイプ 手順: - ゲーム画面をウィンドウモードや仮想フルスクリーンモードへ切替 - UIサイズを100%を設定、カラーテーマをダークにする。 - カードのアニメーションが終わった後、自分の手番で画面をキャプチャ(!!!) コントローラーを使っている方: LT/RTを同時押し。 ミニ・くじテンダーにも使える。 検出: すべてを破棄する << スキャナをリセット ({0} 件が保留中です) (1 件が保留中です) 保存する ソース画像: タイプ: 右クリックで詳細オプションを開く 保存したパターンを削除 カスタムパターンを削除して検出機能をリセット 対戦: 検出機能 ログイン: Google Driveに保存 中止 デッキをNPCによって最適化 現在のデッキ、カードをクリックしてオプションが開けます: デッキをお気に入りに追加 NPC: デッキの数字: 位置: ルール: 勝率: 所有カード数: 可能なデッキ数: テスト済みデッキ数: 進捗状況: 残り時間: 最適化統計: NPCルールを切り替え、トーナメントで無効にする 流行ルール1: 流行ルール2: トーナメント: ルール: 設定 強制的にキャッシュされた画像を使用する スクリーンショットを撮る: デバッグ用 ルールを適用 既知のカード: [カードをボードへドラッグ&ドロップ] スキップ 特殊ルール >> 不明なカードの残り: {0} 表示中のカードを選択: ランダムなデッキを調整する :( リセット ルーレットルール#1を選択 ルーレットルール#2を選択 ルーレットルール#3を選択 ルーレットルール#4を選択 現在のルール: 対戦: シミュレーション 最後の相手カード操作を元に戻す 勝率: 新しいバージョンがダウンロードされました。アップデートを完了するためにプログラムを再起動してください。非表示にするにはここをクリックしてください。 自動スキャン キャプチャ 詳細表示 ステータス 交換済み 自分のデッキはセットアップページと一致しません!ソルバーは正しいカードを検出するためにいくつかの対戦を観察する必要があります。 NPC: {0} デッキ詳細を表示 < 可能な相手のカードです。以下に表示されるカードは含みません: 必ず持ち: {0} 持ち可能: {0} ルール: {0} スキャンID: {0} 不明 NPCが変更されたので、再スキャンを待ってください… 準備完了 (俺のターン!) スキャンゾーンからカーソルを遠ざける! ミニサボテン: 準備完了 不明なカードです!詳細は[対戦: スクリーンショット]で確認してください 自分のデッキが見つかりません ゲームが起動していません ゲームウィンドウは見つかりません ボードが見つかりません 画像を取得できません。 ミニゲームウィンドウが見つかりません 準備完了 ミニゲームウィンドウを検出できませんでした。 不明なパターンです!詳細は[対戦: スクリーンショット]で確認してください 自分の手番を待っています 常に最前面に表示する 常に小さなアイコンを使用 ハードウェアアクセラレーションを無効にする フォント サイズ マーカーの継続時間: ミニ・くじ マーカーの継続時間: カード マーカーの継続時間: スワップ シミュレーションでオプションのルールをスキップ 設定 ================================================ FILE: sources/loc/strings.ko.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 취소 하: 좌: 조정됨: 우: 상태: 상: 현재: 거리: 숨김 잠김 없음 파란색 차례 빨간색 차례 보이기 자동 사용자 정의 <= 분류 기반 <= 낮은 쪽이 더 정확 <= 정확히 일치 해당 없음 알 수 없음 감지: 저장 조정 감지함 FF 트라이어드 버디 덱 최적화를 위한 카드 잠금 카드 선택: 갖고 있는 카드만 사용 (눌러서 변경) 편집 추가 즐겨찾기 #{0} 즐겨찾기한 덱을 제거합니다, 확실한가요? 업데이트 즐겨찾기 덱, 카드를 눌러서 설정: 짧은 이름: 제거 즐겨찾기 덱 편집 ffxiv_dx11 ffxiv 파이널 판타지 설정 내보내기 설정 가져오기 백업 목록 보기 이 컴퓨터에 저장 아이콘 목록 # 이름 갖고있음 성능 레어도 구분 갖고 있는 카드 개수: 카드 온라인에서 찾기 NPC 보상: 카드 찾기: NPC 찾기: 조정... 지우기 보상 (눌러서 추가/삭제) Npc를 고르세요 중복 카드 발견! 보유하지 않은 카드 4성카드가 2개보다 많습니다. 5성카드 한 개만 덱에 넣을 수 있습니다. (없음) 활성화 중앙 파랑 {0} 보드 {0} {1} 빨강 {0} 검출되지 않았습니다. 카드 숫자 규칙 자동 정확한 유사 (일치를 선택) 무효 일부 카드를 검출하지 못했습니다! 대전 상태가 올바르지 않을 경우가 있습니다. 보드를 검출할 수 없습니다. UI 위치의 리셋 또는 Reshade를 무효로 하는 등 시도해주세요. 패턴을 검출할 수 없습니다. 상세 사항은 아래를 참조해주세요. 클라우드 데이터가 적용되었습니다 API 액세스 실패 인증중... 무효, 로컬만 가능 데이터베이스 에러 활성화 인증이 필요합니다. 검색 중... 동기화 성공! 보존됨 {0:P0}에 종료 파란색 우선시작 (무승부) 바꿀 카드를 선택 동점 패배 이겼다! [수정 트리거를 위한 마지막 빨간 카드 놓기] 빨강 시작: 카드를 보드에 끌어다 놓으세요 서로 바꿀 카드를 각각의 덱에서 고르세요 버그 보고: 프로젝트 웹사이트: UI 지역화: 정보 & 설정 모든 번역 제공자분들에게 감사드립니다! 다들 멋져요! (봉사자가 아직 필요해요 😵) 완료함 위치 이름 파워 보상 규칙 카드를 갖고 있는 NPC 수: NPC들 현재 상태: 마지막 감지 숫자 [U-L-D-R] 형식 마지막 감지 형식 규칙: - 게임을 창 또는 전체 창 모드로 실행해야 합니다. - UI: 배율은 100%로, 어두운 구성표로 사용합니다. - NPC를 선택해야 합니다. - 모든 카드 애니메이션이 완료된 후, 파랑 턴에 갈무리 합니다 (!!!) 게임 패드: 트리거 버튼을 동시에 눌러 갈무리 할 수 있습니다 미니 복권에서도 쓸 수 있습니다! 🥰 감지: 모두 버리기 << 스캔 상태 초기화 ({0} 이상 대기중) (1 이상 대기중) 저장 소스 이미지: 형식: 옵션 목록에서 마우스 우클릭 로컬 패턴 해제 모든 커스텀 패턴을 지우고 검출기능을 리셋합니다: 플레이: 스크린샷 로그인: 구글에 저장: 중단 NPC용 덱 최적화 활성화 덱, 옵션을 보려면 카드를 클릭하세요 즐겨찾기에 덱 추가 NPC: 덱 파워 위치 규칙 승률: 갖고 있는 카드 수: 가능한 덱 수: 테스트한 덱 수: 진행 상황 남은 시간: 통계: Npc 규칙 표시, 토너먼트에 사용안함 유행 규칙 1: 유행 규칙 2: 토너먼트: 규칙: 설정 강제로 캐시 이미지 사용 스크린 샷 찍기: 디버그 모드 규칙 적용 알고 있는 카드: [카드를 보드에 끌어다 놓기] 건너뛰기 특별한 룰 >> 남은 알 수 없는 카드: {0} 보이는 카드 선택: 임의 덱 조정 :( 초기화 루렛 규칙 선택 #1 루렛 규칙 선택 #2 루렛 규칙 선택 #3 루렛 규칙 선택 #4 현재 규칙: 플레이: 시뮬레이션 마지막 빨강 이동 물리기 승리 찬스: 새 버전으로 업데이트하려면 다시 시작하세요. 여기를 누르면 보이지 않습니다 자동 스캔 갈무리 자세히 보기 상태 서로바뀜 파란 덱이 설정 페이지와 다릅니다! 추측해보려면 몇몇 게임을 관찰할 필요가 있습니다. NPC: {0} 빨강 덱 자세히 보기: < 가능한 상대방의 카드입니다. 아래에 표시되는 카드는 포함되지 않습니다. 모두 배치됨: {0} 휴대 가능: {0} 규칙: {0} 스캔 ID: {0} 알 수 없음 NPC 변경됨, 스캔을 기다립니다... 준비완료 (현재 턴!) 스캔 구역에 마우스가 있습니다. 비켜주세요. 미니 복권: 준비됨 알 수 없는 카드! 플레이 확인: 스크린샷으로 세부사항 파랑 덱을 찾을 수 없습니다 게임을 찾을 수 없습니다 게임 윈도우를 찾을 수 없습니다 보드를 찾을 수 없습니다 분석할 이미지를 불러올 수 없습니다 미니게임 윈도우를 찾을 수 없습니다 준비 완료 미니게임을 인식할 수 없습니다 알 수 없는 패턴! 플레이 확인: 스크린샷으로 세부사항 파랑 턴을 기다립니다 언제나 맨 위 항상 작은 아이콘 사용 하드웨어 가속 비활성화 글꼴 크기 마커 길이: 복권 마커 길이: 카드 마거 길이: 서로바꿈 시뮬레이션에서 옵션 규칙 건너뛰기 설정 ================================================ FILE: sources/loc/strings.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Cancel D: L: Adjusted: R: Status: U: Current: Distance: Hidden Locked None Placed: Blue Placed: Red Visible auto custom <= Classifier based <= Lower is more accurate <= Exact match N/A unknown Detect: Save Adjust detection FF Triad Buddy Lock card for deck optimization Pick card: Use only owned cards (click to change) Edit Add Fav #{0} Favorite deck will be removed, do you want to continue? Update Favorite deck, click card for options: Short name: Remove Edit favorite deck ffxiv_dx11 ffxiv FINAL FANTASY Export settings Import settings Show backups Local saves Icons List # Name Owned Power Rarity Type Number of owned cards: Cards Find online Npc reward: Find card: Find npc: Adjust... Delete Rewards (click to add/remove) Select npc to play Yes Found duplicate cards! Missing cards! More than two 4+ star! Only one 5 star allowed! Yes (none) Active bot mid top Blue {0} Board {0} {1} Red {0} not detected! Card Number Rule Auto Exact Similar (select match) Disabled Failed to recognize some of cards! Game state won't be accurate Can't find board! Try resetting UI position, turning off reshade, etc... Can't recognize pattern! See details below Cloud save applied API call failed authorizing... disabled, local only database failure active Auth required scanning... synchronized stored aborted at {0:P0} Blue starts (DRAW) Click to change card draw lost yay! [Place last red card to trigger modifier] Red start: drag & drop card on board Select one card from each deck to swap Bug reports: Project home page: UI localization: Info & Settings Thanks to everyone who contributed their translations! You're awesome! ( volunteers still needed ;) ) Completed Location Name Power Reward Rules Number of npcs with cards: Npcs Current state: Last detection Numbers [U-L-D-R] Type Last detection Type Rules: - game must be in Windowed or Borderless Windowed mode - UI: set scale to 100%, use dark scheme - npc must be selected - capture during Blue turn, after all card animations finished (!!!) Gamepad: press both trigger buttons to capture Works with Mini Cactpot too :D Detect: Discard All << Resets scanner state (there are {0} more pending) (there is 1 more pending) Save Source image: Type: ^ Right click on lists for options Remove local patterns Use to clear all custom patterns and start from scratch: Play: Screenshot Login: Save in Google: Abort Optimize deck for NPC Active deck, click card for options: Add favorite deck slot NPC: Deck power: Location: Rules: Win chance: Num owned cards: Num possible decks: Num tested decks: Progress: Time left: Optimization stats: Toggle npc rules, disable for tournament Region rule 1: Region rule 2: Tournament: Rules: Setup Force using cached image Take game screenshot: DEBUG mode Apply rule Known cards: [Drag & drop cards on board] Skip Special Rule >> Unknown cards remaining: {0} Select visible cards: Adjust randomized deck :( Reset Pick roulette rule #1 Pick roulette rule #2 Pick roulette rule #3 Pick roulette rule #4 Current rules: Play: Simulate Undo last Red move Win chance: New version downloaded, please restart program to finish update. Click here to hide. Auto scan Capture Show details Status SWAPPED Blue deck is not matching Setup page! Solver needs to observe a few games to guess correct one. NPC: {0} Red deck details: < Potential red cards, not including visible ones below: All placed: {0} Var placed: {0} Rules: {0} Scan Id: {0} unknown npc changed, waiting for scan... Ready (active turn!) Move cursor away from scan zone! Mini cactpot: Ready Unknown cards! Check Play:Screenshot for details Can't find blue deck Game is not running Can't find game window Can't find board Can't retrieve image to analyze Can't find minigame window Ready Failed to recognize minigame Unknown pattern! Check Play:Screenshot for details Waiting for blue turn Always on top Always use small icons Disable hardware acceleration Font size Marker duration: cactpot Marker duration: card Marker duration: swap Skip optional rules in simulate Settings ================================================ FILE: sources/loc/strings.zh.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 取消 下: 左: 调整后: 右: 状态: 上: 当前: 差距: 已隐藏 已锁定 已放置:蓝色 已放置:红色 可见 自动 自定义 <= 基于分类器 <= 越低越准确 <= 精确匹配 N/A 未知 检测: 保存 检测调整 FF幻卡助手 优化卡组时锁定此幻卡 选择幻卡: 仅使用已拥有的幻卡 编辑 添加 收藏 #{0} 卡组收藏将被移除,是否继续? 更新 卡组收藏,点击下方幻卡查看选项: 卡组名称: 移除 编辑卡组收藏 ffxiv_dx11 ffxiv 最终幻想 导出设置 导入设置 显示备份 本地存档 图标视图 列表视图 编号 名称 已拥有 数值 稀有度 类型 已拥有幻卡数: 幻卡 在线检索 NPC奖励: 查找幻卡: 查找NPC: 调整... 删除 奖励(点击添加/移除) 选择此NPC进行对战 幻卡重复! 卡片缺失! 多余两张4星+幻卡! 只能有一张5星卡! (无) 激活 蓝方 {0} 牌桌 {0} {1} 红方 {0} 未检测到! 幻卡 数值 规则 自动 精确 近似 (选择匹配) 禁用 未能检测某些幻卡! 游戏状态将不准确 无法找到牌桌!请尝试调整界面位置,关闭画面滤镜,等... 无法检测图案!请查看下方详情 已启用云端保存 API调用失败 授权中... 禁用,仅在本地使用 数据库故障 激活 需要授权 扫描中... 已同步 已保存 已在 {0:P0} 中断 蓝方先手 (平局) 点击切换幻卡 平局 耶! [放置最后一张红卡以触发规则] 红方先手: 请将幻卡拖放到牌桌 从双方卡组中各选一张幻卡进行交换 问题反馈: 项目主页: 界面本地化: 信息与设定 感谢所有贡献翻译的人!你太棒了! (仍需要翻译志愿者 ;) ) 完成 位置 名称 强度 奖励 规则 未完全击破的NPC数: NPC 当前状态: 最后一次检测 数值 [上-左-下-右] 类型 最后一次检测 类型 说明: - 游戏必须处于普通窗口或窗口全屏模式 - 界面: 将基本尺寸调整为100%, 使用深色主题 - 必须选中NPC - 待所有幻卡动画结束后,在蓝方回合进行捕获(!!!) 手柄:同时按下LT与RT按键进行捕获 此模式也适用于仙人微彩 :D 检测: 丢弃所有 << 重置扫描仪状态 (还有{0}个待处理) (还有1个待处理) 保存 来源图像: 类型: ^ 右键列表进行编辑 移除本地图案 清除所有图案缓存以重置检测功能: 对局:捕获模式 登录: 保存在谷歌网盘: 中断 为此NPC优化卡组 生效中卡组,点击下方幻卡查看选项 添加收藏卡组 NPC: 卡组强度: 位置: 规则: 获胜概率: 已拥有幻卡数: 卡组可能性: 已测试卡组: 进度: 剩余时间: 优化详情: 切换NPC规则,取消勾选为大赛专用 流行规则1: 流行规则2: 大赛: 规则: 配置 强制使用缓存图像 拍摄游戏截图:DEBUG模式 应用规则 已知幻卡: [请将幻卡拖放到牌桌] 跳过 特殊规则 >> 剩余未知幻卡: {0} 选择可见幻卡: 调整随机卡组 :( 重置 选择天选规则 #1 选择天选规则 #2 选择天选规则 #3 选择天选规则 #4 当前规则: 对局:模拟模式 撤销红方上一步 获胜概率: 新版本已下载完成,请重新启动程序以完成更新。点击此处隐藏。 自动扫描 捕获 展开详情 状态 已交换 蓝方卡组与设置页不匹配!求解器需要观察几局才能猜出正确的幻卡。 NPC: {0} 红方卡组详情: < 隐藏的红方幻卡,不包含下方可见的: 必带卡: {0} 选带卡: {0} 规则: {0} 扫描Id: {0} 未知 NPC已改变,正在等待扫描... 准备就绪(我的回合!) 将鼠标光标从扫描区移开! 仙人微彩:准备就绪 未知幻卡!查看[对局:捕获模式]选项卡了解详情 找不到蓝色卡组 游戏没有运行 无法找到游戏窗口 无法找到牌桌 无法检索到待分析的图像 无法找到小游戏窗口 准备就绪 无法识别小游戏 未知图案!查看[对局:捕获模式]选项卡了解详情 等待蓝方回合 窗口总在最前 始终使用小图标 禁用硬件加速 字体大小 标记持续时间: 仙人微彩 标记持续时间: 幻卡 标记持续时间: 交换 在模拟中跳过可选规则 设置 ================================================ FILE: sources/ui/App.xaml ================================================  ================================================ FILE: sources/ui/App.xaml.cs ================================================ using MgAl2O4.Utils; using System; using System.Globalization; using System.Reflection; using System.Resources; using System.Windows; using System.Windows.Media; namespace FFTriadBuddy.UI { /// /// Interaction logic for App.xaml /// public partial class App : Application { private bool canSaveSettings = false; private void Application_Startup(object sender, StartupEventArgs e) { Logger.Initialize(e.Args); bool canStart = false; bool updatePending = GithubUpdater.FindAndApplyUpdates(); if (!updatePending) { bool hasAssets = LoadAssets(); if (hasAssets) { canStart = true; } else { string appName = Assembly.GetEntryAssembly().GetName().Name; MessageBox.Show("Failed to initialize resources!", appName, MessageBoxButton.OK, MessageBoxImage.Stop); } } #if DEBUG if (Array.Find(e.Args, x => x == "-runTests") != null) { TestManager.RunTests(); canStart = false; } if (Array.Find(e.Args, x => x == "-dataConvert") != null) { var converter = new DataConverter(); converter.Run(); canStart = false; } if (Array.Find(e.Args, x => x == "-runSolverAccTest") != null) { TriadGameTests.RunSolverAccuracyTests(); canStart = false; } else if (Array.Find(e.Args, x => x == "-runSolverStressTest") != null) { TriadGameTests.RunSolverStressTest(); canStart = false; } else if (Array.Find(e.Args, x => x == "-generateSolverTrainingData") != null) { TriadGameTests.GenerateAccuracyTrainingData(); canStart = false; } #endif // DEBUG if (canStart) { int renderingTier = RenderCapability.Tier >> 16; Logger.WriteLine("Rendering tier:{0}", renderingTier); DialogWindowService.Initialize(); OverlayWindowService.Initialize(); AppWindowService.Initialize(); canSaveSettings = true; var settingsDB = PlayerSettingsDB.Get(); ViewModelServices.AppWindow.SetSoftwareRendering(settingsDB.useSoftwareRendering); var window = new MainWindow(); window.FontSize = settingsDB.fontSize; window.Topmost = settingsDB.alwaysOnTop; if (settingsDB.lastHeight > window.MinHeight) { window.Height = settingsDB.lastHeight; } if (settingsDB.lastWidth > window.MinWidth) { window.Width = settingsDB.lastWidth; } window.Show(); } else { Shutdown(); } } private void Application_Exit(object sender, ExitEventArgs e) { if (canSaveSettings) { SettingsModel.Close(); } XInputStub.StopPolling(); Logger.Close(); } private bool LoadAssets() { bool bResult = false; try { var resManager = new ResourceManager("FFTriadBuddy.Properties.Resources", Assembly.GetExecutingAssembly()); var assets = (byte[])resManager.GetObject("assets"); if (AssetManager.Get().Init(assets)) { LocalizationDB.SetCurrentUserLanguage(CultureInfo.CurrentCulture.Name); bResult = TriadCardDB.Get().Load(); bResult = bResult && TriadNpcDB.Get().Load(); bResult = bResult && ImageHashDB.Get().Load(); bResult = bResult && TriadTournamentDB.Get().Load(); bResult = bResult && LocalizationDB.Get().Load(); if (bResult) { SettingsModel.Initialize(); IconDB.Get().Load(); ModelProxyDB.Get().Load(); TriadGameSimulation.StaticInitialize(); } } } catch (Exception ex) { Logger.WriteLine("Init failed: " + ex); bResult = false; } return bResult; } } } ================================================ FILE: sources/ui/modelproxy/BulkObservableCollection.cs ================================================ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; namespace FFTriadBuddy.UI { public class BulkObservableCollection : ObservableCollection { private bool isNotifySuspended = false; public bool IsNotifySuspended => isNotifySuspended; private bool needsNotify = false; private int cachedCount = 0; protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (!isNotifySuspended) { base.OnCollectionChanged(e); } else { needsNotify = true; } } public void SuspendNotifies() { cachedCount = Items.Count; isNotifySuspended = true; needsNotify = false; } public void ResumeNotifies() { isNotifySuspended = false; if (needsNotify) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } public void AddRange(IEnumerable newItems) { SuspendNotifies(); foreach (var item in newItems) { Add(item); } isNotifySuspended = false; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, cachedCount)); } } } ================================================ FILE: sources/ui/modelproxy/CardModelProxy.cs ================================================ using System; namespace FFTriadBuddy.UI { // viewmodel wrapper for model class: card public class CardModelProxy : BaseViewModel, IComparable, IImageHashMatch { public readonly TriadCard cardOb; public string NameLocalized => cardOb.Name.GetLocalized(); public string DescDeckPicker => string.Format("{0} ({1})", NameLocalized, new string('*', (int)cardOb.Rarity + 1)); public int GameSortGroup => cardOb.SortOrder / 1000; public int GameSortOrder => cardOb.SortOrder; public int Id => cardOb.Id; public string DescPower => string.Format("{0:X}-{1:X}-{2:X}-{3:X}", cardOb.Sides[(int)ETriadGameSide.Up], cardOb.Sides[(int)ETriadGameSide.Left], cardOb.Sides[(int)ETriadGameSide.Down], cardOb.Sides[(int)ETriadGameSide.Right]); public string DescRarity { get; private set; } public ETriadCardRarity Rarity => cardOb.Rarity; public string DescCardType => LocalizationDB.Get().mapCardTypes[cardOb.Type].GetLocalized(); public ETriadCardType CardType => cardOb.Type; private bool isOwned; public bool IsOwned { get => isOwned; set { if (isOwned != value) { isOwned = value; OnPropertyChanged(); ModelProxyDB.Get().UpdateOwnedCard(this); } } } public int CompareTo(object obj) { var otherCard = obj as CardModelProxy; return (otherCard != null) ? NameLocalized.CompareTo(otherCard.NameLocalized) : 0; } public CardModelProxy(TriadCard triadCard) { cardOb = triadCard; DescRarity = "*"; for (int idx = 0; idx < (int)triadCard.Rarity; idx++) { DescRarity += " *"; } } public object GetMatchOwner() { return cardOb; } } } ================================================ FILE: sources/ui/modelproxy/IconDB.cs ================================================ using MgAl2O4.Utils; using System.Collections.Generic; using System.IO; using System.Windows.Media.Imaging; namespace FFTriadBuddy.UI { public class IconDB { public List mapCardImages; public List mapCardImagesBig; public Dictionary mapCardTypes; public Dictionary mapCardRarities; public Dictionary mapFlags; private static IconDB instance = new IconDB(); public static IconDB Get() { return instance; } public void Load() { LoadCardImages(); LoadCardTypes(); LoadCardRarities(); LoadFlags(); } private BitmapImage LoadImageFromAsset(string path) { var image = new BitmapImage(); using (var fileStream = AssetManager.Get().GetAsset(path)) { using (var memStream = new MemoryStream()) { if (fileStream != null) { fileStream.CopyTo(memStream); memStream.Position = 0; image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.StreamSource = memStream; image.EndInit(); image.Freeze(); } } } return image; } private void LoadCardImages() { mapCardImages = new List(); mapCardImagesBig = new List(); string nullImagePath = "icons/088001.png"; var nullImg = LoadImageFromAsset(nullImagePath); string nullImageBigPath = "icons/087000.png"; var nullImgBig = LoadImageFromAsset(nullImageBigPath); TriadCardDB cardDB = TriadCardDB.Get(); for (int idx = 0; idx < cardDB.cards.Count; idx++) { var cardOb = cardDB.cards[idx]; if (cardOb != null && cardOb.IsValid()) { string loadPathSmall = "icons/" + cardOb.SmallIconId.ToString("000000") + ".png"; var loadedImage = LoadImageFromAsset(loadPathSmall); mapCardImages.Add(loadedImage); string loadPathBig = "icons/" + cardOb.BigIconId.ToString("000000") + ".png"; loadedImage = LoadImageFromAsset(loadPathBig); mapCardImagesBig.Add(loadedImage); } else { mapCardImages.Add(nullImg); mapCardImagesBig.Add(nullImgBig); } } } private void LoadCardTypes() { mapCardTypes = new Dictionary(); mapCardTypes.Add(ETriadCardType.None, null); mapCardTypes.Add(ETriadCardType.Beastman, LoadImageFromAsset("parts/typeBeastman.png")); mapCardTypes.Add(ETriadCardType.Primal, LoadImageFromAsset("parts/typePrimal.png")); mapCardTypes.Add(ETriadCardType.Scion, LoadImageFromAsset("parts/typeScions.png")); mapCardTypes.Add(ETriadCardType.Garlean, LoadImageFromAsset("parts/typeGarland.png")); } private void LoadCardRarities() { mapCardRarities = new Dictionary(); mapCardRarities.Add(ETriadCardRarity.Common, LoadImageFromAsset("parts/rarityCommon.png")); mapCardRarities.Add(ETriadCardRarity.Uncommon, LoadImageFromAsset("parts/rarityUncommon.png")); mapCardRarities.Add(ETriadCardRarity.Rare, LoadImageFromAsset("parts/rarityRare.png")); mapCardRarities.Add(ETriadCardRarity.Epic, LoadImageFromAsset("parts/rarityEpic.png")); mapCardRarities.Add(ETriadCardRarity.Legendary, LoadImageFromAsset("parts/rarityLegendary.png")); } private void LoadFlags() { mapFlags = new Dictionary(); mapFlags.Add("de", LoadImageFromAsset("flags/flag-germany.png")); mapFlags.Add("en", LoadImageFromAsset("flags/flag-usa.png")); mapFlags.Add("es", LoadImageFromAsset("flags/flag-spain.png")); mapFlags.Add("fr", LoadImageFromAsset("flags/flag-france.png")); mapFlags.Add("ja", LoadImageFromAsset("flags/flag-japan.png")); mapFlags.Add("zh", LoadImageFromAsset("flags/flag-china.png")); mapFlags.Add("ko", LoadImageFromAsset("flags/flag-southkorea.png")); } } } ================================================ FILE: sources/ui/modelproxy/ImageHashDataModelProxy.cs ================================================ using System; using System.Collections.Generic; using System.Drawing.Imaging; using System.IO; using System.Windows.Media.Imaging; namespace FFTriadBuddy.UI { public interface IImageHashMatch { string NameLocalized { get; } object GetMatchOwner(); } public class ImageHashDataModelProxy : LocalizedViewModel { public class NumberVM : IComparable, IImageHashMatch { public int Value; public string NameLocalized => Value.ToString("X"); public int CompareTo(object obj) { return Value.CompareTo((obj as NumberVM).Value); } public object GetMatchOwner() { return Value; } } public readonly ImageHashData hashData; public string DescMatch => hashData.isAuto ? loc.strings.MainForm_Dynamic_Screenshot_MatchType_Auto : (hashData.matchDistance == 0) ? loc.strings.MainForm_Dynamic_Screenshot_MatchType_Exact : loc.strings.MainForm_Dynamic_Screenshot_MatchType_Similar; private string cachedName; public string NameLocalized => cachedName; private string cachedType; public string TypeLocalized => cachedType; private BitmapImage cachedPreview; public BitmapImage PreviewImage { get { GeneratePreview(); return cachedPreview; } } private List listMatches = new List(); public List ListMatches { get { GenerateMatches(); return listMatches; } } private IImageHashMatch currentMatch = null; public IImageHashMatch CurrentMatch { get { GenerateMatches(); return currentMatch; } } private static List listCactpotNumbers; private static List listCardNumbers; public ImageHashDataModelProxy(ImageHashData hashData) { this.hashData = hashData; UpdateCachedText(); } public override void RefreshLocalization() { UpdateCachedText(); OnPropertyChanged("DescMatch"); OnPropertyChanged("NameLocalized"); OnPropertyChanged("TypeLocalized"); } public void UpdateCachedText() { switch (hashData.type) { case EImageHashType.Rule: cachedType = loc.strings.MainForm_Dynamic_Screenshot_HashType_Rule; break; case EImageHashType.Cactpot: cachedType = loc.strings.MainForm_Dynamic_Screenshot_HashType_Number; break; case EImageHashType.CardImage: cachedType = loc.strings.MainForm_Dynamic_Screenshot_HashType_Card; break; case EImageHashType.CardNumber: cachedType = loc.strings.MainForm_Dynamic_Screenshot_HashType_Number; break; default: cachedType = "??"; break; } string descName = "??"; if (hashData.ownerOb != null) { switch (hashData.type) { case EImageHashType.Rule: descName = (hashData.ownerOb as TriadGameModifier).GetLocalizedName(); break; case EImageHashType.Cactpot: descName = ((int)hashData.ownerOb).ToString(); break; case EImageHashType.CardImage: descName = (hashData.ownerOb as TriadCard).ToShortLocalizedString(); break; case EImageHashType.CardNumber: descName = ((int)hashData.ownerOb).ToString(); break; default: break; } } cachedName = cachedType + ": " + descName; } private void GeneratePreview() { if (cachedPreview == null) { hashData.UpdatePreviewImage(); using (var memory = new MemoryStream()) { hashData.previewImage.Save(memory, ImageFormat.Png); memory.Position = 0; cachedPreview = new BitmapImage(); cachedPreview.BeginInit(); cachedPreview.StreamSource = memory; cachedPreview.CacheOption = BitmapCacheOption.OnLoad; cachedPreview.EndInit(); cachedPreview.Freeze(); } } } private void GenerateMatches() { if (listMatches.Count > 0) { return; } var modelProxyDB = ModelProxyDB.Get(); switch (hashData.type) { case EImageHashType.Rule: foreach (var rule in modelProxyDB.Rules) { if ((rule.modOb is TriadGameModifierNone) == false) { listMatches.Add(rule); } } listMatches.Sort(); break; case EImageHashType.Cactpot: if (listCactpotNumbers == null) { listCactpotNumbers = new List(); for (int idx = 1; idx <= 9; idx++) { listCactpotNumbers.Add(new NumberVM() { Value = idx }); } } listMatches.AddRange(listCactpotNumbers); break; case EImageHashType.CardImage: var sameNumberId = ((TriadCard)hashData.ownerOb).SameNumberId; foreach (var cardOb in TriadCardDB.Get().sameNumberMap[sameNumberId]) { listMatches.Add(modelProxyDB.GetCardProxy(cardOb)); } listMatches.Sort(); break; case EImageHashType.CardNumber: if (listCardNumbers == null) { listCardNumbers = new List(); for (int idx = 1; idx <= 10; idx++) { listCardNumbers.Add(new NumberVM() { Value = idx }); } } listMatches.AddRange(listCardNumbers); break; default: break; } currentMatch = listMatches.Find(x => x.GetMatchOwner().Equals(hashData.ownerOb)); } } } ================================================ FILE: sources/ui/modelproxy/ModelProxyDB.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows.Data; namespace FFTriadBuddy.UI { // helper class for translating between tool's data objects and Models used by UI // technically, it's supposed to be ViewModel layer public class ModelProxyDB { private List cards = new List(); public List Cards => cards; private BulkObservableCollection ownedCards = new BulkObservableCollection(); public BulkObservableCollection OwnedCards => ownedCards; private List npcs = new List(); public List Npcs => npcs; private List rules = new List(); public List Rules => rules; private List tournaments = new List(); public List Tournaments => tournaments; public event Action OnCardOwnerChanged; private static ModelProxyDB instance = new ModelProxyDB(); public static ModelProxyDB Get() { return instance; } public void Load() { LoadCards(); LoadOwnedCards(); LoadNpc(); LoadRules(); LoadTournaments(); PlayerSettingsDB.Get().OnUpdated += ModelProxyDB_OnUpdated; UpdateCompletedNpcs(); LocalizationDB.OnLanguageChanged += LocalizationDB_OnLanguageChanged; } private void LoadCards() { TriadCardDB cardDB = TriadCardDB.Get(); cards.Clear(); for (int idx = 0; idx < cardDB.cards.Count; idx++) { var cardEntry = cardDB.cards[idx]; if (cardEntry != null && cardEntry.IsValid()) { cards.Add(new CardModelProxy(cardEntry)); } } } private void LoadOwnedCards() { List removeList = new List(); removeList.AddRange(ownedCards); ownedCards.SuspendNotifies(); ownedCards.Clear(); var settingsDB = PlayerSettingsDB.Get(); foreach (var card in settingsDB.ownedCards) { var cardProxy = cards.Find(x => x.cardOb.Id == card.Id); if (cardProxy != null) { cardProxy.IsOwned = true; ownedCards.Add(cardProxy); removeList.Remove(cardProxy); } } foreach (var card in removeList) { card.IsOwned = false; } ownedCards.ResumeNotifies(); } private void LoadNpc() { TriadNpcDB npcDB = TriadNpcDB.Get(); npcs.Clear(); for (int idx = 0; idx < npcDB.npcs.Count; idx++) { var npcEntry = npcDB.npcs[idx]; if (npcEntry != null) { npcs.Add(new NpcModelProxy(npcEntry)); } } } private void LoadRules() { TriadGameModifierDB modDB = TriadGameModifierDB.Get(); rules.Clear(); for (int idx = 0; idx < modDB.mods.Count; idx++) { var modEntry = modDB.mods[idx]; if (modEntry != null) { rules.Add(new RuleModelProxy(modEntry)); } } var view = CollectionViewSource.GetDefaultView(rules); if (view.SortDescriptions.Count == 0) { view.SortDescriptions.Add(new SortDescription()); } } private void LoadTournaments() { TriadTournamentDB tournamentDB = TriadTournamentDB.Get(); tournaments.Clear(); for (int idx = 0; idx < tournamentDB.tournaments.Count; idx++) { var tourEntry = tournamentDB.tournaments[idx]; if (tourEntry != null) { tournaments.Add(new TournamentModelProxy(tourEntry)); } } var view = CollectionViewSource.GetDefaultView(tournaments); if (view.SortDescriptions.Count == 0) { view.SortDescriptions.Add(new SortDescription()); } } private void ModelProxyDB_OnUpdated(bool bCards, bool bNpcs, bool bDecks) { if (bCards) { LoadOwnedCards(); } } private void LocalizationDB_OnLanguageChanged() { foreach (var npc in npcs) { npc.RefreshLocalization(); } foreach (var tournament in tournaments) { tournament.RefreshLocalization(); } foreach (var rule in rules) { rule.RefreshLocalization(); } CollectionViewSource.GetDefaultView(npcs).Refresh(); CollectionViewSource.GetDefaultView(cards).Refresh(); CollectionViewSource.GetDefaultView(rules).Refresh(); CollectionViewSource.GetDefaultView(tournaments).Refresh(); } public void UpdateOwnedCard(CardModelProxy cardProxy) { if (!ownedCards.IsNotifySuspended) { var settingsDB = PlayerSettingsDB.Get(); var hasChanges = false; if (cardProxy.IsOwned && !OwnedCards.Contains(cardProxy)) { //Logger.WriteLine("Adding owned card: {0}", cardProxy.cardOb.Name.GetCodeName()); OwnedCards.Add(cardProxy); settingsDB.ownedCards.Add(cardProxy.cardOb); hasChanges = true; } else if (!cardProxy.IsOwned && OwnedCards.Contains(cardProxy)) { //Logger.WriteLine("Removing owned card: {0}", cardProxy.cardOb.Name.GetCodeName()); OwnedCards.Remove(cardProxy); settingsDB.ownedCards.Remove(cardProxy.cardOb); hasChanges = true; } if (hasChanges) { settingsDB.MarkDirty(); UpdateCompletedNpcs(); OnCardOwnerChanged?.Invoke(cardProxy); } } } private void UpdateCompletedNpcs() { var settingsDB = PlayerSettingsDB.Get(); foreach (var npc in npcs) { var notOwnedReward = npc.npcOb.Rewards.Find(x => !settingsDB.ownedCards.Contains(x)); npc.IsCompleted = notOwnedReward == null; npc.UpdateCachedText(); } } public NpcModelProxy GetNpcProxy(TriadNpc npcOb) { return Npcs.Find(x => x.npcOb == npcOb); } public CardModelProxy GetCardProxy(TriadCard cardOb) { return Cards.Find(x => x.cardOb == cardOb); } } } ================================================ FILE: sources/ui/modelproxy/NpcModelProxy.cs ================================================ using System; namespace FFTriadBuddy.UI { // viewmodel wrapper for model class: npc public class NpcModelProxy : LocalizedViewModel, IComparable { public readonly TriadNpc npcOb; public string NameLocalized => npcOb.Name.GetLocalized(); public string LocationLocalized => npcOb.GetLocationDesc(); public int DescPower => npcOb.Deck.GetPower(); private bool isCompleted = false; public bool IsCompleted { get { return isCompleted; } set { isCompleted = value; OnPropertyChanged(); } } private string descReward; public string DescReward { get { return descReward; } set { descReward = value; OnPropertyChanged(); } } private string descRules; public string DescRules { get { return descRules; } set { descRules = value; OnPropertyChanged(); } } private string descCompleted; public string DescCompleted { get { return descCompleted; } set { descCompleted = value; OnPropertyChanged(); } } public int CompareTo(object obj) { var otherNpc = obj as NpcModelProxy; return (otherNpc != null) ? NameLocalized.CompareTo(otherNpc.NameLocalized) : 0; } public NpcModelProxy(TriadNpc triadNpc) { npcOb = triadNpc; UpdateCachedText(); } public void UpdateCachedText(bool sendNotifies = true) { var newDescRules = ""; foreach (var rule in npcOb.Rules) { if (newDescRules.Length > 0) { newDescRules += ", "; } newDescRules += rule.GetLocalizedName(); } if (newDescRules.Length == 0) { newDescRules = loc.strings.MainForm_Dynamic_RuleListEmpty; } PlayerSettingsDB settingsDB = PlayerSettingsDB.Get(); var newDescRewards = ""; foreach (var reward in npcOb.Rewards) { if (!settingsDB.ownedCards.Contains(reward)) { if (newDescRewards.Length > 0) { newDescRewards += ", "; } newDescRewards += reward.Name.GetLocalized(); } } descRules = newDescRules; descReward = newDescRewards; descCompleted = IsCompleted ? loc.strings.MainForm_Dynamic_NpcCompletedColumn : ""; if (sendNotifies) { DescRules = descRules; DescReward = descReward; DescCompleted = descCompleted; } } public override void RefreshLocalization() { OnPropertyChanged("NameLocalized"); OnPropertyChanged("LocationLocalized"); UpdateCachedText(); } } } ================================================ FILE: sources/ui/modelproxy/RuleModelProxy.cs ================================================ using System; namespace FFTriadBuddy.UI { // viewmodel wrapper for model class: modifier / rule public class RuleModelProxy : LocalizedViewModel, IComparable, IImageHashMatch { public readonly TriadGameModifier modOb; public string NameLocalized => modOb.GetLocalizedName(); public RuleModelProxy(TriadGameModifier triadMod) { modOb = triadMod; } public int CompareTo(object obj) { var otherRule = obj as RuleModelProxy; return (otherRule != null) ? NameLocalized.CompareTo(otherRule.NameLocalized) : 0; } public object GetMatchOwner() { return modOb; } } } ================================================ FILE: sources/ui/modelproxy/SettingsModel.cs ================================================ using MgAl2O4.GoogleAPI; using MgAl2O4.Utils; using System; using System.Threading.Tasks; namespace FFTriadBuddy.UI { public class SettingsModel { public enum CloudSaveState { None, Loaded, Saved, UpToDate, } public static GoogleDriveService CloudStorage; public static event Action OnCloudStorageApiUpdate; public static event Action OnCloudStorageStateUpdate; private static bool cloudSettingsInitialized = false; private static bool cloudSettingsCanSave = false; private static object cloudSettingsSyncLock = new object(); private static Task cloudSettingsUpdateTask; private static CloudSaveState cachedSaveState; private static GoogleDriveService.EState cachedApiState; public static void Initialize() { var settingsDB = PlayerSettingsDB.Get(); bool loaded = settingsDB.Load(); if (loaded) { settingsDB.SaveBackup(); } else { Logger.WriteLine("Warning: failed to load player settings!"); } if (!string.IsNullOrEmpty(settingsDB.forcedLanguage)) { LocalizationDB.SetCurrentUserLanguage(settingsDB.forcedLanguage); } if (settingsDB.useXInput) { XInputStub.StartPolling(); } CloudStorage = new GoogleDriveService( GoogleClientIdentifiers.Keys, new GoogleOAuth2.Token() { refreshToken = settingsDB.cloudToken }); } public static void Close() { lock (cloudSettingsSyncLock) { cloudSettingsCanSave = false; } var settingsDB = PlayerSettingsDB.Get(); if (settingsDB.isDirty && settingsDB.useCloudStorage) { _ = CloudStorageSave(); } settingsDB.Save(); } public static void SetUseCloudSaves(bool useCloud) { GoogleOAuth2.KillPendingAuthorization(); var settingsDB = PlayerSettingsDB.Get(); settingsDB.useCloudStorage = useCloud; if (useCloud && CloudStorage != null) { if (!cloudSettingsInitialized) { CloudStorageInit(); } else { CloudStorageSendNotifies(CloudSaveState.UpToDate); } } lock (cloudSettingsSyncLock) { cloudSettingsCanSave = useCloud; } if (cloudSettingsUpdateTask == null) { cloudSettingsUpdateTask = new Task(async () => { const int intervalMs = 2 * 60 * 1000; while (true) { await Task.Delay(intervalMs); bool canSave = false; lock (cloudSettingsSyncLock) { canSave = cloudSettingsCanSave; } if (canSave) { if (PlayerSettingsDB.Get().isDirty) { await CloudStorageSave(); } else { CloudStorageSendNotifies(CloudSaveState.UpToDate); } } } }); cloudSettingsUpdateTask.Start(); } } public static async void CloudStorageInit() { cachedApiState = GoogleDriveService.EState.AuthInProgress; OnCloudStorageApiUpdate.Invoke(cachedApiState); try { await CloudStorage.InitFileList(); } catch (Exception ex) { Logger.WriteLine("Exception: " + ex); } OnCloudStorageApiUpdate.Invoke(CloudStorage.GetState()); var settingsDB = PlayerSettingsDB.Get(); settingsDB.cloudToken = CloudStorage.GetAuthToken().refreshToken; bool needsSave = await CloudStorageLoad(); cloudSettingsInitialized = true; if (needsSave) { await CloudStorageSave(); } } private static async Task CloudStorageLoad() { string fileContent = null; try { fileContent = await CloudStorage.DownloadTextFile("FFTriadBuddy-settings.json"); Logger.WriteLine("Loaded cloud save, API response: " + CloudStorage.GetLastApiResponse()); } catch (Exception ex) { Logger.WriteLine("Exception: " + ex); } CloudStorageSendNotifies(CloudSaveState.Loaded); bool needsSave = true; if (!string.IsNullOrEmpty(fileContent)) { needsSave = PlayerSettingsDB.Get().MergeWithContent(fileContent); } return needsSave; } private static async Task CloudStorageSave() { string fileContent = PlayerSettingsDB.Get().SaveToString(); if (!string.IsNullOrEmpty(fileContent)) { try { await CloudStorage.UploadTextFile("FFTriadBuddy-settings.json", fileContent); Logger.WriteLine("Created cloud save, API response: " + CloudStorage.GetLastApiResponse()); } catch (Exception ex) { Logger.WriteLine("Exception: " + ex); } CloudStorageSendNotifies(CloudSaveState.Saved); } } private static void CloudStorageSendNotifies(CloudSaveState state) { cachedApiState = CloudStorage.GetState(); cachedSaveState = state; CloudStorageRequestState(); } public static void CloudStorageRequestState() { if (cachedApiState == GoogleDriveService.EState.NoErrors) { OnCloudStorageStateUpdate.Invoke(cachedSaveState); } else { OnCloudStorageApiUpdate.Invoke(cachedApiState); } } } } ================================================ FILE: sources/ui/modelproxy/TournamentModelProxy.cs ================================================ using System; namespace FFTriadBuddy.UI { // viewmodel wrapper for model class: tournament public class TournamentModelProxy : LocalizedViewModel, IComparable { public readonly TriadTournament tournamentOb; public string NameLocalized => tournamentOb.Name.GetLocalized(); private string descRules; public string DescRules { get { return descRules; } set { descRules = value; OnPropertyChanged(); } } public TournamentModelProxy(TriadTournament triadTournament) { tournamentOb = triadTournament; UpdateCachedText(); } public int CompareTo(object obj) { var otherTournament = obj as TournamentModelProxy; return (otherTournament != null) ? NameLocalized.CompareTo(otherTournament.NameLocalized) : 0; } public void UpdateCachedText(bool sendNotifies = true) { var newDescRules = ""; foreach (var rule in tournamentOb.Rules) { if (newDescRules.Length > 0) { newDescRules += ", "; } newDescRules += rule.GetLocalizedName(); } if (newDescRules.Length == 0) { newDescRules = loc.strings.MainForm_Dynamic_RuleListEmpty; } descRules = newDescRules; if (sendNotifies) { DescRules = descRules; } } public override void RefreshLocalization() { OnPropertyChanged("NameLocalized"); UpdateCachedText(); } } } ================================================ FILE: sources/ui/modelproxy/TriadGameModel.cs ================================================ using MgAl2O4.Utils; using System; using System.Collections.Generic; namespace FFTriadBuddy.UI { // model / state for keeping current game public class TriadGameModel { public class Move { public TriadCard Card; public int CardIdx; public int BoardIdx; public SolverResult WinChance; } public TriadNpc Npc { get; private set; } public TriadDeck PlayerDeck { get; private set; } public List Rules { get; } = new List(); public TriadGameSolver Solver = new TriadGameSolver(); public TriadGameSimulationState GameState = null; public SolverResult CachedWinChance; public List UndoStateRed = new List(); public TriadGameSimulationState UndoStateBlue = null; public event Action OnNpcChanged; public event Action OnDeckChanged; public event Action OnSetupChanged; public event Action OnGameStateChanged; public event Action OnCachedWinChanceChanged; private bool isTournament = false; public TriadGameModel() { TriadNpc npcOb = TriadNpcDB.Get().npcs.Find(x => x?.Id == PlayerSettingsDB.Get().lastNpcId); if (npcOb == null) { npcOb = TriadNpcDB.Get().Find("Triple Triad master"); } SetNpc(npcOb); } public void SetNpc(TriadNpc npc) { if (npc == Npc) { return; } Logger.WriteLine("Game.SetNpc: {0}:{1}", npc?.Id, npc?.Name.GetCodeName()); Npc = npc; OnNpcChanged?.Invoke(npc); var useDeck = FindDeckToUseFor(npc); SetPlayerDeck(useDeck, notifySetupChange: false); UpdateSession(); PlayerSettingsDB.Get().lastNpcId = npc.Id; } public void SetGameRules(List mods) { if (mods.Count == 2) { SetGameRules(mods[0], mods[1], true); } } public void SetGameRules(TriadGameModifier mod1, TriadGameModifier mod2, bool isTournament = false) { this.isTournament = isTournament; Logger.WriteLine("Game.SetRules: '{0}' + '{1}'{2}", mod1?.GetCodeName(), mod2?.GetCodeName(), isTournament ? " (tournament mode)" : ""); Rules.Clear(); Rules.Add(mod1); Rules.Add(mod2); UpdateSession(); } public void SetPlayerDeck(TriadDeck deck, bool notifySetupChange = true) { Logger.WriteLine("Game.SetPlayerDeck: {0}", deck); PlayerDeck = deck; if (Npc != null && deck != null) { PlayerSettingsDB.Get().UpdatePlayerDeckForNpc(Npc, deck); } OnDeckChanged?.Invoke(deck); if (notifySetupChange) { OnSetupChanged?.Invoke(this); } } public void SetCachedWinChance(TriadDeck deck, SolverResult winChance) { if (PlayerDeck.Equals(deck)) { CachedWinChance = winChance; OnCachedWinChanceChanged?.Invoke(this); } } public void ResolveSpecialRule(ETriadGameSpecialMod specialMod) { GameState.resolvedSpecial |= specialMod; OnGameStateChanged?.Invoke(GameState, null); } private TriadDeck FindDeckToUseFor(TriadNpc npc) { PlayerSettingsDB settingsDB = PlayerSettingsDB.Get(); TriadCard[] cardsCopy = null; if (settingsDB.lastDeck.ContainsKey(npc)) { TriadDeck savedDeck = PlayerSettingsDB.Get().lastDeck[npc]; if (savedDeck != null && savedDeck.knownCards.Count == 5) { cardsCopy = savedDeck.knownCards.ToArray(); } } if (cardsCopy == null) { cardsCopy = new TriadCard[5]; if (PlayerDeck == null) { Array.Copy(settingsDB.starterCards, cardsCopy, cardsCopy.Length); } else { Array.Copy(PlayerDeck.knownCards.ToArray(), cardsCopy, cardsCopy.Length); } } return new TriadDeck(cardsCopy); } private void UpdateSession(bool notifySetupChange = true) { Solver = new TriadGameSolver(); Solver.InitializeSimulation(Rules, isTournament ? null : Npc.Rules); #if DEBUG Solver.agent.debugFlags = TriadGameAgent.DebugFlags.AgentInitialize | TriadGameAgent.DebugFlags.ShowMoveStart | TriadGameAgent.DebugFlags.ShowMoveDetails | TriadGameAgent.DebugFlags.ShowMoveDetailsRng; #endif // DEBUG GameReset(); if (notifySetupChange) { OnSetupChanged?.Invoke(this); } } public void GameReset() { Logger.WriteLine("Game.Reset"); GameState = Solver.StartSimulation(PlayerDeck, Npc.Deck, ETriadGameState.InProgressRed); UndoStateBlue = null; UndoStateRed.Clear(); OnGameStateChanged?.Invoke(GameState, null); } public void GameUndoRed() { if (UndoStateRed.Count > 0) { GameState = UndoStateRed[UndoStateRed.Count - 1]; UndoStateRed.RemoveAt(UndoStateRed.Count - 1); OnGameStateChanged?.Invoke(GameState, null); } } public void GameStartBlue() { if (GameState != null && GameState.numCardsPlaced == 0) { GameState.state = ETriadGameState.InProgressBlue; GamePlayBlueCard(); } } public void SetGameForcedBlueCard(TriadCard card) { if (GameState != null && GameState.state == ETriadGameState.InProgressRed && UndoStateBlue != null) { var blueDeckEx = GameState.deckBlue as TriadDeckInstanceManual; if (Solver.HasSimulationRule(ETriadGameSpecialMod.BlueCardSelection) && blueDeckEx != null) { int deckSlotIdx = blueDeckEx.GetCardIndex(card); if (GameState.forcedCardIdx != deckSlotIdx && !blueDeckEx.IsPlaced(deckSlotIdx)) { Logger.WriteLine("Force blue card: {0}", card.Name.GetCodeName()); GameState = UndoStateBlue; GameState.forcedCardIdx = deckSlotIdx; GamePlayBlueCard(); } } } } public void SetGameRedCard(TriadCard card, int boardIdx) { if (GameState != null) { GameState.forcedCardIdx = -1; TriadGameSimulationState newUndoState = new TriadGameSimulationState(GameState); Logger.WriteLine("Red> [{0}]: {1}", boardIdx, card.Name.GetCodeName()); GameState.bDebugRules = true; bool bPlaced = Solver.simulation.PlaceCard(GameState, card, ETriadCardOwner.Red, boardIdx); GameState.bDebugRules = false; // additional debug logs int numBoardPlaced = 0; { int availBoardMask = 0; for (int Idx = 0; Idx < GameState.board.Length; Idx++) { if (GameState.board[Idx] != null) { numBoardPlaced++; } else { availBoardMask |= (1 << Idx); } } Logger.WriteLine(" Board cards:{0} ({1:x}), placed:{2}", numBoardPlaced, availBoardMask, bPlaced); } if (bPlaced) { if (numBoardPlaced == GameState.board.Length) { OnGameStateChanged?.Invoke(GameState, null); } else { GamePlayBlueCard(); } UndoStateRed.Add(newUndoState); } } } private void GamePlayBlueCard() { if (GameState.state == ETriadGameState.InProgressBlue) { if (Solver.HasSimulationRule(ETriadGameSpecialMod.BlueCardSelection)) { UndoStateBlue = new TriadGameSimulationState(GameState); } bool hasMove = Solver.FindNextMove(GameState, out int bestCardIdx, out int bestNextPos, out SolverResult bestChance); if (hasMove) { var bestCardOb = GameState.deckBlue.GetCard(bestCardIdx); Logger.WriteLine("Blue> [{0}]: {1} => {2}: {3:P0}", bestNextPos, bestCardOb.Name.GetCodeName(), bestChance.expectedResult == ETriadGameState.BlueDraw ? "draw" : "win", bestChance.expectedResult == ETriadGameState.BlueDraw ? bestChance.drawChance : bestChance.winChance); GameState.bDebugRules = true; Solver.simulation.PlaceCard(GameState, bestCardIdx, GameState.deckBlue, ETriadCardOwner.Blue, bestNextPos); GameState.bDebugRules = false; OnGameStateChanged?.Invoke(GameState, new Move() { Card = bestCardOb, CardIdx = bestCardIdx, BoardIdx = bestNextPos, WinChance = bestChance }); } else { OnGameStateChanged?.Invoke(GameState, null); } } } public void GameRouletteApplied() { Logger.WriteLine("Game.Roulette applied"); Solver.simulation.UpdateSpecialRules(); OnSetupChanged?.Invoke(this); } } } ================================================ FILE: sources/ui/view/AdjustCardDialog.xaml ================================================  ================================================ FILE: sources/ui/view/OverlayWindowInteractive.xaml.cs ================================================ using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; namespace FFTriadBuddy.UI { /// /// Interaction logic for OverlayWindowInteractive.xaml /// public partial class OverlayWindowInteractive : Window { [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); public const int GWL_EX_STYLE = -20; public const int WS_EX_APPWINDOW = 0x00040000; public const int WS_EX_TOOLWINDOW = 0x00000080; public const int WS_EX_TRANSPARENT = 0x00000020; public event Action OnPanelMoved; private Point moveStartPt; public OverlayWindowInteractive() { InitializeComponent(); if (PlayerSettingsDB.Get().useXInput) { XInputStub.StartPolling(); } } public void SetOverlayActive(bool wantsActive) { Visibility = wantsActive ? Visibility.Visible : Visibility.Hidden; SetXInputEnble(wantsActive); } private void Window_Loaded(object sender, RoutedEventArgs e) { var helper = new WindowInteropHelper(this).Handle; var wndStyleEx = (OverlayWindowInteractive.GetWindowLong(helper, OverlayWindowInteractive.GWL_EX_STYLE) | OverlayWindowInteractive.WS_EX_TOOLWINDOW) & ~OverlayWindowInteractive.WS_EX_APPWINDOW; OverlayWindowInteractive.SetWindowLong(helper, OverlayWindowInteractive.GWL_EX_STYLE, wndStyleEx); } private void Window_Closed(object sender, EventArgs e) { XInputStub.StopPolling(); } private void Border_MouseMove(object sender, MouseEventArgs e) { if (Mouse.OverrideCursor != null) { if (Mouse.LeftButton != MouseButtonState.Pressed) { Mouse.OverrideCursor = null; } else { var border = sender as Border; var movePt = e.GetPosition(this); var newX = Canvas.GetLeft(border) + movePt.X - moveStartPt.X; var newY = Canvas.GetTop(border) + movePt.Y - moveStartPt.Y; SetPanelCanvasPos(newX, newY); moveStartPt = movePt; } } } public void SetPanelCanvasPos(double x, double y) { var clampedX = Math.Max(0, Math.Min(canvas.ActualWidth - panelCapture.ActualWidth, x)); var clampedY = Math.Max(0, Math.Min(canvas.ActualHeight - panelCapture.ActualHeight, y)); Canvas.SetLeft(panelCapture, clampedX); Canvas.SetTop(panelCapture, clampedY); OnPanelMoved?.Invoke(clampedX, clampedY); } private void Window_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (Mouse.OverrideCursor == Cursors.SizeAll) { Mouse.OverrideCursor = null; } } private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Mouse.OverrideCursor = Cursors.SizeAll; moveStartPt = e.GetPosition(this); } public void SetXInputEnble(bool enable) { if (enable) { XInputStub.OnEventMotionTrigger += XInputEventMotion; } else { XInputStub.OnEventMotionTrigger -= XInputEventMotion; } } private void XInputEventMotion() { App.Current.Dispatcher.Invoke(() => { buttonCapture.Command.Execute(null); }); } } } ================================================ FILE: sources/ui/view/OverlayWindowTransparent.xaml ================================================  ================================================ FILE: sources/ui/view/OverlayWindowTransparent.xaml.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Interop; namespace FFTriadBuddy.UI { /// /// Interaction logic for OverlayWindowTransparent.xaml /// public partial class OverlayWindowTransparent : Window { public FrameworkElement panelCapture; public OverlayWindowTransparent() { InitializeComponent(); } public void SetOverlayActive(bool wantsActive) { Visibility = wantsActive ? Visibility.Visible : Visibility.Hidden; } private void Window_Loaded(object sender, RoutedEventArgs e) { var helper = new WindowInteropHelper(this).Handle; var wndStyleEx = (OverlayWindowInteractive.GetWindowLong(helper, OverlayWindowInteractive.GWL_EX_STYLE) | OverlayWindowInteractive.WS_EX_TOOLWINDOW | OverlayWindowInteractive.WS_EX_TRANSPARENT) & ~OverlayWindowInteractive.WS_EX_APPWINDOW; OverlayWindowInteractive.SetWindowLong(helper, OverlayWindowInteractive.GWL_EX_STYLE, wndStyleEx); } public void SetDetailsCanvasPos(double x = -1, double y = -1) { var clampedX = (x < 0) ? Canvas.GetLeft(panelCapture) : x; var clampedY = (y < 0) ? Canvas.GetTop(panelCapture) : y; if (panelDetails.ActualWidth > 0) { Canvas.SetLeft(panelDetails, clampedX - panelDetails.ActualWidth - 10); Canvas.SetTop(panelDetails, clampedY); } if (panelCapture.ActualWidth > 0) { Canvas.SetLeft(panelBoard, clampedX + panelCapture.ActualWidth + 10); Canvas.SetTop(panelBoard, clampedY); } } private void panelDetails_SizeChanged(object sender, SizeChangedEventArgs e) { SetDetailsCanvasPos(); } private void panelDetails_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue) { SetDetailsCanvasPos(); } } private void panelBoard_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue) { SetDetailsCanvasPos(); } } } } ================================================ FILE: sources/ui/view/PageCards.xaml ================================================  ================================================ FILE: sources/ui/view/PageCards.xaml.cs ================================================ using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; namespace FFTriadBuddy.UI { /// /// Interaction logic for PageCards.xaml /// public partial class PageCards : UserControl { public PageCards() { InitializeComponent(); } private void TabItem_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control) { var findCtxMenu = listCards.FindResource("searchCtx") as ContextMenu; if (findCtxMenu != null) { const int PlacementPadding = 10; findCtxMenu.Placement = PlacementMode.RelativePoint; findCtxMenu.PlacementTarget = listCards; findCtxMenu.VerticalOffset = PlacementPadding; findCtxMenu.IsOpen = true; findCtxMenu.HorizontalOffset = listCards.ActualWidth - findCtxMenu.ActualWidth - PlacementPadding; var textBox = ViewUtils.FindVisualChildRecursive(findCtxMenu, x => x is TextBox) as TextBox; textBox?.Focus(); textBox?.SelectAll(); } } } private void searchTextBox_TextChanged(object sender, TextChangedEventArgs e) { var textBox = sender as TextBox; var pageVM = DataContext as PageCardsViewModel; if (pageVM.CommandSearchCard.CanExecute(textBox.Text)) { pageVM.CommandSearchCard.Execute(textBox.Text); } } private void listCards_ContextMenuOpening(object sender, ContextMenuEventArgs e) { var listView = e.Source as ListView; var pageVM = DataContext as PageCardsViewModel; var paramOb = (listView.ContextMenu.Tag ?? listView.SelectedItem) as CardModelProxy; if (pageVM.CommandBuildContextActions.CanExecute(paramOb)) { pageVM.CommandBuildContextActions.Execute(paramOb); } } } } ================================================ FILE: sources/ui/view/PageInfo.xaml ================================================