Created
May 13, 2011 15:02
-
-
Save scizo/970689 to your computer and use it in GitHub Desktop.
Liar's Dice
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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