New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewriting core game loop to be tick based #329

Open
jleclanche opened this Issue Mar 17, 2016 · 9 comments

Comments

Projects
None yet
3 participants
@jleclanche
Owner

jleclanche commented Mar 17, 2016

Currently, every call to modify the game will immediately finish. This clashes with the way choices work in core Hearthstone.

When a choice card is played, the game loop is interrupted until the choice input resolves. This means that, for example, when playing a Discover card with a Knife Juggler on the board, the knife will trigger after the Battlecry completes, which includes the resolution of the choice. In fireplace, the battlecry will finish without waiting for the choice - but fireplace will prevent the user from performing further actions on the game.

A more powerful implementation of the Choice mechanic is blocked by this. Eye of Orsis (LOEA16_13) most notably requires such an implementation: The resolution of the choice is needed to complete the play action (the remainder of the play action being "give the player two more copies of the picked card").

A tick-based loop would also potentially simplify the Kettle server, defining clearly when to check for updates.

Finally, this has performance implications and is potentially more easily implemented in a copy-on-write design, so CC @smallnamespace

@zombie

This comment has been minimized.

Contributor

zombie commented Mar 17, 2016

(note: my current understanding of HS gameloop/ticks is still blurry, so excuse any mistakes)

this looks like major surgery on the engine, being able to pause any "script", only to be continued later? you might get away with doing that for cards defined with your declarative DSL, but for general python methods? and rewriting every test to be async? good luck with all that.. ;)

i'm far from doing this, but my current plan is to use continuations (promises/generators/tasks). that way, i could easily just yeild at the place in code where the script needs to pause to wait for further input, and resume it upon receiving it.

alas, my understanding of high-level python is limited, so i'm unsure how easy this would be using python generators (or possible at all).

@jleclanche

This comment has been minimized.

Owner

jleclanche commented Mar 17, 2016

Well actually, the few remaining cards that don't use the DSL do use yield. They only use Python to determine which actions to send to the game buffer - they don't modify the game state from Python.

@smallnamespace

This comment has been minimized.

Collaborator

smallnamespace commented Mar 30, 2016

@jleclanche

Are there other advantages to moving fully to a tick-based loop, besides being nice for kettle?

It seems to me that narrowly redefining the semantics of playing a choice card (preventing resolution and moving the game engine to an AWAITING_CHOICE state, where further updates are enqueued) would suffice for this.

OTOH, I am very partial to a copy-on-write design since it makes cloning and in particular tree search much nicer, but doing this nicely would require some major surgery.

For example, one can imagine defining game states as a base state + a stack of deltas, so that keeping history of game state is cheap in memory, but to really get mileage out of this you'd need to re-architect every object in the game. You would also need to have some sane rules for when to collapse or cache deltas, and that tradeoff really depends on the particular use case.

In short, my concern is that changing to a tick-based engine might be more up-front work for less immediate benefit, but would like to hear your thoughts.

@jleclanche

This comment has been minimized.

Owner

jleclanche commented Mar 30, 2016

I have some reservations specifically on lazy evaluators, which is an issue Blizzard hit when they came up against eg. Reno Jackson which had to reevaluate POWERED_UP every tick (big performance hit for a duplicate checker).

Right now in fireplace, powered_up is a lazy property which only gets evaluated if it's being looked at. A tick-based architecture would potentially mean it would have to be evaluated every tick, the way Blizzard does it.

There's some other bits and pieces like that, eg. secret exhaustion and so on. Initially, auras worked like that as well.. turns out that doesn't work too well because Hearthstone cares about the results of aura deltas and when they happen (cf. rivendare out of sneeds and similar edge case scenarios).

I also think pausing execution with AWAITING_CHOICE might be more work than implementing a tick based loop.

Regarding COW deltas, my main concern is when a lot of aura updates happen at once and the game runs long. You can see this in HS' game logs, they can easily reach megabytes for single games (of very verbose text, but still).

@jleclanche

This comment has been minimized.

Owner

jleclanche commented Apr 18, 2016

Been thinking about this for a few hours.

I think for the time being, I can add a waiting state to the game. Opening a choice would set it, preventing execution of further actions (and erroring on API calls).

The problem is, action calls would still continue right now, so I'd have to modify all the current low level logic to check for the waiting state. How do we then resume that?

For mulligan, it's easy: we trigger things at the end of the MulliganChoice. But for Discover battlecries it's a lot harder because the Battlecry has to be paused. It's either that, or saving the python stack and resuming it later... nasty. Yeah I definitely think for Discover we have to switch to ticks.

I'll see if I can implement a fix for Mulligan since it's a lot more problematic.

@smallnamespace

This comment has been minimized.

Collaborator

smallnamespace commented Apr 18, 2016

Why would you need to resume the action calls? My first impression would be that once you're in the waiting-for-choice state, nothing can happen until you make a choice, including actions.

@jleclanche

This comment has been minimized.

Owner

jleclanche commented Apr 18, 2016

And how do you intend to do that? I would have to implement an async framework or some kind of event system at the very least. If we're going to start doing such massive rework, might as well do it proper and implement ticks.

jleclanche added a commit that referenced this issue Apr 18, 2016

Implement a proper Mulligan phase, blocking game start until completed
The game no longer automatically continues after the Mulligan choices
have been offered. Instead, the first turn will begin as a callback
at the completion of both Mulligan choices.

Adds an extensive test for mulligan logic.

Note that cards are still not replaced in-place... if only some cards
are replaced, the hand will always have the initial cards on the left
and the replacement ones on the right.

Touches #329

Closes #334
@smallnamespace

This comment has been minimized.

Collaborator

smallnamespace commented Apr 18, 2016

Well, couldn't you check the current state, and just no-op out if we're awaiting a choice? Or would that be the massive rework that you're alluding to?

It might not be that painful -- you could have a check function as a decorator and just decorate the entry points.

@smallnamespace

This comment has been minimized.

Collaborator

smallnamespace commented Apr 18, 2016

So for a tick-based system, where do you envision the tick boundaries would lie?

I could imagine one extreme, where each complete tick is initiated by a player action and runs until all queues are emptied. That would be pretty close to what we have now, except we don't call them 'ticks'.

The other extreme might be yielding at the completion of every action or phase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment