MikeTaylor / scottkit

Scott Adams adventure toolkit: compile, decompile and play 80s-style adventure games
30 stars 10 forks source link

Thoughts on driving the play loop externally? #8

Closed drewish closed 6 years ago

drewish commented 7 years ago

I had a wild idea that I'd like to try to hook up the scottkit play code to a slack bot so you could play it via chat. I've played around with trying to treat it as a shell script and work at the stdin/stdout level but turned into a pretty big PITA. I went in and stripped the binary down to just:

require 'scottkit/game'

file_name = 'game.sao'
options = {}
game = ScottKit::Game.new(options)
game.load(IO.read(file_name))
game.decompile(StringIO.new)
game.play

I think I can just use an StringIO with $stdout to capture the output, clearing it between turns... though I suspect I'll add puts and print methods that target my buffer so I don't have to worry about anything else logging to the global variable.

But I'm not sure quite how to handle the input. The play method's loop calls into the gets method so that seems like a pretty obvious place to hook in. I'm wondering if it'd be feasible to fiber to pipe the input from the slack messages to the game. gets would call thread.resume and wait for input then when slack got something it could call yield to send it across.

Any thoughts?

MikeTaylor commented 7 years ago

Interesting. I'll think about this.

I've also been thinking about trying to write a Zoom plugin for ScottKit games: see http://www.logicalshift.co.uk/mac/zoom.html (I know there is a plugin facility but I can't find any documentation for how to hook into it.) If I get that done, it will probably involve similar changes to the core.

drewish commented 7 years ago

With a little poking I was able to get it down to this to externalize the IO:

require 'scottkit/game'
require 'scottkit/game'

class MyGame < ScottKit::Game
  def gets
    options[:input_fiber].resume
  end

  def puts *args
    options[:output_buffer].puts *args
  end

  def print *args
    options[:output_buffer].print *args
  end
end

file_name = 'game.sao'
output = StringIO.new
my_fiber = Fiber.new do
  while true
    puts output.string
    output.reopen('')
    Fiber.yield(gets)
  end
end
game = MyGame.new(input_fiber: my_fiber, output_buffer: output)
game.load(IO.read(file_name))
game.decompile(StringIO.new)
game.play

Going to see if I can get it hooked up to the slack client next.

MikeTaylor commented 6 years ago

Looks encouraging! Keep me updated.

drewish commented 6 years ago

Turns out my easy fix won't work. The callbacks from slack are coming from a different thread so the fiber isn't able to pass the input across. I'm going to see if I can refactor more of the functionality of the play method into smaller methods and then override the play method and invert the control flow.

MikeTaylor commented 6 years ago

I don't know enough about Slack to have thought on the detail of this approach. But bear this in mind as you progress: my plan with the play method is to introduce the concept of a "driver" object that play invokes several methods of -- crucially, input, output and describe. A simple driver will retain the present play behaviour in a scrolling dialogue; and a curses-based one will provide a ScottFree-like UI where the present location description is always in its own window at the top of the screen. (That's why I need to separate describe from other output.)

Will that scheme help by giving you what you need? Or do you need to rip play apart more brutally than that?

drewish commented 6 years ago

I'm really just trying to pull all the stuff out in there into separate methods so it would be easier to try externalizing the logic. I'll push up a work in progress branch so you can see what I'm looking at.

One thing I was noticing is that it the tests are failing in master it seems like maybe some of them weren't updated for the lighting changes? Do you have a script for running them? Would you be interested in getting Travis setup to run them as part of the PR process? I'm happy to help with that if so.

MikeTaylor commented 6 years ago

The tests need a bit of looking at.

Some of them are rather too precise in what they expect -- for example, there's a complete transcript of solving Scott Adams's Adventureland, which is expected to remain the same. Whenever I change anything in the output format, those tests break and need to be tediously reconfigured. (Check through the git logs, and you'll see a lot of "Rebuild regression expectations" commits.)

I think what we need is a top-level rake target that rebuilds all of the test expectations automatically, so that once we are convinced the code is code we can make an output-format change, then run rake regenerate, and know the tests will work.

MikeTaylor commented 6 years ago

I guess I should have said all that in issue #12 instead. I'll copy it there. Let's discuss on that issue rather than here.