-
Notifications
You must be signed in to change notification settings - Fork 100
Changelog
Add Util.DeepCloneableRandom
- Add Game.Random
- Now each game has an independent random number generator.
- Now user can provide a random seed for each game using GameConfig
- The seed of Game.Random can be reset using
Random.SetSeed(seed)
method
- Remove Util.Random
- Now all Util's helper functions using Random requires a random number generator as its parameter.
- Add Game.Clone(bool resetRandomSeed) parameter The last cloneable random commit accidentally make all cloned games have its prototype's Random, which is different with the default behaviour of Clone(). Instead, if you want to clone a game with the same random number generator(up to the moment), use Game.Clone(resetRandomSeed = false)
Miscellaneous
- Add Cards.BasicTotems
- Add an Exception debugger for tasks (WIP)
Test environment:
- Processor: AMD Ryzen 5 2600
- Memory: DDR4 PC4-21300 16GB
- OS: Windows 10
- Framework: .NET Core 2.1
- Compiler Switch Settings: /optimize+ /debug:pdbonly
- GameConfig: History: false, Logging: false
- Run 20000 games with Innkeeper Expert decks. Action are randomly chosen from all available actions. (10000 games for cloning.)
- All measurements are recorded in seconds. (seconds per 20000 games (10000 per cloning))
- The test process used only 1 thread.
2.0 | Druid | Hunter | Mage | Paladin | Priest | Rogue | Shaman | Warlock | Warrior | Average |
---|---|---|---|---|---|---|---|---|---|---|
Controller.Options() |
11.99 | 6.57 | 10.85 | 7.84 | 16.15 | 8.55 | 10.78 | 6.93 | 9.5 | 9.91 |
Game.Process() |
7.87 | 6.45 | 8.68 | 7.45 | 9.3 | 5.93 | 8.61 | 6.28 | 8.44 | 7.67 |
Game.Clone() |
55.92 | 16.73 | 22.47 | 21.72 | 22.24 | 16.33 | 24.41 | 14.25 | 22.18 | 24.03 |
2.1 | ||||||||||
Controller.Options() |
2.98 | 1.69 | 2.65 | 2.08 | 3.37 | 1.96 | 2.19 | 1.49 | 2.2 | 2.29 |
Game.Process() |
3.48 | 3.04 | 3.93 | 3.38 | 3.77 | 2.59 | 3.35 | 2.78 | 3.67 | 3.33 |
Game.Clone() |
39.6 | 12.64 | 16.83 | 16.12 | 16.45 | 11.95 | 15.82 | 10.42 | 15.7 | 17.28 |
Rate (%) (2.0 / 2.1) * 100 | ||||||||||
Controller.Options() |
402.35 | 388.76 | 409.43 | 376.92 | 479.23 | 436.22 | 492.24 | 465.10 | 431.82 | 432.75 |
Game.Process() |
226.15 | 212.17 | 220.87 | 220.41 | 246.68 | 228.96 | 257.01 | 225.90 | 229.98 | 230.33 |
Game.Clone() |
141.21 | 132.36 | 133.51 | 134.74 | 135.20 | 136.65 | 154.30 | 136.76 | 141.27 | 139.06 |
- 4.3x faster options generation.
- 2.3x faster processing.
- 1.4x faster cloning.
-
95% of Boomsday cards are implemented.
-
90% of The Grant Tornament cards are implemented.
-
List of not yet implemented cards:
- [BOT_299] Omega Assembly
- [BOT_406] Supercollider
- [BOT_436] Prismatic Lens
- [BOT_453] Shooting Star
- [BOT_912] Kangor's Endless Army
- [BOT_914] Whizbang the Wonderful
- [GIL_198] Azalina Soulthief
- [GIL_655] Festeroot Hulk
- [GIL_681] Nightmare Amalgam
- Most
- [AT_003] Fallen Hero
- [AT_004] Arcane Blast
- [AT_005] Polymorph: Boar
- [AT_027] Wilfred Fizzlebang
- [AT_034] Poisoned Blade
- [AT_056] Powershot
- [AT_067] Magnataur Alpha
- [AT_079] Mysterious Challenger
- [AT_081] Eadric the Pure
- [AT_086] Saboteur
- [AT_088] Mogor's Champion
- [AT_115] Fencing Coach
- [AT_130] Sea Reaver
- [EX1_112] Gelbin Mekkatorque
- [NEW1_016] Captain's Parrot
- [PRO_001] Elite Tauren Chieftain
- [GVG_007] Flame Leviathan
- [GVG_014] Vol'jin
- [GVG_016] Fel Reaver
- [GVG_017] Call Pet
- [GVG_021] Mal'Ganis
- [GVG_025] One-eyed Cheat
- [GVG_046] King of Beasts
- [GVG_049] Gahz'rilla
- [GVG_050] Bouncing Blade
- [GVG_054] Ogre Warmaul
- [GVG_065] Ogre Brute
- [GVG_066] Dunemaul Shaman
- [GVG_074] Kezan Mystic
- [GVG_077] Anima Golem
- [GVG_087] Steamwheedle Sniper
- [GVG_088] Ogre Ninja
- [GVG_092] Gnomish Experimenter
- [GVG_097] Lil' Exorcist
- [GVG_107] Enhance-o Mechano
- [GVG_108] Recombobulator
- [GVG_111] Mimiron's Head
- [GVG_112] Mogor the Ogre
- [GVG_113] Foe Reaper 4000
- [GVG_119] Blingtron 3000
- [GVG_122] Wee Spellstopper
- [LOE_009] Obsidian Destroyer
- [LOE_022] Fierce Monkey
- [LOE_023] Dark Peddler
- [LOE_029] Jeweled Scarab
- [LOE_046] Huge Toad
- [LOE_047] Tomb Spider
- [LOE_051] Jungle Moonkin
- [LOE_053] Djinni of Zephyrs
- [LOE_061] Anubisath Sentinel
- [LOE_073] Fossilized Devilsaur
- [LOE_092] Arch-Thief Rafaam
- [LOE_107] Eerie Statue
- [LOE_118] Cursed Blade
- [LOE_119] Animated Armor
- [LOEA10_3] Murloc Tinyfin
You can get ReadOnlySpan<T>
from these LimitedZone<T>
with LimitedZone<T>.GetSpan()
. The method is already used in some part of the simulator, but it should be exploited more.
Span<T>
and Memory<T>
is interesting new features from C# 7.2. There seems to be more fascinating application of these features in SabberStone. I have been designing new faster inner structure using these and that would be the one of the future performance improvement.
Previously each task needed to contain. instance-specific information. Which needed cloning every time when it is processed. To avoid the situation, now tasks do not contains any game-instance specific objects like IEntity Source
. Instead now SimpleTask.Process()
requires these objects as arguments.
With this change, it is even possible that all tasks can be a singleton instance. This scheme would be implemented in near future too and may reduce some memory overheads.
Slight performance and stability improvements.
Basically almost all attributes and properties of Entity
is stored in EntityData
as pairs of GameTag
and int
. This allows a lot of generality and readability within this simulator and makes us help how actual Hearthstone works. However, reading and writing these tags from dictionary tags from dictionary consume the most of times during the runtime.
To mitigate the problem, some of frequently used attributes like ATK and Health is now stored in fields internally.
Note
Please use properties for major attributes instead of direct tag queries for performance and stability. For example, it is still possible to get ATK attribute from Entity[GameTag.ATK]
but Character.AttackDamage
would be slightly faster and using tag indexers could be buggy in some rare situations.
Previously, like how we get attributes from GameTag
s, all card attributes are stored as tags in Card.Tags
Dictionary. To reduce runtime lookup for tags, these tags are stored in properties in a Card
class. As Card
instances are only created once and singletons, this can be regarded as preprocessing. Static memory usage is increased therefore.
To know which entities in the board are valid targets for a playable, we need to see the card's PlayReq
s underneath the playable. These are immutable and can be preprocessed and compressed to more compact data.
SabberStone checks the legality of each task when it is processed, but you can skip with PlayerTask
s with SkipPrePhase
property (This is not a new feature). Because options generated from Controller.Options()
should be legal if you use the tasks for the same Game
instance that creates the options (or in other controlled situations), so the double-checking would be redundant. Now you can generated all options with SkipPrePhase
using the overload.
This new property is used for Carnivorous Cube's Enchantment and similar other cards and also for "Copying Deathrattle" cards. I think this can be useful when you want to extract state representation from SabberStone.
Previously when you need a games with different starting hands and deck orders, you have to create new games each time. To mitigate this problem now you can call Game.Clone()
before Game.StartGame()
so that you can call clone.StartGame()
to prepare new instances.
For users: we aware and understand well that lack of wikis and documentations discourages people from using and contributing to this project. We will prioritise documentation writing job.
There are still tons of things to be optimised or improved. Also new cards from new expansions would be updated soon!… (and not implemented old cards too).
The internal structure of some parts are extremely dirty and complicated, without comprehensive documentations. I understand that this is a serious problem and I will try to write full documentations for hard-coded parts and to relax complications. However, basic APIs like Process, Clone, and Option and properties are almost the same as before and card scripting is quite easy and not really changed from 2.0..
All feedbacks and questions are very welcomed! Discord: https://discord.gg/my9WTwK (Simulators channel) Email: [email protected]
- Basic => 100% from 142 Cards
- Classic => 100% from 239 Cards
- Whispers of the Old Gods => 100% from 134 Cards
- One Night in Karazhan => 100% from 45 Cards
- Mean Streets of Gadgetzan => 99% from 132 Cards
- Journey to Un'Goro => 97% from 135 Cards
- Knights of the Frozen Throne => 98% from 135 Cards
- Kobolds and Catacombs => 91% from 135 Cards
- Total Standard => 98% from 1097 Cards
- Now Sabber uses the latest version of .NET Core
- Refactoring of some fields (You should fix some of your own codes)
- Entirely new Trigger / Aura / Enchantment system, which uses events and the actual ENCHANTMENT entities
- Mass implementation of missing cards
- Far more accurate gameplay simulation with new task system including the recent gameplay changes
- Insanely optimised performance
- Changed card implementation syntax
Old Name | Refactored Name |
---|---|
SabberStoneCore.Enchants.Enchantment |
SabberStoneCore.Enchants.Power |
SabberStoneCore.Enchants.Enchantments |
SabberStoneCore.Enchants.Powers |
SabberStoneCore.Model.Entities.Hero.Power |
SabberStoneCore.Model.Entities.Hero.HeroPower |
SabberStoneCore.Enchants.Enchantment.SingleTask |
SabberStoneCore.Enchants.Power.PowerTask |
Previously we checked every tag changes to trigger effects. The new trigger design uses events. This way we can reduce the number of tags that used for checking triggers and have no other uses (e.g. JUST_PLAYED, SUMMONED, ...), and can simulate the actual situation more precisely. Secondly, the new trigger system helps us to implement new cards with trigger more conveniently because the new implementation syntax doesn't require us to provide an exact tag or value increment. Finally I hope that this new system would help us to implement precise History Blocks
I decided to revive the dead Enchantment cards, to simulate all things accurately. From now on, most of cards that make changes of tags value of itself or other entities actually gives "Enchantment" entity to the target entities. (e.g. Abusive Sergeant, Preparation, Mana Wyrm, Potion of Madness, Crazed Alchemist, and so on) So we should implement both the enchantment giver and the enchantment card itself. I recommend you to check one of the CardSet file to comprehend how things are changed; I think they are really readable :p
Personally, besides the other things I've done, I am really proud of myself that... I optimised Sabber more than 2X faster with this commit.
Not only the changed mechanism makes the engine faster, but also I've introduced custom hash table for tags, which allows super faster cloning.
I benchmarked with random games between players using the same mage expert deck
(https://www.hearthpwn.com/decks/66047-mage-expert-ai-deck). I separately measured two cases; to see the performance of cloning, I cloned each time PlayerTask is processed and measured the duration of Game.Clone()
. Secondly, I processed random decisions and measured the duration of Game.Process()
I Ran 20000 Games to completion in each round and averaged results from 10 rounds.
for 20,000 full games | Sabber before this commit | Sabber with this commit |
---|---|---|
Game.Process() |
13.4127 s | 6.7383 s (199% faster) |
Game.Clone() |
56.827 s | 26.2228 s (217% faster) |
Basic => 100% from 142 Cards
Classic => 100% from 239 Cards
Whispers of the Old Gods => 99% from 134 Cards
One Night in Karazhan => 100% from 45 Cards
Mean Streets of Gadgetzan => 99% from 132 Cards
Journey to Un'Goro => 95% from 135 Cards
Knights of the Frozen Throne => 92% from 135 Cards
Kobolds and Catacombs => 25% from 135 Cards
Total Standard => 89% from 1097 Cards
I have yet to write hash functions for new structure so It is not possible to check the integrity of the clone for now, but I tested stability of cloning with 100,000 random games with random cards.
I have yet to test with any kind of servers ... but I expect the calibration would be easy than before. There are lot of things to implement to simulate precise history such as CHANGE_ENTITY or REVEAL_ENTITY, and METADATA.
I have yet to convert cardsets in wild format. so wild cards cannot be used for now.
I've been developing a faster Options() algorithm and I will commit it soon