diff options
author | Marcin Chrzanowski <marcin.j.chrzanowski@gmail.com> | 2019-08-17 23:31:13 -0700 |
---|---|---|
committer | Marcin Chrzanowski <marcin.j.chrzanowski@gmail.com> | 2019-08-17 23:31:13 -0700 |
commit | e3afffc91d678301fcf7660db541cacc8b7a4e8d (patch) | |
tree | fd391ede8eb099d5b66672751ee01ae07f6deb98 |
Initial commit
-rw-r--r-- | Gemfile | 4 | ||||
-rw-r--r-- | Gemfile.lock | 32 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rw-r--r-- | curling.rb | 17 | ||||
-rw-r--r-- | engine.rb | 51 | ||||
-rw-r--r-- | hex_engine.rb | 140 | ||||
-rw-r--r-- | rooms.rb | 340 |
7 files changed, 605 insertions, 0 deletions
@@ -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 |