JaffarPlus
High-performance best-first search optimizer for tool-assisted speedruns
Loading...
Searching...
No Matches
playback.hpp
Go to the documentation of this file.
1#pragma once
2
9#include "jaffarCommon/deserializers/contiguous.hpp"
10#include "jaffarCommon/hash.hpp"
11#include "jaffarCommon/serializers/contiguous.hpp"
12#include "runner.hpp"
13#include <algorithm>
14#include <string>
15#include <unordered_map>
16#include <vector>
17
18namespace jaffarPlus
19{
20
29class Playback final
30{
31public:
35 struct step_t
36 {
38 std::string inputString;
39
42
45
48
51
53 jaffarCommon::hash::hash_t stateHash;
54
56 std::vector<size_t> _repeatedHashSteps;
57 };
58
63 Playback(Runner& runner) : _runner(&runner)
64 {
65 // Getting game state size
67
68 // Getting renderer state size
70 };
71
84 void initialize(const std::vector<std::string>& inputSequence, bool storeRendererState = true)
85 {
86 _storeRendererState = storeRendererState;
87 // For each input in the sequence, store the game's state
88 for (size_t i = 0; i <= inputSequence.size(); i++)
89 {
90 // Creating new step
91 step_t step;
92
93 // Checking if this is the end of the sequence
94 bool isEndOfSequence = i == inputSequence.size();
95
96 // Setting step input string
97 step.inputString = isEndOfSequence == false ? inputSequence[i] : "<End Of Sequence>";
98
99 // Checking if the input is allowed
100 bool isRegisteredInput = _runner->isInputRegistered(step.inputString);
101
102 // Getting input index
103 if (isEndOfSequence == true) step.inputIndex = 0;
104 if (isEndOfSequence == false && isRegisteredInput == true) step.inputIndex = _runner->getInputIndex(step.inputString);
105 if (isEndOfSequence == false && isRegisteredInput == false) step.inputIndex = _runner->registerInput(step.inputString);
106
107 // Checking if the input is allowed
108 step.isInputAllowed = false;
109 if (isRegisteredInput == true)
110 {
111 auto allowedInputs = _runner->getAllowedInputs();
112 step.isInputAllowed = std::find(allowedInputs.begin(), allowedInputs.end(), step.inputIndex) != allowedInputs.end();
113 }
114
115 // Getting state hash
116 step.stateHash = _runner->computeHash();
117
118 // Recording duplicate states: any earlier steps already filed under this step's hash are
119 // exactly the repeated states the engine would have pruned on encountering them. Looking them
120 // up in a hash map is O(1) amortized, versus the previous O(n^2) scan over every prior step,
121 // so this scales to long movies. The earlier steps are stored in ascending order, matching the
122 // previous behaviour.
123 auto& sameHashSteps = _hashOccurrences[step.stateHash];
124 step._repeatedHashSteps = sameHashSteps;
125 sameHashSteps.push_back(i);
126
127 // Allocating space for the game state data
128 step.gameStateData = malloc(_gameStateSize);
129
130 // Serializing game state
131 jaffarCommon::serializer::Contiguous sg(step.gameStateData, _gameStateSize);
133
134 // Allocating and serializing renderer state (skipped in headless mode to save ~256KB/step)
136 {
138
139 // Updating renderer state
141
142 // Serializing renderer state
143 jaffarCommon::serializer::Contiguous sr(step.rendererStateData, _rendererStateSize);
145 }
146 else
147 step.rendererStateData = nullptr;
148
149 // Advancing state
150 if (i < inputSequence.size()) _runner->advanceState(step.inputIndex);
151 if (i == inputSequence.size()) _runner->advanceState(_sequence.rbegin()->inputIndex);
152
153 // Diagnostic (JAFFAR_DUMP_FULLSTATE_DIR=<dir>): write each step's full serialized state to
154 // <dir>/state<step>.bin. Diffing two replays' dumps at equal depth pinpoints which emulator
155 // fields (beyond RAM) distinguish converging-looking states.
156 if (const char* dumpDir = std::getenv("JAFFAR_DUMP_FULLSTATE_DIR"); dumpDir != nullptr)
157 {
158 std::vector<uint8_t> buf(_gameStateSize);
159 jaffarCommon::serializer::Contiguous s(buf.data(), buf.size());
161 char path[512];
162 snprintf(path, sizeof(path), "%s/state%04lu.bin", dumpDir, i + 1);
163 jaffarCommon::file::saveStringToFile(std::string((char*)buf.data(), buf.size()), path);
164 }
165
166 // Diagnostic (JAFFAR_ROUNDTRIP_PER_STEP=1): serialize+deserialize the runner after every
167 // advance, mimicking the engine's store/reload dynamics. Diffing a --dumpHashes file produced
168 // with this flag against a normal one pinpoints where round-trip advancing forks from live.
169 if (std::getenv("JAFFAR_ROUNDTRIP_PER_STEP") != nullptr)
170 {
171 std::vector<uint8_t> rt(_gameStateSize);
172 {
173 jaffarCommon::serializer::Contiguous s(rt.data(), rt.size());
175 }
176 {
177 jaffarCommon::deserializer::Contiguous d(rt.data(), rt.size());
179 }
180 }
181
182 // Evaluate game rules
184
185 // Determining new game state type
187
188 // Recording the first step at which the solution reaches a win/fail state. Only real applied
189 // inputs are considered (i < size); the i == size iteration re-applies the last input as a
190 // sentinel. The count is "inputs applied" (i + 1), matching the player's step convention.
191 if (i < inputSequence.size())
192 {
193 const auto stateType = _runner->getGame()->getStateType();
194 if (stateType == Game::stateType_t::win && _firstWinStep < 0) _firstWinStep = (ssize_t)i + 1;
195 if (stateType == Game::stateType_t::fail && _firstFailStep < 0) _firstFailStep = (ssize_t)i + 1;
196 }
197
198 // Updating game reward
200
201 // Adding step to the internal storage
202 _sequence.push_back(step);
203 }
204 }
205
210 {
211 // Freeing up memory reserved during initialization
212 for (const auto& step : _sequence)
213 {
214 free(step.gameStateData);
215 if (step.rendererStateData != nullptr) free(step.rendererStateData);
216 }
217 }
218
220 __INLINE__ std::string getStateInputString(const size_t currentStep) const { return getStep(currentStep).inputString; }
222 __INLINE__ jaffarPlus::InputSet::inputIndex_t getStateInputIndex(const size_t currentStep) const { return getStep(currentStep).inputIndex; }
224 __INLINE__ void* getStateData(const size_t currentStep) const { return getStep(currentStep).gameStateData; }
226 __INLINE__ const std::vector<size_t> getStateRepeatedHashSteps(const size_t currentStep) const { return getStep(currentStep)._repeatedHashSteps; }
228 __INLINE__ jaffarCommon::hash::hash_t getStateHash(const size_t currentStep) const { return getStep(currentStep).stateHash; }
230 __INLINE__ bool isInputAllowed(const size_t currentStep) const { return getStep(currentStep).isInputAllowed; }
231
237 __INLINE__ ssize_t getFirstWinStep() const { return _firstWinStep; }
243 __INLINE__ ssize_t getFirstFailStep() const { return _firstFailStep; }
244
249 __INLINE__ void renderFrame(const size_t currentStep)
250 {
251 const auto& step = getStep(currentStep);
252 jaffarCommon::deserializer::Contiguous d(step.rendererStateData, _rendererStateSize);
255 }
256
261 void loadStepData(const size_t stepId)
262 {
263 // Deserializing appropriate state
264 jaffarCommon::deserializer::Contiguous d(getStateData(stepId), _gameStateSize);
266 }
267
271 void printInfo() const
272 {
273 // Now printing information
274 jaffarCommon::logger::log("[J+] Runner Information: \n");
276 jaffarCommon::logger::log("[J+] Game Information: \n");
278 jaffarCommon::logger::log("[J+] Emulator Information: \n");
280 }
281
282private:
289 step_t getStep(const size_t stepId) const
290 {
291 if (stepId >= _sequence.size()) JAFFAR_THROW_RUNTIME("Requested step %lu which exceeds sequence size %lu", stepId, _sequence.size());
292 return _sequence.at(stepId);
293 }
294
300 {
306 size_t operator()(const jaffarCommon::hash::hash_t& h) const noexcept { return h.first ^ (h.second + 0x9E3779B97F4A7C15ULL + (h.first << 6) + (h.first >> 2)); }
307 };
308
311
314
317
320
322 std::vector<step_t> _sequence;
323
325 std::unordered_map<jaffarCommon::hash::hash_t, std::vector<size_t>, hashHasher_t> _hashOccurrences;
326
327 ssize_t _firstWinStep = -1;
328 ssize_t _firstFailStep = -1;
329};
330
331} // namespace jaffarPlus
virtual size_t getRendererStateSize() const =0
Returns the size of the renderer state.
virtual void printInfo() const =0
Prints core-specific debug information.
virtual void deserializeRendererState(jaffarCommon::deserializer::Base &deserializer)=0
Loads the renderer state for a given state/frame from the deserializer.
virtual void showRender()=0
Shows the contents of the emulator's renderer in the window.
virtual void serializeRendererState(jaffarCommon::serializer::Base &serializer) const =0
Gathers the data needed to render a given state/frame into the serializer.
virtual void updateRendererState(const size_t stepIdx, const std::string input)=0
Updates the internal state of the renderer with the current game state.
Emulator * getEmulator() const
Returns a pointer to the internal emulator.
Definition game.hpp:582
void updateReward()
Recomputes the current state's reward from the satisfied rules.
Definition game.hpp:459
void evaluateRules()
Evaluates the rule set against the current state.
Definition game.hpp:363
void printInfo() const
Prints the current game state to the logger.
Definition game.hpp:300
void updateGameStateType()
Recomputes the state type and checkpoint level from the satisfied rules.
Definition game.hpp:417
stateType_t getStateType() const
Returns the current state type (normal, win or fail).
Definition game.hpp:617
@ fail
A fail rule is currently satisfied.
Definition game.hpp:46
@ win
A win rule is currently satisfied.
Definition game.hpp:45
size_t inputIndex_t
Type used to index an input.
Definition inputSet.hpp:29
Replays a solution's input sequence and caches per-step state for navigation.
Definition playback.hpp:30
ssize_t getFirstWinStep() const
Returns the first step (number of inputs applied) at which the solution reaches a win state,...
Definition playback.hpp:237
Playback(Runner &runner)
Constructs the playback over a runner and caches state sizes.
Definition playback.hpp:63
std::vector< step_t > _sequence
The recorded sequence of playback steps.
Definition playback.hpp:322
step_t getStep(const size_t stepId) const
Returns the cached step with the given id.
Definition playback.hpp:289
bool _storeRendererState
Whether to cache the per-step renderer framebuffer (disabled in headless mode to save memory).
Definition playback.hpp:319
void printInfo() const
Prints runner, game, and emulator information.
Definition playback.hpp:271
~Playback()
Frees the game and renderer state memory allocated during initialization.
Definition playback.hpp:209
bool isInputAllowed(const size_t currentStep) const
Returns whether the input of the given step is allowed by the current move set.
Definition playback.hpp:230
ssize_t getFirstFailStep() const
Returns the first step (number of inputs applied) at which the solution reaches a fail state,...
Definition playback.hpp:243
size_t _gameStateSize
Size, in bytes, of a serialized game state.
Definition playback.hpp:313
void initialize(const std::vector< std::string > &inputSequence, bool storeRendererState=true)
Replays the input sequence, recording one cached step per input (plus a trailing end-of-sequence step...
Definition playback.hpp:84
size_t _rendererStateSize
Size, in bytes, of a serialized renderer state.
Definition playback.hpp:316
jaffarPlus::InputSet::inputIndex_t getStateInputIndex(const size_t currentStep) const
Returns the input index of the given step.
Definition playback.hpp:222
Runner * _runner
Pointer to the runner used for playback.
Definition playback.hpp:310
std::string getStateInputString(const size_t currentStep) const
Returns the input string of the given step.
Definition playback.hpp:220
std::unordered_map< jaffarCommon::hash::hash_t, std::vector< size_t >, hashHasher_t > _hashOccurrences
Maps each state hash to the steps (ascending) at which it occurred, used to detect the repeated state...
Definition playback.hpp:325
jaffarCommon::hash::hash_t getStateHash(const size_t currentStep) const
Returns the state hash of the given step.
Definition playback.hpp:228
ssize_t _firstFailStep
First step (inputs applied) reaching a fail state; -1 until/unless one is seen.
Definition playback.hpp:328
void * getStateData(const size_t currentStep) const
Returns the serialized game state data of the given step.
Definition playback.hpp:224
const std::vector< size_t > getStateRepeatedHashSteps(const size_t currentStep) const
Returns the earlier steps (ascending) sharing the given step's hash.
Definition playback.hpp:226
void loadStepData(const size_t stepId)
Loads the cached game state of the given step back into the runner.
Definition playback.hpp:261
ssize_t _firstWinStep
First step (inputs applied) reaching a win state; -1 until/unless one is seen.
Definition playback.hpp:327
void renderFrame(const size_t currentStep)
Renders the cached frame for the given step into the emulator window.
Definition playback.hpp:249
Owns a Game instance and advances it according to configured inputs.
Definition runner.hpp:38
InputSet::inputIndex_t registerInput(const std::string &input)
Registers an input string and returns its numeric index.
Definition runner.hpp:192
const auto getAllowedInputs() const
Returns the inputs currently allowed for the game's state.
Definition runner.hpp:251
size_t getStateSize() const
Computes the size in bytes of the serialized runner state.
Definition runner.hpp:384
void printInfo() const
Logs runner state information.
Definition runner.hpp:472
jaffarPlus::InputSet::inputIndex_t getInputIndex(const std::string &input) const
Looks up the index registered for an input string.
Definition runner.hpp:274
void advanceState(const InputSet::inputIndex_t inputIdx)
Advances the game by one input, then by the configured number of frameskip frames.
Definition runner.hpp:309
jaffarCommon::hash::hash_t computeHash() const
Computes a hash of the current runner state.
Definition runner.hpp:426
Game * getGame() const
Returns a pointer to the owned game instance.
Definition runner.hpp:516
void serializeState(jaffarCommon::serializer::Base &serializer) const
Serializes the runner state: the game state, the input history, and the input counter.
Definition runner.hpp:360
bool isInputRegistered(const std::string &inputString)
Reports whether an input string has been registered.
Definition runner.hpp:292
void deserializeState(jaffarCommon::deserializer::Base &deserializer)
Restores the runner state: the game state, the input history, and the input counter.
Definition runner.hpp:372
Drives a Game forward one input at a time, managing the allowed/candidate input sets,...
Hash functor for 128-bit state hashes (std::pair<uint64_t, uint64_t>).
Definition playback.hpp:300
size_t operator()(const jaffarCommon::hash::hash_t &h) const noexcept
Combines the two 64-bit halves of a state hash into a size_t.
Definition playback.hpp:306
A single recorded playback step.
Definition playback.hpp:36
void * rendererStateData
The step's serialized renderer state data.
Definition playback.hpp:50
bool isInputAllowed
Whether the move is allowed by the current move set.
Definition playback.hpp:44
jaffarCommon::hash::hash_t stateHash
The step's state hash.
Definition playback.hpp:53
void * gameStateData
The step's serialized game state data.
Definition playback.hpp:47
jaffarPlus::InputSet::inputIndex_t inputIndex
The step's input index.
Definition playback.hpp:41
std::vector< size_t > _repeatedHashSteps
Earlier steps (ascending) that shared this step's hash.
Definition playback.hpp:56
std::string inputString
The step's input string.
Definition playback.hpp:38