Skip to content

Instantly share code, notes, and snippets.

@rjungemann
Created January 19, 2010 02:57
Show Gist options
  • Save rjungemann/280615 to your computer and use it in GitHub Desktop.
Save rjungemann/280615 to your computer and use it in GitHub Desktop.
Maoiste, a library which plays/mediates the card game, Mao
# the names that are generated for cards are designed to correlate
# with the naming system used for this SVG-formatted deck of cards
# http://david.bellot.free.fr/svg-cards/
class Object
def blank?
self.nil? || (self.respond_to?(:empty?) && self.empty?)
end
end
class Array
# return an item from a random spot in the array
def pick; self[self.rand_i] end
# remove an item from a random spot in the array and return it
def pull; self.delete_at(self.rand_i) end
# insert an item at a random spot in an array
def slip obj; self.insert(self.rand_i, obj) end
# shuffle the items in the array to new locations
def shuffle!
size.downto(1) { |n| push delete_at(rand(n)) }
self
end
# return a valid, randomly chosen index of an array
def rand_i; (rand * self.size).floor end
end
class Card
@@valid_numbers = (1..10).map { |i| i.to_s }.to_a + %w[jack queen king]
@@valid_suits = %w[club diamond heart spade]
@@valid_jokers = %w[black_joker red_joker]
def self.deck
@@valid_numbers.collect { |number|
@@valid_suits.collect { |suit| "#{number}_#{suit}" }
}.flatten + @@valid_jokers
end
def self.parse card
if @@valid_jokers.include? card
{ :name => card, :suit => "joker" }
elsif
tuple = card.split("_")
raise "Invalid card." if tuple.size != 2
raise "Invalid number." unless @@valid_numbers.include?(tuple.first)
raise "Invalid suit." unless @@valid_suits.include?(tuple.last)
{ :name => card, :number => tuple.first, :suit => tuple.last, :number_value => @@valid_numbers.index(tuple.first) + 1 }
else
nil
end
end
end
# A simulation of the Game of Mao, including rule mutations.
# http://en.wikipedia.org/wiki/Mao_(card_game)
#
# Maoiste is designed to be played over sockets. The server isn't fully implemented,
# but here is a class representing game logic. Unlike the real Game of Mao, the rule
# mutators are not as robust. I intend to use jruby's sandbox library, combined with
# the gazer gem, to allow users to mutate this class using a reasonably accessible
# DSL. Currently mutations can be done by altering instance variables of the Maoiste
# class.
require "#{File.dirname(__FILE__)}/card.rb"
class Maoiste
def initialize
@deck = Card.deck.shuffle
@discard_pile = []
@players = []
@index = 0
@hand_size = 5
@dealer = nil
@ended = nil
@valid_card_conditions = [
{ :is_joker => lambda { |discarded, played| played[:suit] == "joker" || discarded[:suit] == "joker" }},
{ :is_greater_or_equal => lambda { |discarded, played| played[:number_value] >= discarded[:number_value] }},
{ :is_ace_played_on_king => lambda { |discarded, played| played[:number_value] == 1 && discarded[:number_value] == 13 }}
]
@increment_index = lambda { |index, players|
index += 1
index = 0 if index >= players.size
index
}
end
def ended?; @ended end
def valid_card? card
raise "Last discarded card doesn't exist." if last_discarded_card.blank?
raise "Played card doesn't exist." if card.blank?
discarded = Card.parse last_discarded_card
played = Card.parse card
if @valid_card_conditions.to_a.find { |c| c.values.first[discarded, played] == true }
card
else
nil
end
end
def last_discarded_card; @deck.last end
def add_player name
unless @dealer
@dealer = name
add_player name
else
unless(name.blank? || @players.include?(name))
player = { :name => name, :hand => [], :picked_up_hand => false, :messages => [] }
@players << player
player
end
end
end
def remove_player name
@players.reject! { |player| player == name }
end
def find_player name
@players.find { |player| player[:name] == name }
end
def send_message name, message
puts %{Sent "#{message.first}" to "#{name}"}
pl = find_player name
pl[:messages] << (message.dup << Time.now)
end
def start
start_message = ["started", "The game is Mao. play proceeds to the left by either suit or number."]
@discard_pile << @deck.pull
@players.each { |player|
@hand_size.times { player[:hand] << @deck.pull }
send_message player[:name], start_message
}
true
end
def pick_up_hand name
if @discard_pile.size < 1
send_message(name, ["error", "Game hasn't started yet."])
return
end
pl = find_player(name)
send_message(name, ["error", "You arent't playing this round."]) if pl.blank?
if(name == @dealer || find_player(@dealer)[:picked_up_hand])
pl[:picked_up_hand] = true
send_message(name, ["picked_up_hand", pl[:hand]])
else
pl[:hand] << @deck.pull
send_message(name, ["penalty", "Picking up cards too soon."])
end
pl
end
def draw_card name
if @discard_pile.size < 1
send_message(name, ["error", "Game hasn't started yet."])
return
end
end_game if @deck.size < 1
pl = find_player(name)
send_message(name, ["error", "You arent't playing this round."]) if pl.blank?
send_message(name, ["error", "You haven't picked up your hand yet."]) unless pl[:picked_up_hand]
send_message(name, ["drew", card])
pl
end
def play name, card = nil
if @discard_pile.size < 1
send_message(name, ["error", "Game hasn't started yet."])
return
end
pl = find_player(name)
send_message(name, ["error", "You aren't playing this round."]) if pl.blank?
send_message(name, ["error", "You haven't picked up your hand yet."]) unless pl[:picked_up_hand]
if card
send_message(name, ["error", "You do not have that card."]) unless pl[:hand].include? card
end_game if(pl[:hand].size < 1)
pl[:hand].reject! { |c| card == c }
@discard_pile << card
end_game if(pl[:hand].size < 1)
send_message(name, ["played", card])
send_message(name, ["penalty", "Playing an invalid card."]) unless(valid_card? card)
else
draw_card name
end
send_message(name, ["penalty", "Playing out of order."]) unless(@players.index(pl) == @index)
pl
end
def end_turn name
if @discard_pile.size < 1
send_message(name, ["error", "Game hasn't started yet."])
return
end
pl = find_player(name)
send_message(name, ["error", "You aren't playing this round."]) if pl.blank?
send_message(name, ["error", "You haven't picked up your hand yet."]) unless pl[:picked_up_hand]
send_message(name, ["error", "It's not currently your turn."]) unless(@players.index(pl) == @index)
@increment_index[@index, @players]
next_pl = @players[@index]
send_message(name, ["ended_turn"])
send_message(next_pl[:name], ["your_turn"])
pl
end
def end_game
@ended = true
if @discard_pile.size < 1
send_message(name, ["error", "Game hasn't started yet."])
return
end
@players.each do |player|
if player[:hand].size < 1
send_message(player[:name], ["won", "You won this match."])
else
send_message(player[:name], ["lost", "You lost this match."])
end
end
true
end
end
# Not exactly a test in the classical sense of the word. Rather, this method is
# a simulation of the game of Mao. The computer will play a match against itself,
# very quickly (about ten seconds on my computer). This also illustrates the API.
require "#{File.dirname(__FILE__)}/maoiste.rb"
class Maoiste
def self.test
require 'pp'
pp m = Maoiste.new
pp m.add_player "dealer"
pp m.add_player "one"
pp m.add_player "two"
pp m.start
pp m.pick_up_hand "one" # results in "penalty, picking up cards too soon"
pp m.pick_up_hand "dealer"
pp m.pick_up_hand "one"
pp m.pick_up_hand "two"
until m.ended?
%w[dealer one two].each do |name|
break if m.ended?
pp m.last_discarded_card
pp m.draw_card name
break if m.ended?
card = (rand > 0.5) ? nil : m.find_player(name)[:hand].pick
pp m.play name, card
break if m.ended?
pp m.end_turn name
end
end
m
end
end
# What the implementation of the "nice day rule" will look like. I need to finish
# implementing hooks for when a turn ends, etc.
#
# The "expectation" methods are used when a particular action is required from a
# user, such as saying "have a nice day" when a double of a card is played, or
# to "slap" a Jack when a Jack appears.
class Maoiste
def expect_action action, players
@expectations ||= {}
@expectations[action] = { :players => players, :fulfillments => [] }
end
def act name, action
@expectations[action][:fulfillments] << name
end
def end_expectation action, &block
yield action, @expectations
end
end
#m.when_card_played do |name, card|
# if card == last_discarded_card
# @nice_day_level = @nice_day_level + 1 || 1
# @nice_day_action = "have_#{'very_' * (@nice_day_level - 1)}nice_day"
#
# expect_action @nice_day_action, *@players
# else
# @nice_day_level = 0
# end
#end
#
#m.when_turn_ended do |name|
# end_expectation(@nice_day_action) do |action, expectations|
# @nice_day_action = nil
#
# @players.each do |player|
# unless expectations[:fulfillments].include? player[:name]
# send_message(name, ["penalty", "Failure to have a #{'very ' * (@nice_day_level - 1)}nice day."])
# draw_card player[:name]
# end
# end
# end
#end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment