Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 3a208ba43419d952d72ad4b445bc48cba11882f0 @jamis committed Nov 15, 2010
Showing with 425 additions and 0 deletions.
  1. +10 −0 README.markdown
  2. +21 −0 Rakefile
  3. +9 −0 bin/tinker
  4. +44 −0 examples/forest.tinker
  5. +23 −0 lib/tinker.rb
  6. +21 −0 lib/tinker/exit.rb
  7. +166 −0 lib/tinker/handler.rb
  8. +9 −0 lib/tinker/item.rb
  9. +47 −0 lib/tinker/place.rb
  10. +33 −0 lib/tinker/player.rb
  11. +42 −0 lib/tinker/world.rb
@@ -0,0 +1,10 @@
+Tinker
+======
+
+Tinker is a system for developing simple "choose-your-own-adventure"-style
+games. It was inspired by Sarah Allen's "pie" framework.
+
+Tinker should really have never been, except I was sitting in an airport
+without Internet access, wishing I could dig into pie, and decided that I'd
+see where I could get on a similar system, instead of moping about lost
+opportunities.
@@ -0,0 +1,21 @@
+require 'rake'
+require 'rake/gempackagetask'
+
+spec = Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.summary = "A simple CYOA game system."
+ s.name = 'tinker'
+ s.version = "0.0.1"
+ s.files = FileList["README.markdown", "Rakefile", "bin/*", "lib/**/*.rb", "spec/**/*.rb", "examples/**/*.tinker"].to_a
+ s.executables << "tinker"
+ s.description = "Tinker is a simple choose-your-own-adventure game system."
+ s.author = "Jamis Buck"
+ s.email = "jamis@jamisbuck.org"
+ s.homepage = "http://github.com/jamis/tinker"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_zip = true
+ pkg.need_tar = true
+end
+
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+
+require 'tinker'
+
+if ARGV.length == 1
+ Tinker.play(ARGV.first)
+else
+ warn "usage: #{$0} <game-file>"
+end
@@ -0,0 +1,44 @@
+place "Spooky forest" do
+ description <<-WORDS
+ You are in a very spooky forest. Owls are hooting eerily, and you can hear
+ things moving around, just out of sight. To the north you can see some
+ mountains poking above the trees. To the west, the forest seems to get
+ thinner.
+ WORDS
+
+ go "north", "Mountains"
+ go "west", "Plains"
+end
+
+place "Mountains" do
+ description "You're in the mountains now, and it is very cold. There is a locked door here."
+ go "through the door", "Dark cave", :with => "rusted key"
+ go "south", "Spooky forest"
+end
+
+place "Dark cave" do
+ description "The cave is very dark. You can hear something breathing further on."
+ go "further" do
+ to "Winner", :with => "shiny sword"
+ to "Monster", :without => "shiny sword"
+ end
+ go "out", "Mountains"
+end
+
+place "Monster" do
+ description "Oh, no! There was a scary monster, and it ate you all up!"
+end
+
+place "Plains" do
+ description "The plains are very large, and very flat. Lots of grass."
+ go "east", "Spooky forest"
+end
+
+place "Winner" do
+ description "A huge monster jumps out at you, but you fight it off with your sword! You find a huge hoard of treasure. Congratulations, you win!"
+end
+
+item "rusted key", :in => "Spooky forest"
+item "shiny sword", :in => "Plains"
+
+start :at => "Spooky forest"
@@ -0,0 +1,23 @@
+require 'rack'
+require 'rack/showexceptions'
+
+require 'tinker/handler'
+require 'tinker/world'
+
+module Tinker
+ def self.play(file)
+ world = World.new
+ world.instance_eval(File.read(file), file, 1)
+
+ puts "Please open your web browser and go to:"
+ puts
+ puts " http://localhost:1414"
+ puts
+
+ Rack::Handler::WEBrick.run(
+ Rack::ShowExceptions.new(
+ Rack::Lint.new(
+ Tinker::Handler.new(world))),
+ :Port => 1414)
+ end
+end
@@ -0,0 +1,21 @@
+module Tinker
+ class Exit
+ attr_reader :direction, :destination, :options
+
+ def initialize(direction, destination, options)
+ @direction = direction
+ @destination = destination.strip.downcase
+ @options = options
+ end
+
+ def allows?(player)
+ if options[:with]
+ player.inventory.include?(options[:with].downcase.strip)
+ elsif options[:without]
+ !player.inventory.include?(options[:without].downcase.strip)
+ else
+ true
+ end
+ end
+ end
+end
@@ -0,0 +1,166 @@
+require 'rack'
+require 'json'
+
+require 'tinker/player'
+
+module Tinker
+ class Handler
+ def initialize(world)
+ @world = world
+ end
+
+ def call(env)
+ req = Rack::Request.new(env)
+ res = Rack::Response.new
+
+ if req.request_method == "GET"
+ if req.path_info == "/favicon.ico"
+ return [404, {"Content-Type" => "text/plain"}, "Not found"]
+ else
+ start_new_game(req, res)
+ end
+ else
+ handle_transition(req, res)
+ end
+
+ res.finish
+ end
+
+ def start_new_game(req, res)
+ player = Player.new(@world.initial_state)
+ player.move_to(@world.starting_location)
+ render_game(player, req, res)
+ end
+
+ def handle_transition(req, res)
+ player = Player.new(req.POST["state"])
+ action = req.POST["action"]
+
+ if action == "go"
+ handle_go(player, req, res)
+ elsif action == "get"
+ handle_get(player, req, res)
+ elsif action == "drop"
+ handle_drop(player, req, res)
+ end
+ end
+
+ def handle_go(player, req, res)
+ direction = req.POST["key"]
+
+ location = @world.places[player.location]
+ path = location.exits[direction]
+
+ player.move_to(path.destination)
+ render_game(player, req, res)
+ end
+
+ def handle_get(player, req, res)
+ key = req.POST["key"]
+ player.at(player.location)[:items].delete(key)
+ player.inventory << key
+ render_game(player, req, res)
+ end
+
+ def handle_drop(player, req, res)
+ key = req.POST["key"]
+ player.at(player.location)[:items] << key
+ player.inventory.delete(key)
+ render_game(player, req, res)
+ end
+
+ def do_action(label, action, key)
+ "<a href=\"#\" onclick='doAction(#{action.to_json}, #{key.to_json})'>#{label}</a>"
+ end
+
+ JAVASCRIPT = <<-JS
+ <script type="text/javascript">
+ function doAction(action, key) {
+ var form = document.getElementById('exec');
+ var actionField = document.getElementById('action');
+ var keyField = document.getElementById('key');
+
+ actionField.value = action;
+ keyField.value = key;
+
+ form.submit();
+ }
+ </script>
+ JS
+
+ STYLES = <<-CSS
+ <style type="text/css">
+ body {
+ text-align: center;
+ background: #777;
+ }
+
+ #container {
+ text-align: left;
+ width: 600;
+ margin: auto;
+ border: 1px solid black;
+ background: white;
+ }
+
+ #container h1 {
+ padding: 5px 10px;
+ background: #aaa;
+ margin: 0;
+ border-bottom: 2px solid black;
+ }
+
+ #container p {
+ margin: 10px;
+ }
+ </style>
+ CSS
+
+ def render_game(player, req, res)
+ location = @world.places[player.location]
+
+ res.write "<html><head><title>#{location.name}</title>#{JAVASCRIPT}#{STYLES}</head>"
+ res.write "<body><div id='container'>"
+ res.write "<h1>#{location.name}</h1>"
+ res.write "<p>#{location.description}</p>"
+
+ if player.at(player.location)[:items].any?
+ res.write "<p>The following things are here:</p>"
+ res.write "<ul>"
+ player.at(player.location)[:items].each do |key|
+ item = @world.items[key]
+ res.write "<li>#{item.name} (#{do_action('get this now', 'get', key)})</li>"
+ end
+ res.write "</ul>"
+ end
+
+ if location.exits.empty?
+ res.write "<p>The end! <a href='/'>Click here to play again.</a></p>"
+ else
+ res.write "<p>You can go:</p>"
+ res.write "<ul>"
+ location.exits.keys.sort.each do |key|
+ path = location.exits[key]
+ next unless path.allows?(player)
+ res.write "<li>#{do_action(path.direction, 'go', key)}</li>"
+ end
+ res.write "</ul>"
+
+ if player.inventory.any?
+ res.write "<hr />"
+ res.write "<p>You are currently carrying:</p>"
+ res.write "<ul>"
+ player.inventory.each do |key|
+ item = @world.items[key]
+ res.write "<li>#{item.name} (#{do_action('drop this here', 'drop', key)})</li>"
+ end
+ res.write "</ul>"
+ end
+ end
+
+ res.write "</div>" # container
+ res.write "<form method='post' id='exec'><input type='hidden' name='action' id='action' /><input type='hidden' name='key' id='key' /><input type='hidden' name='state' value='#{player.dump}' /></form>"
+ res.write "</body></html>"
+ end
+ end
+end
@@ -0,0 +1,9 @@
+module Tinker
+ class Item
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+ end
+end
@@ -0,0 +1,47 @@
+require 'tinker/exit'
+
+module Tinker
+ class Place
+ attr_reader :name, :description, :exits
+
+ def initialize(name)
+ @name = name
+ @exits = {}
+ end
+
+ def description(desc=nil)
+ @description = desc if desc
+ @description
+ end
+
+ def go(direction, *args, &block)
+ if block_given? && args.empty?
+ ExitParser.new(self, direction).instance_eval(&block)
+ elsif !block_given? && (args.length == 1 || args.length == 2)
+ destination = args[0]
+ options = args[1] || {}
+ @exits[direction.downcase] = Exit.new(direction, destination, options)
+ else
+ raise ArgumentError, "wrong use of `go' at `#{name}' with `#{direction}'"
+ end
+ end
+
+ class ExitParser #:nodoc:
+ def initialize(place, direction)
+ @direction = direction
+ @count = 0
+ @place = place
+ end
+
+ def to(destination, options)
+ if !options.key?(:with) && !options.key?(:without)
+ raise ArgumentError, "`to' must have :with or :without to select the destination"
+ end
+
+ key = "#{@direction}-#{@count}"
+ @place.exits[key] = Exit.new(@direction, destination, options)
+ @count += 1
+ end
+ end
+ end
+end
@@ -0,0 +1,33 @@
+module Tinker
+ class Player
+ def initialize(state)
+ if state.is_a?(Hash)
+ @state = Marshal.load(Marshal.dump(state)) # deep copy
+ else
+ @state = Marshal.load(state.unpack("m*").first)
+ end
+
+ @state[:self] ||= { :items => [] }
+ end
+
+ def dump
+ [Marshal.dump(@state)].pack("m*").strip
+ end
+
+ def move_to(location)
+ @state[:self][:at] = location
+ end
+
+ def at(location)
+ @state[location.downcase.strip] ||= { :items => [] }
+ end
+
+ def location
+ @state[:self][:at]
+ end
+
+ def inventory
+ @state[:self][:items]
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 3a208ba

Please sign in to comment.