Skip to content
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

Intent to participate: SkiFree, but it's a survival horror novel [now at 50,000] #44

Open
katstasaph opened this issue Nov 6, 2021 · 13 comments

Comments

@katstasaph
Copy link

katstasaph commented Nov 6, 2021

What is this?

A novel that is automatically generated through playing everyone's favorite Windows 95-era game, SkiFree.

Why is this?

I don't even know.

But seriously, why is this?

The direct inspiration was Mario's Big Adventure from last year. I'd been tossing around the idea of doing something like this with old Windows games since I started the Oregon Trail decompilation thing; the benefit of SkiFree is that it actually runs on Windows 10 without the need for a virtual machine. Also, it has a yeti.

How is this?

The code runs SkiFree (not included, but it is available for free in a compatible version), hooks that process, and captures the text printed to screen, i.e., the time/distance/speed/style points displayed in the top right of the game window. The heavy lifting is handled by the Deviare library, which was definitely not intended to be used for this.

To do: Actually turn that text into something resembling a narrative; guarantee and/or stop after 50,000 words.

Real optimistic thinking: Not depend on so many globals; hook other functions (using the text printed is somewhat limited in the info it conveys, but it's the most immediately usable)

Where is this?

Here. Currently it isn't much of a novel, all it does right now is print what's on screen; this is more a proof of concept to extract the text.

@katstasaph
Copy link
Author

katstasaph commented Nov 6, 2021

The basic loop: The text refreshes on its own quite often, so it should be trivial to print a flavor text sentence every time (or almost every time, etc.) TextOutA is called. From there we can get specific; you can get a non-trivial amount of story cues from the text alone. We're extracting strings, and most strings have a unique character sequence. There are two ways to figure out what kind of string we have:

  • The text is always(?) printed in the same order, so just do a looping counter of 0, 1, 2, 3, 0, 1, 2, 3.
  • Analyze the string characters themselves: if it has a colon it's time; else if it has "m/s" it's speed; else if it has "m" it's distance, else it's style.

We can also get cues from how those strings change:

Time

  • Only shows up in slalom games
  • Straightforward, it counts up as time passes, and stops counting once you reach the end

Potential story takeaways:

  • If we want to do separate stories based on the type of ski route we're doing this is one way to get that, if it's anything besides 0 we are
  • We know if we've spent a long time out there

Distance

  • At the beginning of the game, the distance is 0.
  • If you choose a lane (slalom/freestyle/etc) the distance sets itself to a positive number and starts counting down to 0.
  • Once you reach 0, the distance is set to 1000 and starts counting up.
  • If you don't choose a lane, the distance just counts up.
  • Once you reach about 2000m, the yeti starts showing up and the distance is set to negative numbers.
  • If you go up from the starting point, the distance counts down from 0 (i.e. -1, -2...). The yeti starts showing up around -100 or so.

Potential story takeaways:

  • From the beginning, if the distance suddenly jumps from ~30 to whatever the starting values are, we can set a flag that we're on the main path. (to determine "suddenly jumps" we can just get the delta, new distance - old distance) Otherwise we can set a flag that we're on the uncharted path.
  • From there we can determine whether we're backtracking by checking whether our numbers are going up/down.
  • If we reach 0 and it's not the beginning, we have completed one of the main paths. (We could also possibly hook MessageBoxA, you get a popup with your high score in-game.)
  • If we reach 2000 and/or -100 we know the yeti shows up.
  • When the yeti eats you the distance snaps back to 0, so we know that's the end
  • We can have a running counter of deltas between distances (the absolute value of them, i.e., 0 to -2 will count as 2 meters) and this will give us a rough idea of how long we've been out there. (this should be capped, of course)

Speed

  • Straight down mph: 21
  • Slightly left/right: 15 (it doesn't snap immediately to 15, it gradually gets there)
  • Sharply left/right: 7
  • Fully left/right: 0
  • Going up: Negative
  • Jumps boost our speed above 21
  • Snowbanks slow our speed down gradually.

Potential story takeaways:

  • If our speed is 21 we are probably going straight down
  • If our speed ever goes above 21 we know we jumped
  • If our speed stays 15 or 7 for a while we are probably turning
  • If our speed dips below 7 we have likely hit a snowbank or crashed
  • If our speed is negative we are going up.

Style

  • Only shows up during freestyle
  • If it goes up we pulled off some tricks
  • If it goes down we crashed

Potential story takeaways:

  • If it's anything but 0 we're doing freestyle
  • If it goes up or down we know whether we pulled off some tricks or crashed.

All of this should at least be enough cues to do some kind of rudimentary story.

@katstasaph
Copy link
Author

katstasaph commented Nov 7, 2021

Added the beginnings of parsing text into output. The text will vary quite a bit more than this ultimately, but we have now gone from spitting out the text unaltered to this marginally more riveting work (yes, "950m meters" is redundant, but this is placeholder text anyway):

I don't know how long I've been out here.
I've gone 950m meters.
I'm going 21m/s.
Am I skiing well?
I don't know how long I've been out here.
I've gone 943m meters.
I'm going 21m/s.
Am I skiing well?
I don't know how long I've been out here.
I've gone 940m meters.
I'm going 15m/s.
I suck at skiing.
I don't know how long I've been out here.
I've gone 940m meters.
I'm going 06m/s.
I suck at skiing.

At this point, one playthrough produces about ~4,000 words. Clearly it needs some bulking up.

@katstasaph katstasaph changed the title Intent to participate: SkiFree, but it's a novel Intent to participate: SkiFree, but it's a survival horror novel Nov 7, 2021
@hugovk hugovk added the preview label Nov 7, 2021
@katstasaph
Copy link
Author

Added some rudimentary flavor text. At some point I'm probably going to need to add a random chance to make it less spammy, but at this point we need all the wordcount we can get.

Some flavor text is one-time use, other flavor text is not. When I get the delta processing fully fleshed out there will be more tagging. I also need to add verbose/terse variants to most of them, so that the more "unique" strings don't print as often as they do.

Sample output now:

Movement behind me; a noise. Whoosh, colder breeze, dusting up of snow. But when I turn, nothing.
Snow stinging the eyes.
I don't know how long I've been out here.
Was that ski lift there before? And was it empty?
Just keep going...
I've gone 880m meters now.
Snow stinging the eyes.
Do I have any skin left to go numb?
I'm going 03m/s.
Why did I agree to do this?
The scenery blurs into itself.

@katstasaph
Copy link
Author

katstasaph commented Nov 9, 2021

Added the beginnings of the distance delta outline mentioned above, plus some (rudimentary again) flavor text for the intro only.

Right now a typical playthrough nets you ~15000 words. The tailored text will ultimately be more than one short sentence each, which will bulk matters up.

Was that ski lift there before? And was it empty?
Keep moving. There is no other choice.
I don't know how long I've been out here.
Cold beyond cold. Cold as in absence.
Airborne. The slope looks so much bigger from here.
I've gone 743m now.
Snow stinging the eyes.

@katstasaph
Copy link
Author

katstasaph commented Nov 9, 2021

Refactored a bit in order to add the possibility of follow-up sentences. Fleshed those out a bit. Current sample output (one playthrough is now at ~18,000 to 20,000 words):

Cold beyond cold. Cold as in absence. Why did I agree to do this?
I've gone 673m now.
Was that ski lift there before? And was it empty?
I don't remember when I started holding my breath. Something is holding it for me. Why did I agree to do this?
I'm going 00m/s.
If I close my eyes I'll crash.
Snow stinging the eyes. Where have all the people gone?
I suck at skiing.
Wind like encroaching walls.
There are no landmarks anywhere.
It feels like I've been skiing here forever.
If I close my eyes I'll crash. If I don't keep moving I'll freeze.
Each breath takes more effort than the last.
I've gone 674m now. None of the ski lifts acknowledge my struggling upward.
Snow stinging the eyes.
The scenery blurs into itself.
I'm going 00m/s.
Cold in the bones. If I don't keep moving I'll freeze.
There is only one option: keep skiing.

So now we have some acknowledgement of actual in-game events, but even after I implement them all, there is still a lot more going on than is reflected in the text.

A lot of this takes the form of bitmaps appearing, and we do have API bitmap functions that can be hooked. (hooking more than one at once will require some more work on the Deviare side but presumably doable?) Unfortunately we can't just check to see whether a bitmap has been drawn and which it is, because it almost definitely refreshes every frame.

So in order to get at what actually is happening and what exactly is possibly, it's time to consult the decompilation again (using Snowman, I do have Ghidra but old habits, etc.) On that note, I leave you with this excerpt from the decompiled code:

MessageBoxA(ecx123, 0, "Whoa, like, can't load bitmaps!  Yer outa memory, duuude!", eax148, 48);

@lee2sman
Copy link

Really impressive! You've already gone quite far so maybe this won't be useful but one idea i had for the color commentary: you could export youtube video transcript of commentators watching skiing olympics events, then randomly pull some of those sentences in. You can use youtube-dl to download the transcription.

Here's a complicated walkthrough. I bet you could figure out a 'good enough' pipeline that's even simpler.

@lee2sman
Copy link

I realize that my comment above is not so useful since your generated text is a bit more stoic musings, not a sports commentary! You've probably seen Ennuigi I take it! Reminds me of that.

@katstasaph
Copy link
Author

Thanks! The tone isn't 100% settled yet, since the text that's here has sort of been quickly dashed off placeholders. The horror element hasn't really come into play yet.

I wasn't familiar with that previously, also! Kind of similar to my original idea with this.

@katstasaph
Copy link
Author

katstasaph commented Nov 10, 2021

Today's update doesn't add any more wordcount (it's still roughly 20,000 per full run) but does do two things:

Crash detection:
Obviously the only real way to detect a crash is to use the actual code. In lieu of that, we can get a rough idea of whether we've crashed from the text output, erring on the side of false negatives. Two things are checked, if both true a recent crash is detected:

  • Has our speed dropped by over 7 meters/second (7 being the slowest you down the slope normally) in our last three speed measures?
  • Was our last distance delta 0?

I do know this produces false positives if you come to a stop normally in some cases; not sure how to handle this at the moment. It may require doing it the hard/real way.

Post-route detection
This is easy enough, have we gone more than ~1000 meters but less than ~2000?

Crash text sample:

I've gone 967m now.
The scenery blurs into itself.
The snow is infinite. Just keep going...
Crashed. Limbs topple over swaddled limbs and I'm facedown in the show.
Snow that deafens and numbs. There is only one option: keep skiing.
Wind like encroaching walls.
I suck at skiing.
Movement behind me; a noise. Whoosh, colder breeze, dusting up of snow. But when I turn, nothing.
Gravity controls me totally. My legs would ache if I could feel them.

Post-route text sample:

There are no landmarks anywhere.
I've gone 1564m now. The only thing more unsettling than a darkening sky: a sky that never darkens. I can't stop moving. The snow will swallow me, I know.
Just keep going...
Snow that deafens and numbs.
I'm going 21m/s.
How far down can this slope go?
Do I have any skin left to go numb? Where have all the people gone?

@katstasaph
Copy link
Author

Got sidetracked by life but I did replace the style text with slightly less placeholdery versions. We are still however at ~24,000 to ~30,000. Padding must be done.

Snow and snow and snow and snow.
I've gone 1247m now. The colors are sickeningly bright against the snow. Too bright to be real. The snow begins to chatter. It must be the snow. There is nothing else.
If I focus, I'll get out of here faster.
How far down can this slope go?
I'm going 15m/s.
I ski on autopilot, outside myself. Why did I agree to do this?
Snow like static in the ears. The farther I go, the steeper it looks.
I ask myself how I'm skiing. Why?

@katstasaph
Copy link
Author

Getting down to the wire now and still at ~25,000. We can pad more, we can account for multiple playthroughs. We can also pad by adding yeti lines.

The snow is infinite. No time to shiver.
Was that ski lift there before? And was it empty? If I close my eyes I'll crash.
I must have been out here something like 0:02:39.64.
Snow like static in the ears. No time to shiver.
Cold finds its way into everything. My legs would ache if I could feel them.
I've gone 1705 meters now. The snow seems denser here. Deep like quicksand.
An all-encompassing sense of chill.
A world of nothing but ice. An all-encompassing sense of chill.
I've collapsed. Despite myself, I yelp. I hear the words escape my lips. But I hear nothing.

NO ESCAPE NO ESCAPE

@katstasaph
Copy link
Author

Now have it producing roughly ~50K words per playthrough; a sample story is now on the Github.

Still a few things I want to do (like refactor a certain function) but you get the idea!

@katstasaph katstasaph changed the title Intent to participate: SkiFree, but it's a survival horror novel Intent to participate: SkiFree, but it's a survival horror novel [now at 50,000] Dec 1, 2021
@hugovk
Copy link
Member

hugovk commented Dec 1, 2021

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

No branches or pull requests

3 participants