Skip to content

Instantly share code, notes, and snippets.

@scizo
Created May 13, 2011 15:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scizo/970689 to your computer and use it in GitHub Desktop.
Save scizo/970689 to your computer and use it in GitHub Desktop.
Liar's Dice
require 'eventmachine'
module Liard
TEST = false
MINIMUM_PLAYERS = 2
HELP = <<-END
-- Commands from client
BID <num> <val> Creates a bid of "num vals (e.g. four 3's)"
CHALLENGE Challenges last bid (if one exists)
CHAT <msg> Sends a message to all clients
HELP Lists these commands
READY Set client to ready for restart
*SETNAME <name> Set (static) name to use. *Must be called before other commands and within 15 seconds of connecting.
UNREADY Set client to "not ready" for restart
WHO [<connection #>] Request a (list of) NAME response from server
WHOSETURN Request a CURRENTTURN response from server
-- Commands from server
BID <connection #> <num> <val> <name> Indicates a bid from person # of "num vals (eight 5's)" from name
CHALLENGE <connection #> <name> Indicates a challenge from person #/name
CHAT <name>: <msg> Indicates a chat message in this format: <name>: message...
CURRENTTURN <connection #> <seconds> <name> Indicates whose turn it is, turn timeout in seconds, and person's name
LOSEDICE <connection #> <dice> <name> Indicates that person # lost <dice> dice
LOSEDICEALL <except connection #> Indicates that all remaining persons, except one, lose one die each
NAME <connection #> <dice> <name> Declares a person's #, dice remaining, and name (static)
RESULT <connection #> <dice> <#> <[#]> <[etc.]> Reveals another person's roll (after a challenge)
ROLL <numdice> <#> <[#]> <[#]> <[#]> <[#]> <[#]> Your roll for the round
STARTING Indicates a restart in 15 seconds or when all clients report ready (whichever occurs first)
Note: The client (you) is always connection #1.
This means it's your turn whenever you hear, "CURRENTTURN 1 <time> <name>"
END
class Player
attr_accessor :connection, :name, :ready, :dice
def initialize(conn)
@connection = conn
@name = nil
@ready = nil
@dice = []
end
def send(data)
@connection.send_data data
end
def dice_with_value(value)
return @dice.reduce(0) { |c, v| [1, value].include?(v) ? c+=1 : c }
end
def lose_dice(value)
@dice.pop(value)
end
end
class Game
def initialize
@players = []
@started = false
@current_player = nil
@last_player = nil
end
def <<(connection)
player = Player.new connection
@players << player
player
end
def relative_index(from, to)
from = @players.index from if from.is_a?(Player)
to = @players.index to if to.is_a?(Player)
diff = to - from
diff += @players.size if diff < 0
diff + 1
end
def surviving_players
@players.select { |p| p.dice.size > 0 }
end
def next_player
players = surviving_players
i = players.index(@current_player) + 1
i -= players.size if i >= players.size
surviving_players[i]
end
def valid_bid(number, value)
if value < 1 || 6 < value
return false
end
_number, _current_number = number, @current_bid[0]
_number *= 2 if value == 1
_current_number *=2 if @current_bid[1] == 1
return false if _number < _current_number
return false if _number == _current_number and value <= @current_bid[1]
return true
end
def roll(num)
dice = []
num.times { dice << %w[1 2 3 4 5 6].sample }
dice
end
def dice_with_value(value)
return surviving_players.reduce(0) { |sum, p| sum + p.dice_with_value(value) }
end
def start
@started = true
@timer.cancel if @timer
@timer = nil
@current_player = @players.sample
@players.each { |p| p.dice = roll 5 }
start_round
end
def next_round(first_player)
@current_player = first_player
surviving_players.each { |p| p.dice = roll p.dice.size }
end
def start_round
*@current_bid = 0, 0
@players.each do |p|
surviving_players.each { |q| p.send "NAME #{q.name} #{q.dice.size}\n" }
p.send "ROLL #{p.dice.join(' ')}\n"
end
@current_player.send "CURRENTTURN #{@current_player.name} -1\n"
end
def help(player, *args)
player.send HELP
end
def setname(player, name=nil, *args)
if not player.name.nil?
player.send "Error: Name change not allowed.\n"
elsif @players.collect(&:name).include? name
player.send "Error: Name already exists.\n"
else
player.name = name
end
end
def ready(player, *args)
if player.name.nil?
player.send "Error: Client must ID with SETNAME. Use HELP for valid commands.\n"
return
end
player.ready = true
unless TEST
count = @players.count &:ready
if count >= MINIMUM_PLAYERS and @timer.nil? and !@started
@players.each { |p| p.send "STARTING\n" }
@timer = EventMachine::Timer.new(15) { start }
end
end
end
def unready(player, *args)
player.ready = false
count = @players.count &:ready
if count < MINIMUM_PLAYERS and !@timer.nil? and !@started
@timer.cancel
@timer = nil
@players.each { |p| p.send "WAITING\n" }
end
end
def who(player, *players)
requested = @players if players.empty?
requested ||= @players.select do |p|
players.include? relative_index(player, p).to_s
end
requested.each do |p|
player.send "NAME #{p.name || '<unidentified>'} #{p.dice.size}\n"
end
end
def whoseturn(player, *args)
unless @started
player.send "CURRENTTURN #{@current_player.name} -1\n"
else
player.send "Error: No ones turn until the game begins.\n"
end
end
def chat(player, *message)
@players.select { |p| !p.equal? player }.each do |p|
p.send "CHAT <#{player.name}>: #{message.join(' ')}\n"
end
end
def bid(player, number=nil, value=nil, *args)
number, value = number.to_i, value.to_i
if not player.equal?(@current_player)
player.send "Error: It is not your turn. Try WHOSETURN or HELP.\n"
elsif number.nil? or value.nil?
player.send "Error: Invalid Bid: Too few arguments.\n"
elsif not valid_bid number, value
player.send "Error: Invalid Bid.\n"
else
@players.each do |p|
p.send "BID #{player.name} #{number} #{value}\n" unless p.equal?(player)
end
*@current_bid = number, value
@last_player, @current_player = @current_player, next_player
@current_player.send "CURRENTTURN #{@current_player.name} -1\n"
end
end
def challenge(player, *args)
if not player.equal?(@current_player)
return player.send "Error: It is not your turn. Try WHOSETURN or HELP.\n"
elsif @current_bid[0] == 0
return player.send "Error: No previous bids exist. Try BID or HELP.\n"
end
@players.each do |p|
p.send "CHALLENGE #{player.name}\n" unless p.equal?(player)
surviving_players.each do |q|
p.send "RESULT #{q.name} #{q.dice.join(' ')}\n"
end
end
diff = @current_bid[0] - dice_with_value(@current_bid[1])
if diff == 0
surviving_players.each { |p| p.lose_dice 1 unless p.equal?(@last_player) }
end
loser = diff > 0 ? @last_player : @current_player
loser.lose_dice diff.abs unless diff == 0
@players.each do |p|
if diff == 0
surviving_players.each do |q|
p.send "LOSEDICE #{q.name} 1\n" unless q.equal?(@last_player)
end
else
p.send "LOSEDICE #{loser.name} #{diff.abs}\n"
end
end
next_round loser
end
end
class Engine < EventMachine::Connection
def initialize
@buffer = ""
@@game ||= Game.new
@fiber = Fiber.new { poll }
end
def post_init
@player = @@game << self
@fiber.resume
end
def poll
send_data "Liars Dice 0.1\nYour name must be set with the SETNAME command to begin\n"
loop do
command, *args = Fiber.yield
@@game.send command.downcase.to_sym, @player, *args if command
end
end
def receive_data(data)
@buffer << data
while @buffer.include? "\n" do
command, _, @buffer = @buffer.partition "\n"
@fiber.resume command.split
end
end
end
end
if __FILE__ == $0
EventMachine::run do
host = '0.0.0.0'
port = 6789
EventMachine::start_server host, port, Liard::Engine
puts "liard running on #{host}:#{port}..."
end
end
require './liard'
Liard::TEST = true
class Connection
attr_accessor :messages
def initialize
@messages = []
end
def send_data(data)
@messages << data
end
end
game = Liard::Game.new
player1 = game << Connection.new
player2 = game << Connection.new
player3 = game << Connection.new
game.setname(player1, 'scizo')
game.setname(player2, 'smniel')
game.setname(player3, 'scott')
game.ready(player1)
game.ready(player2)
game.ready(player3)
game.start
puts 'player1', player1.connection.messages
puts 'player2', player2.connection.messages
puts 'player3', player3.connection.messages
[player1, player2, player3].each { |p| puts player.dice_with_value(1) }
(1..6).each { |i| puts game.dice_with_value(i), i }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment