Ok this is the actual inital code... need to convert from mongo to sqlite...

This commit is contained in:
Alex Stevenson 2025-07-15 23:00:02 -04:00
parent 8c856ce465
commit 8fa1664bcb
13 changed files with 1168 additions and 0 deletions

166
active_rpg.rb Normal file
View file

@ -0,0 +1,166 @@
load "mongo_connect.rb"
load "mongo_document_wrapper.rb"
load "entity_factory.rb"
load "rpg_grid.rb"
load "rpg_bets.rb"
class ActiveRpg
@grid = nil
@mc = nil
@ef = nil
def initialize(grid_size = 10, db="testdb", coll="entities")
@mc = MongoConnect.new(db, coll)
@ef = EntityFactory.new(@mc)
@grid = RpgGrid.new(@mc, grid_size)
@bets = RpgBets.new(@mc)
end
# this should only really ever be needed in a simulation run to clear out the DB and reset the indexes
def get_mc()
return @mc
end
# this is just a placeholder hopefully. need to flesh out how bets are gonna work but gonna
# delegate it to the irc bot for now...
def get_bets()
return @bets
end
# Create and/or load player. Spawn another entity. Move the player.
def take_turn(player_name, line)
p = find_player(player_name)
new_loc = @grid.get_move_location(p.get("location"))
# Update the player doc with new location/exp/line
p.set({
'exp' => p.get('exp').to_i + 1,
'location' => new_loc,
'last_line' => line,
})
# Spawn a new Monster
spawn_monster(player_name, line)
# Spawn a potential event
spawn_event()
# Resolve interaction from player movement and grab a string and return it
res = resolve_conflict(p, new_loc)
update_user_boss(player_name, line)
return res
end
def find_player(player_name)
found_player = @mc.collection.find({"type" => :player, "name" => player_name}).first
if found_player == nil then
p = @ef.get(:player)
p.set({
"name" => player_name,
"exp" => 1,
"location" => @grid.find_location()
})
return p
else
return @ef.build_instance(found_player)
end
end
def update_user_boss(player_name, line)
found_boss = @mc.collection.find({"type" => :boss, "spawned_by" => player_name}).first
if found_boss != nil then
#level them up by line length...
b = @ef.build_instance(found_boss)
b.set({"exp" => b.get("exp") + line.length})
else
if rand(1000) > 900 then
spawn_boss(player_name, line)
end
end
end
def spawn_boss(player_name, line)
b = @ef.get(:boss)
b.set({
"spawned_by" => player_name,
"location" => @grid.find_location(),
"exp" => line.length,
"last_line" => line,
})
end
def spawn_monster(player_name, line)
m = @ef.get(:monster)
m.set({
"spawned_by" => player_name,
"location" => @grid.find_location(),
"line" => line,
"exp" => line.length,
})
end
def spawn_event()
if rand(1000) <= 10 then
e = @ef.get(:random_event)
e.set({
"location" => @grid.find_location(),
})
end
end
def resolve_conflict(player, loc)
ents = find_at_location(loc)
if ents.count > 1 then
battle_ents = []
ents.each do |e|
if e.get("name") != player.get("name") then
battle_ents.push e
end
end
results = []
# A bit clunky but basically resolve any conflicts with other objects
# and nuke any of them that aren't other players
battle_ents.each do |b|
res = player.resolve(b)
if res != nil or res != "" then
results.push(res)
end
if b.get("type") != :player then
b.delete()
else
new_loc = @grid.find_location()
b.set({"location" => new_loc})
end
end
return results.join("; ")
end
end
def find_at_location(loc)
ents = @mc.collection.find({"location" => loc})
wrapper_docs = []
ents.each do |e|
wrapper_docs.push(@ef.build_instance(e))
end
return wrapper_docs
end
end

91
entities/boss.rb Normal file
View file

@ -0,0 +1,91 @@
class Boss < Entity
@@bossMods = [
"Insanely Violent", "Horiffic and Huge", "Incredibly Monstrous", "World-Threatening",
"Disasterously Unhinged", "Literally God-like", "Absurdly Deranged", "Wildly Disastrous",
"Potentially Universe Ending", "Unhinged Ancient", "Insatiably Hungry", "Malevolently Cruel",
"Practically Omniscient Like Wow You Don't Even Know",
]
@@bossTypes = [
"Space Demon", "Literal Star", "Supervillian", "Kitten", "Bob", "Subatomic Particle",
"Dental Hygenist", "Creature from Beyond the Stars", "Previously Slumbering Elder Being",
"Bag of Snacks that Says it's 20g In Size But Somehow 19g's of it are Just Air???",
"Mysterious Button", "English Teacher", "Corrupted Wizard", "Pit Fiend",
"King Henry VIII's 9th Wife", "Guy Fieri", "Celestial Being", "Balloon-Shaped Horror",
"GREAT Worm", "Planet Smasher", "DragBot Mecha-Queen",
]
@@bossLosses = [
"returned from the dimension from which they came", "slumbers under the sea once more",
"flees to the stars", "gives up and vanishes without a trace", "runs home to devise a new plan",
]
@@bossWins = [
"isn't sure what to do with themselves now, and goes off to plan an easier conflict",
"only partially destroys the planet", "deletes a star out there, probably somewhere over the rainbow",
"summoned several eldritch beings that quickly passed away in their atmosphere", "eats some cake",
]
@@encounterTypes = [
"tangle", "struggle", "wrestle", "argue", "combat", "stare down", "grapple",
]
def get_default_doc()
q = {
'type' => :boss,
'spawned_by' => nil,
'location' => nil,
'exp' => nil,
'last_line' => nil,
'name' => @@bossMods.sample + " " + @@bossTypes.sample
}
return q
end
def resolve(p)
max_player_attack = p.get('exp')
max_xp_gain = (self.get('exp').to_f + 0.25).to_i
encounter_str = "runs into a Boss, spawned by " +
self.get("spawned_by") + " with their wicked incantation of \"" +
self.get("last_line") + "\"! Known by legend as the "
result = p.get("name") + "[" + max_player_attack.to_s + "str] "
result = result + encounter_str
result = result + self.get("name") + "[" + self.get('exp').to_s + "str] "
player_attack = 1+rand(max_player_attack)
foe_attack = 1+rand(self.get('exp'))
result = result + "They " + @@encounterTypes.sample + "! "
result = result + "[" + player_attack.to_s + "dmg vs " + foe_attack.to_s + "dmg]! "
if player_attack > foe_attack then
xp = 1 + rand(max_xp_gain)
result = result + p.get("name") + " wins! "
result = result + "The " + self.get('name') + " " + @@bossLosses.sample + "! "
result = result + p.get('name') + " earns " + xp.to_s + "xp!"
p.gain_exp(xp)
elsif player_attack < foe_attack then
result = result + "The " + self.get("name") + " wins and " + @@bossWins.sample + "! "
if self.get("spawned_by") != p.get("name") then
raw_spawner = @mc.collection().find({"type" => :player, "name" => self.get("spawned_by")}).first
s = Player.new(@mc, raw_spawner)
xp = 1 + rand(1 + (s.get("exp")*0.05).to_i)
result = result + self.get("spawned_by") + "'s nefarious plan succeeded! They gain " + xp.to_s + "xp!"
s.gain_exp(xp)
else
result = result + self.get("spawned_by") + " was betrayed by their creation! "
result = result + "The shame they feel is immeasurable!"
end
else
result = result + "They tied! They begrudgingly back away from each other."
end
return result
end
end

11
entities/entity.rb Normal file
View file

@ -0,0 +1,11 @@
load "mongo_document_wrapper.rb"
class Entity < MongoDocumentWrapper
def initialize(mc, doc=nil)
super(mc, doc)
end
def resolve(target)
return nil
end
end

131
entities/monster.rb Normal file
View file

@ -0,0 +1,131 @@
class Monster < Entity
@@monsterMods = [
"Crafty", "Stinky", "Spooky", "Surprisingly Stout", "Shockingly Petite",
"Malevolent", "Loud", "Nudist", "Rude", "Aroused", "Depressed",
"Seemingly Friendly but Violent", "Lazy", "Ugly", "Cute Lil'", "Impish", "Ancient",
"Ambiguously Gendered", "Idiotic", "Sticky", "Angery", "Dusty ol'", "Big",
"Mega", "Itty Bitty", "Vibrant Orange", "Yella-Bellied", "Cowardly", "Brave",
"Crusty", "Jankety", "Viral", "Blue-ish", "Moldy", "Musty", "Haunted", "Cursed",
"Buttery", "Sexy", "Drunken", "Foreign", "Extremely Sharp", "Shifty", "Smrat",
"Sharply Dressed", "Irate", "Psychic", "Emphatic", "Unreasonable",
"Dumb as Rocks", "Possessed", "High as Hell", "Annoying", "VERY LOUD",
"Oddly Erotic", "Confused", "Arrogant", "Undead", "Frustrated", "Transparent",
"Thicc", "Holy", "Unholy", "Filthy", "Slimy", "Contagious", "Fartin'", "Eccentric",
"Shartin'", "Eccentric", "Moist", "Ecstatic", "Zooming", "Grim (Up North)",
"Flatulent", "Nefarious", "Rotten", "Sacred", "Handsome", "Purdy", "Howling",
"Weeping", "Real Handsy", "Underachieving", "Playful", "Bubbly", "Offputting",
"Unnerving", "Mysterious", "Creeeepy", "Dirty", "Unfunny", "Inverted", "Inside-Out",
"Bleak", "Near-Sighted", "Uncooked", "Burnt", "Frosty", "Corpulent", "Winged",
"Mastermind", "Lackey", "Scientific", "Tenured", "Illegal", "[redacted]",
"Invisible", "Floating", "Oozing", "smol", "Embarrassing", "Cheeky", "Slanderous",
"Perverted", "Super-Fast", "Boring", "Educated", "Wary", "Ribbed", "Bumpy",
"Smooth", "Cheeked Up", "Insane", "Faaaaast", "Stoned", "Worthless", "Timid",
"Voyeuristic", "Watchful", "Crunchy", "Uncouth", "Brazen", "Regretful", "Practically Dead",
"Obviously Unhinged", "Volatile", "Problematic", "Wary", "Gifted", "Abstract",
"Disgusting", "Amateur", "Expired", "Fading", "Funky", "Bearded", "Feeble", "Frail",
"Crude", "Stringy", "El Fuego", "Paper Crown Wearing", "Majestic", "Beastly", "Number 7",
"Onanistic", "Stained", "Lucky", "Priapic", "Phallic", "Professional", "Wild", "Feral",
"Electric", "Droopy", "Dripping", "Questionable", "Wandering", "Scousey", "Troubled",
"Well-Adjusted", "Innocent", "Creepy", "Clown-Like", "Abrasive", "Arrogant", "1337",
"Tastefully Nude", "Wet", "All-Powerful", "Scandelous", "Remarkedly Stupid",
]
@@monsterTypes = [
"Goblin", "Orc", "Skeleton", "Skelly", "Bob", "Tim", "Witch", "Demon", "Cricket",
"Slime", "Ghost", "Gila Monster", "Sentient Paperclip", "Cthlululu", "Imp",
"BUG?!", "Influencer", "Duck", "Alium", "Hippie", "Druid", "Bean", "Woof", "Sheep",
"Opossum", "Dwarf", "Lizard Fella", "Sailor", "Spy", "Agent", "ChonKee", "Boar",
"Squirrel", "Hobo", "Hairball", "Wizard", "Pupper", "Doggo", "Cat", "Mother",
"Karen", "Magister", "Thief", "Leaky Glass", "Dead Battery", "Cow",
"Disembodied Foot", "Cloud", "Patch of Grass", "Ginger", "Warrior", "Bride",
"Badger", "Parrot", "Mummy", "Raider", "Knight", "Paladin", "Necromancer",
"Pixie", "Sprite", "Stranger", "College Student", "Child", "Foreigner", "Creep",
"Basement Dweller", "Pile of Clothes", "Box of Sand", "Lady", "Dude", "Nerd",
"Luggage", "Spooder", "Pirate", "Scientist", "Professor", "Monkey", "Tree", "Bus",
"[redacted]", "Moth", "Perv", "Sanic", "Wolverine", "Rock", "Eggplant", "Stoner",
"Honeybee", "Wasp", "Gnome", "Tunnel Dweller", "Truck", "Shrimp Appetizer Platter",
"Marine", "Pilot", "Faceless Beard", "Pork Slab", "Denizen", "Archer", "Weirdo Next Door",
"Can of Diet Sprite", "Ferret", "Guttersnipe", "Elf", "Horror", "Terror", "Wanderer",
"Person Who Works in the Cubicle Next to You", "Bunny Rabbit", "Hamster", "Degu",
"Metalhead", "Baking-Soda Volcano", "Canadian", "Moon", "Jazz Musician", "Robot",
"AI Program", "Flat Earther", "Entrepreneur", "Social Media Platform", "Man?", "Beast",
"American", "Brit", "Hippo", "7", "B-List Celebrity", "Bear", "Childs Drawing",
"Anime Character", "Merperson", "Salesperson", "Skellington", "Scallies", "Clown Puppet",
"Clown", "Puppet", "n00b", "Spider Shaped Man", "Bat Shaped Man", "Armadillo", "Unshucked Ear of Corn",
"Dustmite", "Single Follicle of Hair", "Optimal Pride",
]
@@encounter = [
"ran into", "encountered", "crossed paths with", "slammed into", "almost got stepped on by",
"crossed paths with", "almost got eaten by", "got into fisticuffs with", "failed to hide from",
"hunted down", "picked a fight with", "got caught sleeping by", "drunkenly bumped into",
"got bored and starting puching at", "attempted to poach", "tried to collect a trophy from",
"got jealous of and attacked", "got lost then found and got punchy with", "had beef to settle with",
"was inappropriately touched by", "had a misunderstanding with",
"was frustrated and confused by and started fighting",
]
@@getWin = [
"is brutally slaughtered", "straight up dies", "explodes into a million tiny pieces", "has been murdered",
"collapses into a pile of tears before slowly dying", "should probably be buried soon", "DESTROYED!!!",
"is beaten to a bloody pulp", "is permamently damaged", "wonders where things went so wrong and flees",
"dies from embarrassment", "ineffectively begs for forgiveness", "was exsanguinated",
]
@@getLoss = [
"mysteriously disappears", "vanishes in a puff of smoke", "spits at your feet and flips you the bird",
"returns to the dimension from whence they came", "saunters off happily", "gets bored of winning and leaves",
"somehow managed to not completely murder you", "runs away cackling", "is quite embarrassed for you",
"bows, then turns to walk away",
]
def get_default_doc()
return {
'type' => :monster,
'spawned_by' => nil,
'location' => nil,
'exp' => nil,
'line' => "",
'name' => @@monsterMods.sample + " " + @@monsterTypes.sample
}
end
def resolve(p)
max_xp_gain = self.get('exp')
max_player_strength = p.get('last_line').length
name = self.get("name")
article = "a"
if ['A','E','I','O','U', 'a','e','i','o','u'].include?(self.get('name')[0])
article = "an"
end
player_attack = 1+rand(max_player_strength)
foe_attack = 1+rand(self.get('exp'))
result = p.get('name') + "[" + max_player_strength.to_s + "str] "
result = result + @@encounter.sample + " " + article
result = result + " " + name
result = result + "[" + max_xp_gain.to_s + "str]" + "! "
result = result + "[" + player_attack.to_s + "dmg vs " + foe_attack.to_s + "dmg] "
if player_attack > foe_attack then
xp_gain = 1 + rand(max_xp_gain)
result = result + "The " + name + " " + @@getWin.sample + "! "
result = result + p.get("name") + " wins and earns " + xp_gain.to_s + "xp! "
p.gain_exp( xp_gain)
elsif player_attack < foe_attack then
result = result + "The " + self.get("name") + " wins and " + @@getLoss.sample + "! "
else
result = result + "They tied! They shake hands and agree to disagree. "
end
return result
end
end

76
entities/player.rb Normal file
View file

@ -0,0 +1,76 @@
class Player < Entity
def initialize(mc, doc=nil)
super(mc, doc)
end
def get_default_doc()
q = {
'type' => :player,
'name' => nil,
'location' => nil,
'exp' => 0,
'last_line' => nil
}
return q
end
def gain_exp(amount)
self.set_exp(self.get('exp') + amount)
end
# be careful with this one. very rare use case when you want
# to use this directly from a bot or playground
def set_exp(amount)
self.set({'exp' => amount})
end
# yeah this needs a WHOLE LOT of cleanup and rethinking....
def resolve(foe)
result = nil
# call the resolve method for anything that's not a player.
if foe.get('type') != :player then
result = foe.resolve(self)
# Handle player v player here.... can't exactly recall resolve without
# recursing forever
# most of this should probably be somehow abstracted into Entity...
# I'm ok with this for now.
else
max_player_attack = self.get('exp')
max_foe_attack = foe.get('exp')
player_str = self.get('name')
player_str = player_str + "[" + max_player_attack.to_s + "str] "
result = player_str + "felt the need to fight "
result = result + foe.get('name')
result = result + "[" + max_foe_attack.to_s + "str]" + "! "
player_attack = 1+rand(max_player_attack)
foe_attack = 1+rand(foe.get('exp'))
result = result + "[" + player_attack.to_s + "dmg vs " + foe_attack.to_s + "dmg]! "
if player_attack > foe_attack then
# Max of 25% of foe exp, minimum of 1. Cannot pass a 0 into rand without
# getting a float, so this is a bit cludgy. if rand gets a 1 it always returns
# 0 so the floor is effectively 1 :/
max_xp_gain = (foe.get('exp').to_f * 0.25).to_i + 1
xp = 1 + rand(max_xp_gain)
result = result + foe.get('name') + " lost! "
result = result + self.get('name') + " wins and earns " + xp.to_s + "xp!"
gain_exp(xp)
elsif player_attack < foe_attack then
result = result + foe.get("name") + " wins and saunters away"
else
result = result + "They tied! They shake hands and agree to disagree and part ways."
end
end
return result
end
end

61
entities/random_event.rb Normal file
View file

@ -0,0 +1,61 @@
class RandomEvent < Entity
@@eventTypes = [
"ate a real solid kebab", "discovered the secret of magnets", "managed to do almost seven whole pushups in a row",
"found a giant cache of fresh beans", "got help from a particularly friendly woodling sprite",
"received a kiss from a travelling prince. Or princess. They weren't exactly sure but it felt great",
"somehow managed to wake up rested", "finished a Rubix cube without swapping any of the stickers this time",
"made a bet with an impish gnome and won", "arm wrestled an ogre and won", "discovered a way to impersonate Guy Fieri",
"found an altar adorned with skulls but it turned out that the god being worshiped was just misunderstood and was actually pretty chill",
"paid for insurance at the blackjack table and it actually worked", "boiled a pot of water without screwing it up",
"finally replied to that text they got from a friend like a week ago but kept putting off responding for no good reason",
"made friends with a tree", "got invited to sit at the cool kids table", "found some dusty sacred texts",
"chose to be a better person", "digs up dirt on their political opponents", "was gifted a jar of honey from The True Queen Bee",
"changed a flat tire for a weary traveller", "won at a game of poker with a tribe of wandering mages",
"didn't die, not even a little", "accidentally solved world hunger, at least for a few minutes",
"felt at one with the universe before realizing it was just a bit of indigestion. But still, it felt pretty good",
"took a big refreshing gulp of ocean water", "replaced the batteries in the smoke detector",
"passed the lie detector test (somehow)", "was found innocent on all charges", "brushed their teeth and took a shower",
"stood up to the schoolyard bully", "found a penny. Not a particularly lucky one but hey, it's better than nothing",
"finally remembered the name of that song that they had half stuck in their head for the past several days",
"discovered music", "found the right cable from that big box of cables they keep saved in that one closet for some reason",
"artfully evaded a swarm of angry ducks", "fell into a hole but found a big ol' pile of treasure at the bottom",
"overheard a joke a friend said but no one else in the group did, repeated it and got good big laughs in return",
"found their way out of The Endless Wood", "found a scratch-off lottery ticket and won a couple bucks",
"got a haircut", "felt a fleeting moment of happiness", "went to bed early", "comforted a grieving child",
"deleted those dozen apps from their phone that they hadn't used in years", "planned a party that people actually showed up for",
"caught a lucky fish that whispered eldritch secrets to them", "raised awareness for The Cause", "helped a friend move",
"didn't burn down the house", "crafted a not-haunted puppet", "saw their childhood imaginary friend",
"discovered a cure for sneezing", "saved the world but it wasn't THAT big of a deal", "donated money to charity",
"regretted previous decisions and made amends", "educated the ignorant", "cut loose on the dance floor",
"filed the appropriate paperwork to prevent The Great Disintegration just in time", "made a new friend",
"finished that TV series their friend recommended a good few months back", "took a hot bath", "won the game",
"cracked the code", "found a sheep stuck on its side, and after righting it found a super small secret wishing well",
"raked some leaves for the elderly couple next door", "kissed a toad, hallucinated",
"saved up the pennies over the years and can now afford that downpayment", "lost a foot but found a fresh new one",
"ran into a crowd, who then started chanting and calling them Muad'Dib",
]
def get_default_doc()
q = {
'type' => :random_event,
'location' => nil,
'name' => @@eventTypes.sample
}
return q
end
def resolve(p)
exp = p.get('exp')
max_xp_gain = ((exp.to_f * (0.10)).to_i)+1
actual_xp_gain = rand(max_xp_gain)+1
result = p.get('name') + "[" + exp.to_s + "xp] " + self.get('name') + "!"
result = result + " They gained " + actual_xp_gain.to_s + "xp!"
p.gain_exp(actual_xp_gain)
return result
end
end

50
entity_factory.rb Normal file
View file

@ -0,0 +1,50 @@
load "entities/entity.rb"
load "entities/player.rb"
load "entities/monster.rb"
load "entities/boss.rb"
load "entities/random_event.rb"
class EntityFactory
@mc = nil
def initialize(mc)
@mc = mc
end
# Convert the symbol into a class. There might be a way to automate this better...
# Make sure to load the class if you're adding a new one here.
def get_ent_class(type)
ent_class = nil
case type
when :player
ent_class = Player
when :monster
ent_class = Monster
when :boss
ent_class = Boss
when :random_event
ent_class = RandomEvent
end
return ent_class
end
# get a new entity if doc is nil, otherwise load from that doc
def get(type, doc=nil)
return get_ent_class(type).new(@mc, doc)
end
# try to load an object instance of an entity based on the doc
def build_instance(doc)
if doc['type'] != nil then
return get_ent_class(doc['type']).new(@mc, doc)
else
return nil
end
end
def run_query(filter={})
return @mc.collection().find(filter)
end
end

23
mongo_connect.rb Normal file
View file

@ -0,0 +1,23 @@
#require 'rubygems' # not necessary for Ruby 1.9
require 'mongo'
Mongo::Logger.logger.level = ::Logger::FATAL
include Mongo
class MongoConnect
def initialize(dbname, collname)
@db = Mongo::Client.new(
["127.0.0.1:27017"],
:database => dbname)
@coll = @db[collname.to_sym]
end
def db()
return @db
end
def collection()
return @coll
end
end

77
mongo_document_wrapper.rb Normal file
View file

@ -0,0 +1,77 @@
class MongoDocumentWrapper
@mc = nil # MongoConnect instance
@doc = nil # MongoDocument instance....
@id = nil # BSON ID for the Doc
def initialize(mc, doc=nil)
@mc = mc
if doc == nil then
create()
else
@id = doc['_id']
@doc = doc
end
end
def create()
@id = BSON::ObjectId.new()
doc = get_default_doc()
doc['_id'] = @id
@doc = doc
@mc.collection.insert_one(@doc)
end
def load()
@doc = @mc.collection().find({"_id" => @id}).first()
end
# save and grab the ID and doc....
def set(params={}, persist=true)
@doc.merge!(params)
if persist == true then
persist()
end
end
def get(field)
return @doc[field]
end
def persist()
@mc.collection().update_one({"_id" => @id}, @doc)
end
def delete()
if @id == nil then
die
end
@mc.collection().delete_one({"_id" => @id})
end
def update(query)
@mc.collection().update_one({"_id" => @id}, query)
end
def get_default_doc()
puts "Override this in ent class"
return {}
end
def self.build_from(mc, doc=nil)
if doc == nil then
puts "wtf whoops??"
else
@mc = mc
@doc = doc
@id = @doc['_id']
end
return self
end
end

167
rpg_bets.rb Normal file
View file

@ -0,0 +1,167 @@
# I am not happy with most of this.
class RpgBets < MongoDocumentWrapper
@bets = []
def initialize(mc)
doc = mc.collection().find({"type" => :bets}).first
if doc == nil then
@mc = mc
self.create()
else
super(mc, doc)
end
@bets = self.get("bets")
end
# This needs so much cleanup
# Call with 2 player names and an amount. Returns a string with the interaction output
def place_bet(player, target, amount)
# NO SELF BETS!
if player == target then
return "No self bets " + player + "!"
end
# so this happened in the sim...
if amount == 0 then
return "Who bets 0?"
end
possible_player = @mc.collection().find({"type" => :player, "name" => player}).first
# This should never really happen, but just in case...
if possible_player == nil then
return "Who the heck are YOU " + player + "??"
end
# Check to see if the target even exists...
possible_target = @mc.collection().find({"type" => :player, "name" => target}).first
if possible_target == nil then
return "Who the heck is " + target + "?"
end
# if we get here we know we have both a valid player and target
p = Player.new(@mc, possible_player)
t = Player.new(@mc, possible_target)
# Make sure betting/calling player has enough xp to place the bet...
if p.get("exp") < amount then
return "Sorry " + p.get("name") + " but you only have " + p.get("exp").to_s + "xp to bet with!"
end
bet = get_bet(p.get('name'), t.get('name'), amount)
response = ""
# Check the exp on the bet and see if the new amount is greater.
# If greater, if it's from the same player who already owns the most
# recent bet, do nothing. Otherwise, replace the old exp with the new one
# and replace the last bet player with the current.
if amount > bet['exp'] then
if bet['last_bet'] == p.get('name') then
response = "You already have the highest bet of " + bet['exp'].to_s + "xp!"
else
bet['exp'] = amount
bet['last_bet'] = player
response = player + " upped the ante to " + amount.to_s + "xp against " + target + "!"
end
# If amount is less than the current bet, don't return immediately but spit out
# a message about how it's not enough
elsif amount < bet['exp'] then
response = player + " be real, yo. Current bet with " + target + " is at " + t.get('exp').to_s + "xp. "
response = response + "Match to fight, or overbid them."
# fight? This should prolly be refactored into some function.... it gets busy...
else
if bet['last_bet'] == player then
response = "You have the highest bet already against " + target + " for "
response = response + bet['exp'].to_s + "xp! They need to call you on it."
else
# this whole block needs cleaning. It's gross atm.
p_attack = 1 + rand(amount)
t_attack = 1 + rand(amount)
response = player + " and " + target + " fight over " + amount.to_s + "xp! "
response = response + "[" + p_attack.to_s + "dmg vs " + t_attack.to_s + "dmg]! "
winner = loser = nil
if p_attack > t_attack then
winner = p
loser = t
elsif t_attack > p_attack then
winner = t
loser = p
# If it's a tie, persist the bet back down with no changes and return
else
@bets.push(bet)
self.set({"bets" => @bets})
response = "They tied! The crowd goes wild in anticipation! The bet remains unresolved!"
return response
end
response = response + winner.get("name") + " wins! They won " + amount.to_s + "xp "
response = response + "from " + loser.get("name") + ", who respectfully hands it over! "
# update the players...
winner.gain_exp(amount)
loser.gain_exp(-amount)
if loser.get("exp") <= 0 then
response = response + "Holy wow! " + loser.get("name") + " went negative! "
response = response + "The Powers That Be resets them to 1xp!"
loser.set_exp(1)
puts response
die
end
return response
end
end
@bets.push(bet)
self.set({"bets" => @bets})
return response
end
def get_player_bets(player)
found_bets = []
@bets.each do |b|
if b['players'].include?(player) then
found_bets.push(b)
end
end
return found_bets
end
def get_bet(player, target, amount)
found_bet = nil
@bets.each do |b|
if b["players"].include?(player) and b["players"].include?(target) then
found_bet = b
end
end
# This feels cludgy but deleting an element from inside an iterator (the .each above)
# is apparently straight up disallowed in ruby. so need to nuke it here. It will either
# resolve and disappear in the place_bet method or be updated and re-inserted there
if found_bet != nil then
@bets.delete_if{ |b| b["players"].include?(player) and b["players"].include?(target) }
return found_bet
end
return {"players" => [player, target], "last_bet" => player, "exp" => amount}
end
def get_default_doc()
return {
"type" => :bets,
"bets" => [],
}
end
end

67
rpg_grid.rb Normal file
View file

@ -0,0 +1,67 @@
class RpgGrid
@mc = nil
@gridSize = nil
# need the @mc to query occupied locations.
def initialize(mc, grid_size=25)
@mc = mc
@gridSize = grid_size
end
# Finds a place to put either a player or a monster that isn't currently occupied
# Players need to spawn in a non-player cell, monsters can replace an existing monster
def find_location()
loop do
x = rand(@gridSize)
y = rand(@gridSize)
loc = [x, y]
n = @mc.collection().find({'location' => [x, y]})
if n.count == 0 then
return loc
elsif n.first["type"] == :monster then
@mc.collection().delete_one({"_id" => n.first["_id"]})
return loc
end
end
end
# Move the +/- 1 on the x/y with them "bumping" off the walls to find a new position.
# Gotta keep em in the grid.
def get_move_location(cur_loc=nil)
if cur_loc == nil then
cur_loc = [0, 0]
end
# get the x/y differences
dx = get_move_diff(cur_loc[0])
dy = get_move_diff(cur_loc[1])
# Build a new [x,y] to save to location
new_loc = [cur_loc[0] + dx, cur_loc[1] + dy]
return new_loc
end
# Should keep all chars inside the @gridSize playfield though without getting locked on an edge
def get_move_diff(cur_loc)
# i should be a num between 0 and 2. If 0 == 0, 1 == 1, 2 == -1. Intuitive right?
i = rand(3)
d = 0
if i == 2 then
d = -1
else
d = i
end
if cur_loc == 0 and d == -1 then
d = rand(2)
elsif cur_loc == @gridSize - 1 and d == 1 then
d = rand(2) - 1
end
return d
end
end

127
rpgqui.rb Normal file
View file

@ -0,0 +1,127 @@
require 'cinch'
load "active_rpg.rb"
def fix_nick(n)
return n[0] + "\uFEFF" + n[1,n.length]
end
# Clean up all nicks in the responses
def fix_nicks(response, users)
fixed_response = response
users.keys.each do |u|
split_response = fixed_response.split(u.nick)
fixed_response = split_response.join(fix_nick(u.nick))
end
return fixed_response
end
bot = Cinch::Bot.new do
# Set up the data store here...
$w = ActiveRpg.new(25, 'libera-rpg_new')
$output_channel = "#active_rpg"
$intro_message = "Hi! I'm a bot that runs a passively active RPG game. Please join " +
$output_channel + " to track progress"
configure do |c|
c.server = "irc.libera.chat"
c.user = "Monqui"
c.password = "tbmsn"
c.nick = "rpgqui"
c.delay_joins = 15
c.channels = ["#bakedbeans", "#sconesandcream fatjoints", $output_channel]
c.timeouts.connect = 30
end
on :join do |m|
if m.channel != $output_channel then
m.reply $intro_message + ", or say '!help' for a list of commands!"
end
end
on :message, ".bots" do |m|
m.reply "rEpOrtInG iN! [ruby] Lmao!"
end
on :message, /^(.*)$/ do |m, line|
synchronize(:rpg) do
if line[0] != "!" then
res = $w.take_turn(m.user.nick, line)
if res != "" and res != nil then
fixed_response = fix_nicks(res, m.channel.users)
self.bot.channel_list.find($output_channel).send(fixed_response)
end
end
end
end
on :message, /^!help$/ do |m|
response = $intro_message
response = response + ", or use the following commands to get more info!\n"
response = response + "!rpg - personal user stats; !critters - list entities in the world; "
response = response + "!leaderboard - ugly printout of all user stats; "
response = response + "!rpgbet - bet against another user for xp! "
response = response + "(\"!rpgbet [target_player] [amount]\")"
m.reply response
end
on :message, /^!rpg$/ do |m|
synchronize(:rpg) do
mc = $w.get_mc()
u = mc.collection.find({"type" => :player, "name" => m.user.nick}).first
pet = mc.collection.find({"type" => :boss, "spawned_by" => m.user.nick}).first
out = m.user.nick + " currently has " + u['exp'].to_s + "xp at " + u['location'].inspect + "! "
if pet != nil then
out = out + "Their pet, the " + pet["name"] + " is chilling out at " + pet['location'].inspect + " "
out = out + "with " + pet["exp"].to_s + " points of power!"
end
m.reply out
end
end
on :message, /^!critters$/ do |m|
synchronize(:rpg) do
mc = $w.get_mc()
out = "Critters crittering around: " + mc.collection().find({"type" => :monster}).count.to_s
out = out + "\n"
out = out + "Bosses bossing around: " + mc.collection().find({"type" => :boss}).count.to_s
out = out + "\n"
out = out + "Events eventing around: " + mc.collection().find({"type" => :random_event}).count.to_s
m.reply out
end
end
on :message, /^!leaderboard$/ do |m|
synchronize(:rpg) do
mc = $w.get_mc()
users = mc.collection.find({"type" => "player"})
sorted = users.to_a.sort_by{|i| i['exp']}
sorted.reverse!
out_str = ""
sorted.each do |i|
out_str = out_str + fix_nick(i['name']) + i['location'].inspect + ": " + i['exp'].to_s + '; '
end
m.reply(out_str)
end
end
on :message, /^!rpgbet (\S*) (\d*)$/ do |m, target, amount|
bets = $w.get_bets()
# m.reply m.user.nick + " wants to post up a bet of " + amount.to_s + " against " + target
m.reply bets.place_bet(m.user.nick, target, amount.to_i)
end
end
bot.start

121
testground.rb Normal file
View file

@ -0,0 +1,121 @@
# This is a very bodged together wrapper that lets you spin up an
# instance of ActiveRpg in its own little sandbox. Randomly generates
# lots of potential turns/players/monsters/bets/whatevers
load "active_rpg.rb"
#configgy bits
collection_name = "testground"
grid_size = 25
player_num = 20
turns = 1000
max_damage = 100
$ar = nil # ActigeRpg instance
$bets = nil
# call this if you want to drop all of the DB.
def reset_all()
$ar.get_mc().collection.drop()
$ar.get_mc().collection().indexes.create_one(_id: 1)
end
# generates a line of given length.
def get_line(len)
line = Array.new(1+rand(len), ".").join
end
# I want to find a way to make it a bit more dynamic in
# adding bets here.... More variations
def place_bet(player, target)
chance = rand(1000)
if chance <= 2 then
amount = 1+rand(10)
puts $bets.place_bet(player, target, amount)
end
end
# start of simulator code
puts "Starting run in collection " + collection_name + ", " + grid_size.to_s + " grid."
puts player_num.to_s + " players, " + turns.to_s + " turns, " + max_damage.to_s + " max damage."
puts "Creating ActiveRpg"
$ar = ActiveRpg.new(grid_size, collection_name)
puts "Grabbing bets..."
$bets = $ar.get_bets()
puts "Dropping and building index on the collection"
reset_all()
players = []
puts "Creating players..."
for x in 1..player_num do
# this should be the basis of aw.take_turn()...
players.push(name = (0...8).map { (65 + rand(26)).chr }.join)
end
puts players.inspect
puts "Starting turns..."
for t in 1..turns do
players.each do |p|
res = $ar.take_turn(p, get_line(max_damage))
if res.to_s != "" then
puts res
end
place_bet(p, players.sample)
end
end
# end of simluation code. Below prints out a summary of it.
puts "FINISHED!\n\n"
print "SUMMARY for " + collection_name + ", grid=" + grid_size.to_s + ". "
print player_num.to_s + " players, " + turns.to_s + " turns, " + max_damage.to_s + " max damage!"
puts
final_players = $ar.get_mc().collection().find({"type" => :player})
puts "Players: " + final_players.count.to_s
final_players.each do |p|
puts p['name'] + ": " + p['location'].inspect + "; " + p['exp'].to_s + "xp; last_line: " + p['last_line']
end
puts
final_monsters = $ar.get_mc().collection().find({"type" => :monster})
puts "Monsters: " + final_monsters.count.to_s
puts
final_bosses = $ar.get_mc().collection().find({"type" => :boss})
puts "Bosses: " + final_bosses.count.to_s
final_bosses.each do |b|
puts b['name'] + ": " + b['location'].inspect + "; " + b['exp'].to_s + "xp; " + b['last_line'] + "; " + b['spawned_by']
end
puts
final_events = $ar.get_mc().collection().find({"type" => :random_event})
puts "Events: " + final_events.count.to_s
final_events.each do |e|
puts e['name'] + ": " + e['location'].inspect
end
puts
open_bets = 0
raw_bets = []
final_players.each do |p|
player_bets = $bets.get_player_bets(p['name'])
open_bets = open_bets + player_bets.count
raw_bets.push(player_bets)
end
puts raw_bets.inspect
puts "Open bets: " + open_bets.to_s