MikeTaylor / scottkit

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

Refactor game loop to allow easy external use #16

Closed drewish closed 6 years ago

drewish commented 6 years ago

This "Fixes #8" but is really just a conversation starter. It's currently rebased on #15 so we can ensure the specs pass.

Here's the hacky script I'm using so you can get and idea of how the input and output are handled:

require 'slack-ruby-client'
require 'scottkit/game'

# The game sends it output to stdout but we just capture the strings in a buffer
# and pull them out as chunks which we send in a single message. We ignore the
# play method since it blocks on `gets` and just call some methods directly.
class MyGame < ScottKit::Game
  attr_reader :output_buffer

  def initialize(options)
    @output_buffer = StringIO.new
    super
  end

  def puts *args
    output_buffer.puts *args
  end

  def print *args
    output_buffer.print *args
  end

  # Grab all the output and reset the buffer
  def prompt_for_turn
    super

    string = output_buffer.string
    output_buffer.reopen('')
    string
  end
end

Slack.configure do |config|
  config.token = ENV['SLACK_API_TOKEN']
  config.logger = Logger.new(STDOUT)
  config.logger.level = Logger::INFO
end

client = Slack::RealTime::Client.new
channel = nil

file_name = 'game.sao'
game = MyGame.new(output_buffer: StringIO.new)
game.load(IO.read(file_name))
game.decompile(StringIO.new)

client.on :hello do
  puts "Successfully connected, welcome '#{client.self.name}' to the '#{client.team.name}' team at https://#{client.team.domain}.slack.com."

  # Limit the conversation to one channel for now
  channel = client.channels.values.find { |c| c.name == 'bots' }

  game.prepare_to_play
  client.message channel: channel['id'], text: game.prompt_for_turn
end

client.on :message do |data|
  next if data.channel != channel['id']
  # TODO: Figure out what do do about starting a new game
  next if game.finished?

  client.typing channel: data.channel

  game.process_turn(data.text)
  client.message channel: data.channel, text: game.prompt_for_turn
end

client.on :close do |_data|
  puts "Client is about to disconnect"
end

client.on :closed do |_data|
  puts "Client has disconnected successfully!"
end

client.start_async

loop do
  Thread.pass
end
drewish commented 6 years ago

I went ahead and put that up at https://github.com/drewish/scottbot I'll work on getting a proper readme in there to make it a little clearer how to get it up and running.

MikeTaylor commented 6 years ago

It's nice to see this progressing. The changes to ScottKit itself are obviously benign, and I will merge them momentarily. A couple of questions about your client code, though ...

drewish commented 6 years ago

The only reason I'm subclassing at this point is to replace $stdout with my object. We could make that an option that defaults to the current value.

Good note on the decompile. I think it was copy pasta'ed from the play method in the binary that runs from source. I'd switched to an .sao at some point and never removed it.

MikeTaylor commented 6 years ago

I was going to say "the only reason ever to call decompile is if you want to get a SCK". But I see from my own bin/scottkit that that's not quite true:

def play(game)
  # Decompile (and discard result) to get entity names resolved in
  # right order.  This ensures that debugging output that uses these
  # names will use them in the same way as decompiler output.
  dummy = StringIO.new
  game.decompile(dummy)
  game.play
end
drewish commented 6 years ago

Right, that's exactly where I'd gotten it. I didn't realize it still came into play with the compiled files as well.

MikeTaylor commented 6 years ago

Well, it doesn't really. I should wrap that use. New issue incoming.

drewish commented 6 years ago

Just a note that this isn't capturing output from instructions so output from actions that call print will be lost. #32 starts to address this issue.