m-chrzan.xyz
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcin Chrzanowski <marcin.j.chrzanowski@gmail.com>2019-08-17 23:31:13 -0700
committerMarcin Chrzanowski <marcin.j.chrzanowski@gmail.com>2019-08-17 23:31:13 -0700
commite3afffc91d678301fcf7660db541cacc8b7a4e8d (patch)
treefd391ede8eb099d5b66672751ee01ae07f6deb98
Initial commit
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock32
-rw-r--r--README.md21
-rw-r--r--curling.rb17
-rw-r--r--engine.rb51
-rw-r--r--hex_engine.rb140
-rw-r--r--rooms.rb340
7 files changed, 605 insertions, 0 deletions
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..8014308
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem 'sinatra'
+gem 'sinatra-contrib'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..6cbca1b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,32 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ backports (3.15.0)
+ multi_json (1.13.1)
+ mustermann (1.0.3)
+ rack (2.0.7)
+ rack-protection (2.0.5)
+ rack
+ sinatra (2.0.5)
+ mustermann (~> 1.0)
+ rack (~> 2.0)
+ rack-protection (= 2.0.5)
+ tilt (~> 2.0)
+ sinatra-contrib (2.0.5)
+ backports (>= 2.8.2)
+ multi_json
+ mustermann (~> 1.0)
+ rack-protection (= 2.0.5)
+ sinatra (= 2.0.5)
+ tilt (>= 1.3, < 3)
+ tilt (2.0.9)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ sinatra
+ sinatra-contrib
+
+BUNDLED WITH
+ 2.0.1
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..28f3894
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# Library for cUrl-based games
+
+curling allows you to create simple games served over HTTP, intended to be
+interacted with in a CLI environment with curl.
+
+State is stored locally on the user's machine in cookies.
+
+## Basic interaction
+
+ curl -c c.txt -b c.txt http://endpoint.com/
+ You have 100HP, 10 coins. What do you want to do?
+ >fight
+ >heal (-5 coins)
+ curl -c c.txt -b c.txt http://endpoint.com/fight
+ You won, but took some damage. You find 2 coins!
+ You have 75HP, 12 coins.
+ >fight
+ >heal (-5 coins)
+ # edits cookie file to go back to 100HP
+ curl -c c.txt -b c.txt http://endpoint.com/fight
+ Error: invalid state!
diff --git a/curling.rb b/curling.rb
new file mode 100644
index 0000000..a295738
--- /dev/null
+++ b/curling.rb
@@ -0,0 +1,17 @@
+require 'sinatra'
+require 'sinatra/cookies'
+
+require './hex_engine'
+
+def secret
+ 'tajny_token_hehe'
+end
+
+get '/:command' do |command|
+ engine = Hex.new cookies.to_h
+ engine.step command
+ engine.state_h.each_pair do |key, value|
+ cookies[key] = value
+ end
+ engine.message
+end
diff --git a/engine.rb b/engine.rb
new file mode 100644
index 0000000..638a80e
--- /dev/null
+++ b/engine.rb
@@ -0,0 +1,51 @@
+class Engine
+ def initialize hash
+ validate_checksum hash
+ hash_to_state hash
+ end
+
+ def validate_checksum hash
+ if !checksum_valid? hash
+ throw 'Invalid state, checksum does not match!'
+ end
+ end
+
+ def checksum_valid? hash
+ if hash.empty?
+ return true
+ end
+
+ expected = checksum hash
+ expected == hash['checksum']
+ end
+
+ def checksum hash
+ string = hash
+ .keys
+ .filter { |k| k != 'checksum' }
+ .sort
+ .map { |k| "#{k}:#{hash[k]}" }
+ .append("secret:#{secret}")
+ .join '|'
+ Digest::SHA2.hexdigest string
+ end
+
+ def hash_to_state hash
+ end
+
+ def step command
+ end
+
+ def state_h
+ hash = state_to_hash
+ hash[:checksum] = checksum hash
+ hash
+ end
+
+ def state_to_hash
+ {}
+ end
+
+ def message
+ end
+end
diff --git a/hex_engine.rb b/hex_engine.rb
new file mode 100644
index 0000000..f01adfc
--- /dev/null
+++ b/hex_engine.rb
@@ -0,0 +1,140 @@
+require './engine'
+require './rooms'
+
+class Hex < Engine
+ attr_accessor :state, :health, :endurance, :xp, :level, :keys, :magic_weapon, :magic_armor
+ def initialize hash
+ super
+ end
+
+ def hash_to_state hash
+ @state = hash['state'] || 'init'
+ @health = (hash['health'] || 6).to_i
+ @endurance = (hash['endurance'] || 6).to_i
+ @xp = (hash['xp'] || 49).to_i
+ @level =( hash['level'] || 1).to_i
+ @keys = (hash['keys'] || 0).to_i
+ @magic_weapon = (hash['magic_weapon'] || 0).to_i
+ @magic_armor = (hash['magic_armor'] || 0).to_i
+ @last_roll = (hash['last_roll'] || 0).to_i
+ @messages = []
+ end
+
+ def step command
+ case @state
+ when 'init'
+ add_message <<~MSG.chomp
+ A curse has infested an ancient keep near your town.
+ The evil magic has filled the keep with monsters.
+ Can you save your home from this Hex?
+ MSG
+ @state = 'play'
+ when 'dead'
+ add_message <<~MSG.chomp
+ You are dead.
+ Thanks for playing! Better luck next time.
+ MSG
+ when 'win'
+ add_message <<~MSG.chomp
+ You have slain the monsters and emerge from the keep victorious!
+ Congratulations and thanks for playing!
+ MSG
+ else
+ resolve_last_room command
+ end
+
+ if @state == 'play'
+ enter_next_room
+ end
+ end
+
+ def resolve_last_room command
+ room = get_room
+ room.resolve command
+ end
+
+ def enter_next_room
+ roll_room
+ room = get_room
+ room.enter
+ end
+
+ def get_room
+ (if @xp >= 50
+ Rooms::Store
+ else
+ case @last_roll + @keys
+ when 1, 3
+ Rooms::Empty
+ when 2
+ Rooms::Trap
+ when 4, 5
+ Rooms::Monster
+ when 6
+ Rooms::Treasure
+ when 7
+ Rooms::Stairs
+ when 8
+ Rooms::Boss
+ when 9
+ Rooms::Exit
+ end
+ end).new self
+ end
+
+ def roll_room
+ @last_roll = roll_die
+ end
+
+ def roll_die
+ 1 + rand(6)
+ end
+
+ def add_message message
+ @messages.push(message)
+ end
+
+ def update_with_bonus current, update, bonus
+ if update < 0
+ current + [update + bonus, 0].min
+ else
+ current + update
+ end
+ end
+
+ def update_health health
+ @health = update_with_bonus @health, health, @magic_armor
+ end
+
+ def update_endurance endurance
+ @endurance = update_with_bonus @endurance, endurance, @magic_weapon
+ end
+
+ def update_hex health, endurance, xp
+ update_health health
+ update_endurance endurance
+ @xp += xp
+ end
+
+ def state_to_hash
+ hash = {}
+ hash['state'] = @state
+ hash['health'] = @health
+ hash['endurance'] = @endurance
+ hash['xp'] = @xp
+ hash['level'] = @level
+ hash['keys'] = @keys
+ hash['magic_weapon'] = @magic_weapon
+ hash['magic_armor'] = @magic_armor
+ hash['last_roll'] = @last_roll
+ hash
+ end
+
+ def message
+ <<~MSG
+ H: #{@health} E: #{@endurance} X: #{@xp} Keys: #{@keys}
+ Weapon: #{@magic_weapon} Armor: #{@magic_armor} Level: #{@level}
+ #{@messages.join "\n"}
+ MSG
+ end
+end
diff --git a/rooms.rb b/rooms.rb
new file mode 100644
index 0000000..ff71507
--- /dev/null
+++ b/rooms.rb
@@ -0,0 +1,340 @@
+module Rooms
+ class Room
+ def initialize engine
+ @engine = engine
+ end
+
+ def resolve command
+ @command = command
+ if @engine.state == 'invalid'
+ @engine.state = 'play'
+ end
+
+ if @engine.state == 'play'
+ _do_resolve
+ end
+ end
+
+ def check_death
+ if @engine.health <= 0
+ @engine.state = 'dead'
+ elsif @engine.endurance <= 0
+ @engine.state = 'dead'
+ end
+ end
+
+ def _do_resolve
+ do_resolve
+ check_death
+ if !instant_resolve
+ puts "Adding message: #{resolve_message}"
+ @engine.add_message resolve_message
+ end
+ end
+
+ # TODO: limit weapon/armor levels
+ def do_resolve
+ case @command
+ when 'c'
+ when 'w'
+ if @engine.xp >= 50
+ @engine.xp -= 50
+ @engine.magic_weapon += 1
+ @engine.add_message "You've upgraded your magic weapon!"
+ else
+ @engine.add_message 'Not enough XP to upgrade...'
+ end
+ when 'a'
+ if @engine.xp >= 50
+ @engine.xp -= 50
+ @engine.magic_armor += 1
+ @engine.add_message "You've upgraded your magic armor!"
+ else
+ @engine.add_message 'Not enough XP to upgrade...'
+ end
+ else
+ do_invalid
+ end
+ end
+
+ def do_invalid
+ @engine.state = 'invalid'
+ @engine.add_message "Invalid command: #{@command}"
+ enter
+ end
+
+ def resolve_message
+ if @engine.state == 'dead'
+ if @engine.health <= 0
+ death_message
+ elsif @engine.endurance <= 0
+ exhaust_message
+ end
+ else
+ success_message
+ end
+ end
+
+ def enter_message
+ ''
+ end
+
+ def death_message
+ 'You die'
+ end
+
+ def exhaust_message
+ 'You fall over exhausted'
+ end
+
+ def success_message
+ ''
+ end
+
+ def options
+ [
+ 'continue',
+ ]
+ end
+
+ def enter
+ @engine.add_message enter_message
+ if instant_resolve
+ do_instant_resolve
+ @engine.add_message resolve_message
+ end
+
+ options.each do |option|
+ @engine.add_message ">[#{option[0]}]#{option[1..]}"
+ end
+ end
+ end
+
+ class Store < Room
+ def instant_resolve
+ false
+ end
+
+ def enter_message
+ 'You have enough XP to upgrade your equipment!'
+ end
+
+ def options
+ [
+ 'weapon upgrade',
+ 'armor upgrade'
+ ]
+ end
+
+ def do_resolve
+ case @command
+ when 'a'
+ @engine.xp -= 50
+ @engine.magic_armor += 1
+ @engine.add_message "You've upgraded your magic armor!"
+ when 'w'
+ @engine.xp -= 50
+ @engine.magic_weapon += 1
+ @engine.add_message "You've upgraded your magic weapon!"
+ else
+ do_invalid
+ end
+ end
+
+ end
+
+ class Empty < Room
+ def instant_resolve
+ false
+ end
+
+ def do_resolve
+ case @command
+ when 'r'
+ @engine.update_hex 1, 3, 0
+ when 's'
+ @engine.update_hex 3, -1, 0
+ when 'm'
+ else
+ do_invalid
+ end
+ end
+
+ def enter_message
+ 'You enter an empty room. Nothing dangerous or interesting happening here.'
+ end
+
+ def success_message
+ case @command
+ when 'r'
+ 'You rest for a bit. You feel refreshed!'
+ when 's'
+ 'After a while, you find some dried meat. It looks good to eat'
+ when 'm'
+ "You decide there's no time to waste. You continue down the corridor."
+ end
+ end
+
+ def options
+ [
+ 'rest (+1 H, +3 E)',
+ 'scavenge for food (+3 H, -1 E)'
+ ]
+ end
+ end
+
+ class Trap < Room
+ def instant_resolve
+ false
+ end
+
+ def do_resolve
+ case @command
+ when 't'
+ @engine.update_hex -@engine.level, 0, 0
+ when 'd'
+ @engine.update_hex 0, -@engine.level, 0
+ else
+ do_invalid
+ end
+ end
+
+ def success_message
+ case @command
+ when 't'
+ 'Ouch!'
+ when 'd'
+ 'Very carefully, you manage to disable the trap.'
+ end
+ end
+
+ def enter_message
+ 'You stop yourself short. Your foot was about to brush a tripwire!'
+ end
+
+ def options
+ [
+ "trigger trap (-#{@engine.level} H)",
+ "disable trap (-#{@engine.level} E)"
+ ]
+ end
+ end
+
+ class Monster < Room
+ def instant_resolve
+ false
+ end
+
+ def do_resolve
+ case @command
+ when 'r'
+ when 'f'
+ @engine.update_hex -@engine.level, -@engine.level, @engine.level
+ else
+ do_invalid
+ end
+ end
+
+ def enter_message
+ 'You turn the corner and see an ugly monster!'
+ end
+
+ def success_message
+ case @command
+ when 'r'
+ 'You book it from there, heart beating fast. But you manage to lose the monster.'
+ when 'f'
+ 'The monster is defeated, your clothes still wet with blood, its and yours.'
+ end
+ end
+
+ def options
+ [
+ "run away",
+ "fight (-#{@engine.level} H, -#{@engine.level} E, #{@engine.level} X)"
+ ]
+ end
+ end
+
+ class Treasure < Room
+ def instant_resolve
+ true
+ end
+
+ def do_instant_resolve
+ roll = 1 + rand(6)
+ case roll + @engine.level
+ when 1..3
+ @found = 'key'
+ @engine.keys += 1
+ else
+ @found = 'gold'
+ @engine.update_hex 0, 0, @engine.level
+ end
+ end
+
+ def enter_message
+ 'You enter a dusty room. Something shiny on the ground catches your eye.'
+ end
+
+ def success_message
+ case @found
+ when 'key'
+ 'You find a key!'
+ when 'gold'
+ "You found some gold! (+#{@engine.level} X)"
+ end
+ end
+ end
+
+ class Stairs < Room
+ def instant_resolve
+ true
+ end
+
+ def do_instant_resolve
+ p 'increasing level'
+ @engine.level += 1
+ end
+
+ def enter_message
+ 'You come across a set of cracked stairs.'
+ end
+
+ def success_message
+ 'You journey downwards.'
+ end
+ end
+
+ class Boss < Room
+ def instant_resolve
+ true
+ end
+
+ def do_instant_resolve
+ @engine.update_hex -2 * @engine.level, -2 * @engine.level, 2 * @engine.level
+ end
+
+ def enter_message
+ "You encounter a giant boss monster. There's no escape routes, it's live or die!"
+ end
+
+ def success_message
+ 'You defeat the fiend!'
+ end
+ end
+
+ class Exit < Room
+ def instant_resolve
+ true
+ end
+
+ def do_instant_resolve
+ @engine.state = 'win'
+ end
+
+ def success_message
+ 'You won! Congratulations!'
+ end
+ end
+end