480, false self.caption = "Tutorial 2" @background_image = Gosu::Image.new(self, "bg1.jpg", true) end def update end def draw @background_image.draw(0, 0, 0) end end window = GameWindow.new window.show x y z Sunday, 9 June, 13
= "Tutorial 3" @background_image = Image.new(self, "bg1.jpg", true) @player = Player.new(self) @player.warp(320, 240) end def update @player.turn_left if button_down? KbLeft @player.turn_right if button_down? KbRight @player.accelerate if button_down? KbUp @player.move end def draw @player.draw @background_image.draw(0, 0, 0) end end place him in middle of screen Sunday, 9 June, 13
= "Tutorial 3" @background_image = Image.new(self, "bg1.jpg", true) @player = Player.new(self) @player.warp(320, 240) end def update @player.turn_left if button_down? KbLeft @player.turn_right if button_down? KbRight @player.accelerate if button_down? KbUp @player.move end def draw @player.draw @background_image.draw(0, 0, 0) end end } move according to user input Sunday, 9 June, 13
= "Tutorial 3" @background_image = Image.new(self, "bg1.jpg", true) @player = Player.new(self) @player.warp(320, 240) end def update @player.turn_left if button_down? KbLeft @player.turn_right if button_down? KbRight @player.accelerate if button_down? KbUp @player.move end def draw @player.draw @background_image.draw(0, 0, 0) end end draw the player Sunday, 9 June, 13
used as sprites Often placed in sequence, image can be retrieved from knowing the location Reduces memory usage and increase drawing speed Sunday, 9 June, 13
..................... ..................... ..................... ..................... ..................... 20 x 32 = 640 15 x 32 = 480 class Map def initialize(window, mapfile) lines = File.readlines(mapfile).map { |line| line.chomp } @window, @height, @width = window, lines.size, lines.first.size @tiles = Array.new(@width) do |x| Array.new(@height) do |y| case lines[y][x] when '.' SpriteImage::Earth when "#" SpriteImage::Wall when '"' SpriteImage::Grass end end end end def draw @height.times do |y| @width.times do |x| tile = @tiles[x][y] @window.spritesheet[tile].draw(x * 32, y * 32, 1) end end Sunday, 9 June, 13
be in the client Server should only receive messages and broadcast to all clients Messages not compressed/encoded (takes time at the server) Don’t send useless messages Sunday, 9 June, 13
object:tank1 object:tank2 object:tank2 object:shot1 object:shot1 object:shot1 Tank 1 creates a shot, message sent to server and broadcast to everyone Sunday, 9 June, 13
Tank2 object:tank2 object:tank1 object:tank2 object:tank2 delete:shot1 delete:shot1 delete:shot1 When the shot goes out of range, Tank 1 sends a delete message to the server, broadcasted to everyone Sunday, 9 June, 13
object:tank1 Tank2 object:tank2 object:tank1 object:tank2 object:tank2 object:shot1 object:shot1 object:shot1 If Tank 1’s shot hits Tank 2, reduce hit points from Tank1 Sunday, 9 June, 13
accumulated till and sent only 1 time in a frame refresh Messages from client -> server : message type + sprite Message from server -> client : sprite only Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear store my previous coordinates Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear move! Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear go back to previous coordinates if I hit the wall, go out or hit another tank Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear add me to the list of messages to send to server Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear check the other shots on screen to see if it hits me, if it does, tell the server I was hit Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear move my shots, if it hits the wall or goes out, remove it Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear if not, tell the server its new position Sunday, 9 June, 13
@me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield? @other_tanks.each do |player, tank| @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30) end add_to_message_queue('obj', @me) @other_shots.each_value do |shot| if @me.alive? and @me.collide_with?(shot, 16) @me.hit add_to_message_queue('obj', @me) end end @me_shots.each do |shot| shot.move # move the bullet if shot.hit_wall? or shot.outside_battlefield? @me_shots.delete shot add_to_message_queue('del', shot) else add_to_message_queue('obj', shot) end end @client.send_message @messages.join("\n") @messages.clear all my actions are processed, now to send messages to server Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end read messages from the server Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end parse the server messages into sprites Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end for tank sprites other than me, set the properties and move it Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end only time the server tells me about my changes is when I’m hit Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end move the shot sprites Sunday, 9 June, 13
|row| sprite = row.split("|") if sprite.size == 9 player = sprite[3] @valid_sprites << sprite[0] case sprite[1] when 'tank' unless player == @player if @other_tanks[player] @other_tanks[player].points = sprite[7].to_i @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6]) else @other_tanks[player] = Tank.from_sprite(self, sprite) end else @me.points = sprite[7].to_i end when 'shot' unless player == @player shot = Shot.from_sprite(self, sprite) @other_shots[shot.uuid] = shot shot.warp_to(sprite[4], sprite[5], sprite[6]) end end end end Sunday, 9 June, 13
!@valid_sprites.include?(tank.uuid) end end if shots and tanks (other than myself) weren’t broadcast from the server, this means they’ve been removed Sunday, 9 June, 13
port) puts "Starting Tanks Arena at #{host}:#{port}." @server = TCPServer.new(host, port) @sprites = Hash.new @players = Hash.new async.run end def shutdown @server.close if @server end def run loop { async.handle_connection @server.accept } end What to do when the server terminates Sunday, 9 June, 13
port) puts "Starting Tanks Arena at #{host}:#{port}." @server = TCPServer.new(host, port) @sprites = Hash.new @players = Hash.new async.run end def shutdown @server.close if @server end def run loop { async.handle_connection @server.accept } end Run the Arena object in a new thread Sunday, 9 June, 13
port) puts "Starting Tanks Arena at #{host}:#{port}." @server = TCPServer.new(host, port) @sprites = Hash.new @players = Hash.new async.run end def shutdown @server.close if @server end def run loop { async.handle_connection @server.accept } end When a client connects, handle the connection in a new thread Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Uniquely identifies a user Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Get data from the client Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Add to list of players if player is new Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Add to list of sprites in this server Sunday, 9 June, 13
puts "#{user} has joined the arena." loop do data = socket.readpartial(4096) data_array = data.split("\n") if data_array and !data_array.empty? begin data_array.each do |row| message = row.split("|") if message.size == 10 case message[0] when 'obj' @players[user] = message[1..9] unless @players[user] @sprites[message[1]] = message[1..9] when 'del' @sprites.delete message[1] end end . . . Remove sprite from this server Sunday, 9 June, 13
<< sprite.join("|") << "\n") if sprite end socket.write response end rescue Exception => exception puts exception.backtrace end end # end data end # end loop rescue EOFError => err player = @players[user] puts "#{player[3]} has left arena." @sprites.delete player[0] @players.delete user socket.close end end Sunday, 9 June, 13
<< sprite.join("|") << "\n") if sprite end socket.write response end rescue Exception => exception puts exception.backtrace end end # end data end # end loop rescue EOFError => err player = @players[user] puts "#{player[3]} has left arena." @sprites.delete player[0] @players.delete user socket.close end end Send list of sprites to the client Sunday, 9 June, 13
<< sprite.join("|") << "\n") if sprite end socket.write response end rescue Exception => exception puts exception.backtrace end end # end data end # end loop rescue EOFError => err player = @players[user] puts "#{player[3]} has left arena." @sprites.delete player[0] @players.delete user socket.close end end If client disconnects, remove the player and sprite Sunday, 9 June, 13
= Arena.supervise(server, port.to_i) trap("INT") do supervisor.terminate exit end sleep Monitors and restarts the server if it crashes Sunday, 9 June, 13
= Arena.supervise(server, port.to_i) trap("INT") do supervisor.terminate exit end sleep Nothing for the main thread to do so, sleep and let the other threads run Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Start server at any of these ports Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Registry of all arenas Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Start arena Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Register arena Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Terminate arena Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Let client know about the customizations Sunday, 9 June, 13
= Celluloid::Actor.all haml :arenas end post "/arena/start" do port = @@port_range.delete @@port_range.sample arena = Arena.new(request.host, port, request.port) arena.map_url = params[:map_url] arena.spritesheet_url = params[:spritesheet_url] arena.default_hp = params[:default_hp].to_i Celluloid::Actor[:"arena_#{port}"] = arena redirect "/" end get "/arena/stop/:name" do raise "No such arena" unless Celluloid::Actor[params[:name].to_sym] Celluloid::Actor[params[:name].to_sym].terminate redirect "/" end get "/config/:name" do arena = Celluloid::Actor[params[:name].to_sym] array = [arena.map_url, arena.spritesheet_url, arena.default_hp.to_s].join("|") end Sunday, 9 June, 13