#ruby #save #yaml
#ruby #Сохранить #yaml
Вопрос:
Я работаю над текстовым приключением на Ruby под названием Conquest, и я очень доволен тем, как оно продвигается, но я хотел бы добавить возможность сохранения вашей игры в ближайшее время. Раньше я просто делал подобные вещи с YAML, но я бы предположил, что есть способ получше.
Эта игра также немного сложнее, чем другие, которые я создал, поэтому мне нужен очень простой способ управления системой сохранения.
Я думал о том, чтобы сделать что-то подобное с YAML для каждого экземпляра моих пользовательских классов в моей игре:
--- !ruby/object:Player
items: ...
Но я подумал, что это, вероятно, плохой способ сделать это. Я покажу вам некоторые из моих файлов, и мне бы хотелось знать, что, по вашему мнению, является лучшим способом решения этой проблемы. И помните, что это продолжается, поэтому некоторые вещи, такие как задания, работают не полностью, но вы поймете суть.
библиотека/player.rb:
class Player
attr_accessor :items
def initialize
@items = {}
@quests = QuestList.quests
end
def pickup(key, item)
@items[key.to_sym] = item
if key == "scroll"
@quests[:mordor].start
end
end
def inventory
@items.values.each { |item|
a_or_an = %w[a e i o u].include?(item.name[0])
? "an " : "a "
a_or_an = "" if item.name[-1] == "s"
puts "#{a_or_an}#{item.name.downcase}"
}
end
end
библиотека/item.rb:
class Item
attr_reader :name, :description, :hidden, :can_pickup
def initialize(name, description, options = {})
@name = name
@description = description
@options = options
@hidden = options[:hidden] || false
@can_pickup = options[:hidden] || true
add_info
end
def add_info
end
end
class Prop < Item
def add_info
@hidden = true
@can_pickup = false
end
end
# sends you to a new room, usally something that
# has more functionality than just a room
class Transporter < Prop
attr_accessor :goto
def add_info
@hidden = true
@can_pickup = false
@goto = options[:goto]
end
end
# SUBCLASSES BELOW: (only subclass when you have a good reason)
# can be eaten,
# use item.is_a? Food
class Food < Item
end
class Tree < Prop
def climb
if @options[:can_climb]
puts @description
else
puts "You start climbing the tree, but you don't get far before you fall down."
end
end
end
библиотека /quest_list.rb (вероятно, она будет заменена системой сохранения):
module QuestList
QUESTS = {
# this need a better name ⬇️
main: Quest.new("The main mission", []),
mordor: Quest.new("Onward to Mordor", [])
}
def self.quests
QUESTS
end
end
lib/room_list.rb (this’ll probably be replaced by the saving system as well):
module RoomList
ROOMS = {
castle_main:
Room.new("Main room", "This is the main room of the castle. It needs a better descriptionnand name. Theres a hallway south.",
paths: { s: :hallway}
),
hallway:
Room.new("Hallway", "This castle has a long hallway. There is a door to the west andna large room north.",
paths: { n: :castle_main, s: :castle, w: :dinning_hall }
),
dinning_hall:
Room.new("Dinning hall", "The dinning hall. There is a door to the east.",
paths: { e: :hallway }
),
castle:
Room.new("Castle", "You are in the castle. There's a long hallway to the north, andnthe courtyard is to the south.",
paths: { n: :hallway, s: :courtyard }
),
courtyard:
Room.new("Castle courtyard", "You are at the castle courtyard. There's a nice fountain in the center.nThe castle entrance is north. There is a forest south.",
paths: { n: :castle, s: :forest },
items: {
# this peach is useless, it'll confuse people
# a peach: 🍑
peach: Food.new("Peach", "A delicious peach")
}),
forest:
Room.new("Large forest", "This forest is very dense. There is a nice courtyard north.nThe forest continues west and south.",
paths: { n: :courtyard, s: :forest_1, w: :forest__1 }
),
forest__1:
Room.new("Large forest", "This forest is very nice. You can go north, east and west intonsome more forest.",
paths: { n: :forest__2, e: :forest, w: :sticks }
),
sticks:
Room.new("Large forest", "This forest is getting boring, but hey, who knows what you'll find here!nYou can go east.",
paths: { e: :forest__1 },
items: {
sticks: Item.new("Sticks", "Just a couple of sticks. They like they are cedar wood.")
}),
forest__2:
Room.new("Large forest", "You are in a large forest. There looks like theres a grand building overneast, but you can't quite get to it from here. You can go south.",
paths: { s: :forest__1 }
),
forest_1:
Room.new("Large forest", "There is a large, magnificent tree east. The forest continuesnnorth and south.",
paths: { n: :forest, e: :banyan_tree, s: :forest_2 }
),
banyan_tree:
# http://en.wikipedia.org/wiki/Banyan
Room.new("Large banyan tree", "There is a large banyan tree, with many twists and roots going up the tree.nYou can go west.",
paths: { w: :forest_1 },
items: {
tree: Tree.new("Banyan", "You climb up the top of the tree, and see lots of trees and ancastle somewhere around north. It looks like there is a smallnvillage some where south east. You climb back down.", { # 👻
can_climb: true
})}),
forest_2:
Room.new("Large forest", "Just some more forest. The forest continues north and south.",
paths: { n: :forest_1, s: :forest_3 }
),
forest_3:
Room.new("Large forest", "Dang, how many trees are in this forest? You can go north, south, and west.",
paths: { n: :forest_2, s: :forest_4, w: :more_trees }
),
more_trees:
Room.new("Large forest", "You can go east and west.",
paths: { e: :forest_3, w: :more_trees_1 }
),
more_trees_1:
Room.new("Large forest", "You can go east and south.",
paths: { e: :more_trees, s: :more_trees_2 }
),
more_trees_2:
Room.new("Large forest", "You can go north and south.",
paths: { n: :more_trees_1, s: :more_trees_3 }
),
more_trees_3:
Room.new("Large forest", "You can go north and east",
paths: { n: :more_trees_2, e: :path_to_village }
),
path_to_village:
Room.new("Large forest", "Its hard to see because of all these trees, but you think you see a smallnhut to the east. You can also go back west",
paths: { e: :village, w: :more_trees_3 }
),
village:
# add an item or 2 here
Room.new("Abandon village", "There are a bunch of huts here, some people must have lived here before.nThere is some more forest down south. You can go back west into the forest.",
paths: { w: :path_to_village, s: :forest_by_village },
items: {
pickaxe: Item.new("Pickaxe", "Be careful, it looks sharp.")
}),
forest_by_village:
Room.new("Large forest", "Geez more forest. The village is north, and there is a valley east",
paths: { n: :village, e: :valley }
),
valley:
Room.new("Valley", "It's a beautiful valley, with some giganic mountains east, with somensnow of the tops. There is a forest to the west",
paths: { e: :mountains, w: :forest_by_village }
),
mountains:
Room.new("Mountains", "There are many tall mountains with snow on the tops. You can go back west.",
paths: { u: :mountain, w: :valley },
has_mountain: true
),
mountain:
Room.new("Tall mountain", "This mountain is very steep. You can continue climbing or go back down",
paths: { d: :mountains, u: :mountain_1 },
# the scroll and Randy should be moved to mountain_3 once it exists
items: {
scroll: Item.new("Scroll", "Its some kind of elvish... You can't read it.") },
people: {
# Randy will read elvish in the future
randy: Person.new("Randy", "He's just an elf",
race: "Elf",
talk: "I can read elvish. Go figure."
)}),
mountain_1:
Room.new("Tall mountain", "Climbing this mountain is very tiring. You can continue climbingnor go back down",
paths: { d: :mountain }
),
forest_4:
Room.new("Large forest", "There is a lot of trees here. It's very shady in this area.nThe forest continues north.",
paths: { n: :forest_3 }
)
}
def self.room_list
ROOMS
end
end
lib/delegate.rb:
class Delegate
attr_accessor :current_room
def initialize
@rooms = RoomList.room_list
@player = Player.new
@current_room = @rooms[:courtyard]
@help = 0
end
def parse(input)
directions = "up|down|north|east|south|west|u|d|n|e|s|w"
# input will always be converted to lower case before getting here
case input
when /^(?<direction>(#{directions}))$/
direction = $~[:direction]
walk(direction)
when /^(go|walk)( (?<direction>#{directions}|to mordor))?$/
direction = $~[:direction]
if direction
walk(direction)
else
puts "#{input.capitalize} where?"
end
when /^(get|take|pickup|pick up)( (?<item>[a-z ] ))?$/
item = $~[:item]
if item
pickup(item)
else
puts "Please supply an object to #{input}."
end
when /^look( (?<item>[a-z] ))?$/
item = $~[:item]
item.nil? ? look : inspect(item)
when /^inspect( (?<item>[a-z] ))?$/
item = $~[:item]
if item
inspect(item)
else
puts "Please supply an object to inspect."
end
when /^rub sticks( together)?$/
rub_sticks
when /^quests?$/
# this is probably going to be a for statement. You understand thos more than i do so have at it.
# this should loop through the list of quests in quests.yml and return the ones that are true
# correction: it should call .each, for statments are bad practice in ruby
when /^(i|inv|inventory)$/
inventory
when /^climb( (?<tree_name>[a-z] ))?( tree)?$/
# this regex needs to be cleaned up, just the tree part really
# nvm, the whole regex sucks
🌳 = $~[:tree_name]
climb(🌳)
# doesn't have to be a tree...
when /^(help|h)$/
@smart_aleck ||= ["Why?","No.","Stop asking plz.","seriously, shut up.","...","...","...","Ok, seriously.","Do u not understand the meaning of "be quiet"?","ug"].to_enum
begin
puts @smart_aleck.next
rescue StopIteration
@smart_aleck.rewind
puts @smart_aleck.next
end
when /^(quit|exit)$/
quit
when /^s?$/
else
😱 = ["I don't speak jibberish.","Speak up. Ur not making any sense.","R u trying to confuse me? Cuz dats not gonna work","What the heck is that supposed to mean?"]
puts 😱.sample
end
end
def walk(direction)
if direction != "to mordor"
if new_room = @rooms[@current_room[direction]]
@current_room = new_room.enter
else
puts "You can't go that way."
end
else
#TODO: add quest system. We should have a main quest and other side quests like going to mordor.
puts "One does not simply walk to Mordor... You need to find the eagles. They will take you to Mordor."
end
end
def pickup(item)
if _item = @current_room.items[item.to_sym]
if _item.can_pickup
_item = @current_room.remove_item(item)
@player.pickup(item, _item)
else
puts "You can't pick that up."
end
else
puts "That item isn't in here."
end
end
def inventory
@player.inventory
end
def look
@current_room.look
end
def inspect(item)
# this could be refactored
if the_item = @player.items[item.to_sym]
puts the_item.description
elsif the_item = @current_room.items[item.to_sym]
puts the_item.description
else
puts "This item is not here or your inventory."
end
end
def rub_sticks
if @player.items[:sticks]
# do something involving fire
puts "I need to implement this."
end
end
def climb(thing_name)
if 🌳 = @current_room.items[:tree]
name = 🌳.name.downcase
if thing_name.nil? || thing_name == "tree" || thing_name == name
🌳.climb
else
puts "You can't climb that."
end
# I don't like how this works :(
elsif @current_room.options[:has_mountain]
if ["up", "mountain", nil].include? thing_name
walk("u")
end
else
puts "You can't climb that."
end
end
def quit
exit
end
end
lib/quest.rb:
class Quest
attr_accessor :steps
def initialize(name, steps, options = {})
@name = name
# steps (the argument) should be a hash like this:
# [:found_ring, :melted_ring]
@steps = steps.inject({}) { |hash, step| hash[step] = false; hash }
# then @step will be this:
# { found_ring: false, melted_ring: false }
@started = false
@options = options
end
def start
@started = true
puts "#{'Quest started!'.cyan} - #{name}"
end
end
библиотека/room.rb:
class Room
attr_reader :items, :options, :people
def initialize(name, description, options = {})
@name = name
@description = description
@paths = options[:paths] || {}
@items = options[:items] || {}
@people = options[:people] || {}
@options = options
@visited = false
end
def [](direction)
@paths[direction.to_sym]
end
def enter
puts @name.cyan
unless @visited
puts @description
list_items
end
@visited = true # can't hurt to set it every time, right?
self
end
def remove_item(item)
@items.delete(item.to_sym)
end
def look
puts @name.cyan
puts @description
list_items
end
def list_items
visible_items = @items.values.select { |i| (!i.hidden) amp;amp; i.can_pickup }
unless visible_items.empty?
puts "Items that are here:".magenta
visible_items.map do |item|
a_or_an = %w[a e i o u].include?(item.name[0])
? "an " : "a "
a_or_an = "" if item.name[-1] == "s"
puts "#{a_or_an}#{item.name.downcase}"
end
end
visible_people = @people.values.select { |i| (!i.hidden) amp;amp; i.can_pickup }
unless visible_people.empty?
puts "People that are here:".magenta
visible_people.map do |people|
puts "#{people.name}"
end
end
end
end
Я знаю, что Delegate.current_room
это должно быть свойством Player not Delegate, если я собираюсь ее сохранять, просто у меня не нашлось времени это исправить.
Итак, что вы все думаете о сохранении этого с помощью сериализации YAML? (уродливые !ruby/object:Class
вещи)
Мне бы очень хотелось узнать о лучшем способе решения этой проблемы, но я ничего не могу придумать. Я подумал, что мог бы поместить yaml или другой формат сохранения в ~/.conquest_save
Я был бы рад услышать здесь все ваши комментарии, спасибо! А полный проект находится на Github здесь.
Комментарии:
1. почему не db и
player_gateway
?2. Вы могли бы добавить базу данных sqlite3, поскольку это всего лишь файл. Это был бы самый элегантный и понятный способ, который приходит на ум.
3. Я думал об этом, но я не знал, можете ли вы или как сохранять объекты ruby, такие как мой класс player.
Ответ №1:
Вы можете попробовать использовать Marshal.
http://ruby-doc.org/core-2.1.2/Marshal.html
Два основных метода, которые нужно использовать, будут следующими:
data = Marshal.dump(o)
obj = Marshal.load(data)
На самом деле LPC MUD традиционно используют SQL. Я обнаружил это, когда у меня возникла какая-то ошибка и произошел сбой матрицы, затем, просмотрев журнал, я, к своему большому удивлению, увидел ошибки, связанные с SQL.
Я также попытал счастья с MUD и на самом деле сказал себе, что если это станет сложнее, чем файлы yaml, то этим путем я не пойду.
Я думаю, если ваша система действительно усложнится, вы пробовали абстрагироваться и просто использовать оболочку поверх чего-то вроде postgresql или mysql, например activerecord?
Комментарии:
1. Это говорит о том, что это часть Ruby Core в версии 2.1.2, которую я использую, но в ней говорится
load such file -- marshal
, что мне нравится эта идея, спасибо!2. Я добавил это в свою игру, и это отлично работает! Спасибо!
3. У меня есть вопрос по этому поводу, недавно мне пришлось прекратить использовать Marshal для сохранения моей игры, потому что он не обновил значения, которые я изменил в более поздней версии. Например, у каждой комнаты есть
@paths
свойство, которое является хэшем. Итак, как только Маршалл сохранит комнату, если я добавлю путь к@paths
, его там не будет, пока я не перезапущу игру и не удалю старый файл, который был сохранен. Это всего лишь один пример, это произошло со многими другими вещами, такими как@tasks
свойство квеста и многое другое. У вас есть идея, как предотвратить это и по-прежнему использовать Marshal?