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