cardgame 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +9 -0
- data/TODO.md +6 -0
- data/bin/game +78 -0
- data/cardgame.gemspec +25 -0
- data/lib/cardgame.rb +6 -0
- data/lib/cardgame/card.rb +43 -0
- data/lib/cardgame/deck.rb +45 -0
- data/lib/cardgame/french.rb +17 -0
- data/lib/cardgame/player.rb +57 -0
- data/lib/cardgame/uno.rb +17 -0
- data/lib/cardgame/version.rb +3 -0
- data/test/fixtures/french.json +24 -0
- data/test/fixtures/uno.yaml +41 -0
- data/test/helper.rb +50 -0
- data/test/unit/test_card.rb +11 -0
- data/test/unit/test_french.rb +13 -0
- data/test/unit/test_uno.rb +15 -0
- metadata +84 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
data/TODO.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
* Extract the playing engine
|
2
|
+
* Finish the retry-mode after a player violated the rules
|
3
|
+
* Build a rules engine that enforces the game rules and prevents cheating (e.g. it must not be possible to add new cards to the deck)
|
4
|
+
* Implement swapping of cards when a zero was played
|
5
|
+
* Implement reverse order if Reverse card was played
|
6
|
+
* Can we detect the endless loop that occasionally occurs?
|
data/bin/game
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
require 'cardgame'
|
5
|
+
include CardGame
|
6
|
+
|
7
|
+
class RuleViolation < StandardError; end
|
8
|
+
|
9
|
+
#
|
10
|
+
# Ensure that everyone follows the rules
|
11
|
+
#
|
12
|
+
def matches_rules(played, pile)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnoUno < StandardError
|
17
|
+
attr_reader :player, :card
|
18
|
+
def initialize(player, card)
|
19
|
+
@player, @card = player, card
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
players = []
|
24
|
+
('A'..'C').each{|i| players << Player.new(i)}
|
25
|
+
|
26
|
+
stack = Uno.new.deal
|
27
|
+
|
28
|
+
Array.class_eval do
|
29
|
+
def replenish(pile)
|
30
|
+
concat(pile)
|
31
|
+
pile.clear
|
32
|
+
pile.push(pop) # the last card stays on the pile
|
33
|
+
sort_by!{rand}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# deal out cards to the players
|
38
|
+
7.times{
|
39
|
+
players.each{|player|
|
40
|
+
player << stack.pop
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
# init the game
|
45
|
+
pile = []
|
46
|
+
|
47
|
+
begin
|
48
|
+
pile.push(stack.pop)
|
49
|
+
end while !pile.first && pile.first.trump? # do not allow trumps (jokers) as initial card
|
50
|
+
|
51
|
+
# puts "Starting the game with #{players.size} players"
|
52
|
+
rounds = 0
|
53
|
+
|
54
|
+
# main game loop - ask each player to play
|
55
|
+
loop do
|
56
|
+
begin
|
57
|
+
rounds += 1
|
58
|
+
players.each{|player|
|
59
|
+
begin
|
60
|
+
played = player.play(pile.last)
|
61
|
+
|
62
|
+
if played && matches_rules(played, pile.last)
|
63
|
+
pile.push(played)
|
64
|
+
else
|
65
|
+
stack.replenish(pile) if stack.empty? # replenish stack from pile
|
66
|
+
player << stack.pop # draw a card from the stack
|
67
|
+
end
|
68
|
+
rescue RuleViolation => v
|
69
|
+
puts v.message
|
70
|
+
player << played # Push the offending card back to the player
|
71
|
+
retry
|
72
|
+
end
|
73
|
+
}
|
74
|
+
rescue UnoUno => u
|
75
|
+
puts "#{u.player} wins with #{u.card} after #{rounds} rounds."
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
end
|
data/cardgame.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "cardgame/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cardgame"
|
7
|
+
s.version = CardGame::VERSION
|
8
|
+
s.authors = ["Nicholas E. Rabenau"]
|
9
|
+
s.email = ["nerab@gmx.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Models card games, e.g. Uno}
|
12
|
+
s.description = %q{This gem is a small exercise in OO modeling. It models card games, especially Uno.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "cardgame"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
s.add_runtime_dependency 'activesupport', '~> 3.2'
|
25
|
+
end
|
data/lib/cardgame.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module CardGame
|
2
|
+
class BaseCard
|
3
|
+
attr_reader :rank
|
4
|
+
|
5
|
+
def initialize(rank)
|
6
|
+
@rank = rank
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
@rank
|
11
|
+
end
|
12
|
+
|
13
|
+
def trump?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Trump < BaseCard
|
19
|
+
# When playing a trump card, the player can set which suit it has so that the next player has to follow it
|
20
|
+
attr_accessor :suit
|
21
|
+
|
22
|
+
def trump?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"#{super} requiring #{@suit}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Card < BaseCard
|
32
|
+
attr_reader :suit
|
33
|
+
|
34
|
+
def initialize(rank, suit)
|
35
|
+
super(rank)
|
36
|
+
@suit = suit
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{@suit} #{super}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module CardGame
|
4
|
+
class Deck
|
5
|
+
class << self
|
6
|
+
def ranks
|
7
|
+
[]
|
8
|
+
end
|
9
|
+
|
10
|
+
def trumps
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
|
14
|
+
def suits
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@cards = []
|
23
|
+
|
24
|
+
self.class.ranks.each{|name|
|
25
|
+
self.class.suits.each{|color|
|
26
|
+
push Card.new(name, color)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
self.class.trumps.each{|j| push Trump.new(j)}
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Deal out a shuffled deck
|
35
|
+
#
|
36
|
+
def deal
|
37
|
+
@cards.sort_by{rand}
|
38
|
+
end
|
39
|
+
|
40
|
+
def_delegator :@cards, :size
|
41
|
+
|
42
|
+
private
|
43
|
+
def_delegator :@cards, :push
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module CardGame
|
4
|
+
class Player
|
5
|
+
extend Forwardable
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
@hand = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"Player #{@name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Select one or more of our cards (according to our playing strategy)
|
19
|
+
#
|
20
|
+
# Note the the player has no further knowledge about the state of the game except the topmost card of the pile that is passed.
|
21
|
+
# If a player wants to implement more advanced strategies (e.g. by observing and counting the cards already played),
|
22
|
+
# we need to implement a notification mechanism that tells every player which card the current player played.
|
23
|
+
#
|
24
|
+
# TODO Need to obey if top_card is a trump, unless we can trump that one
|
25
|
+
# TODO If the game system allows it, return two identical cards
|
26
|
+
#
|
27
|
+
def play(top_card)
|
28
|
+
raise "There must be a card on the pile" if top_card.nil?
|
29
|
+
|
30
|
+
# puts
|
31
|
+
# puts "#{self} plays:"
|
32
|
+
# puts "#{@hand.size} in hand and sees the top card #{top_card}"
|
33
|
+
|
34
|
+
candidates = @hand.select{|card| top_card.rank == card.rank || top_card.suit == card.suit || card.trump?}
|
35
|
+
|
36
|
+
selected = candidates.sort_by{rand}.pop
|
37
|
+
|
38
|
+
if selected.nil?
|
39
|
+
# puts "Decided NOT to play"
|
40
|
+
else
|
41
|
+
selected.suit = Uno.suits.sample if selected.trump? # TODO Improve suit selection to take our current hand into account
|
42
|
+
# puts "Decided to play #{selected}"
|
43
|
+
@hand.delete_if{|card| selected == card}
|
44
|
+
# puts "Has #{@hand.size} left in hand."
|
45
|
+
end
|
46
|
+
|
47
|
+
# raise Uno.new(selected) if 1 == @hand.size
|
48
|
+
raise UnoUno.new(self, selected) if 0 == @hand.size
|
49
|
+
|
50
|
+
selected
|
51
|
+
end
|
52
|
+
|
53
|
+
def_delegator :@hand, :size
|
54
|
+
def_delegator :@hand, :<<
|
55
|
+
def_delegator :@hand, :empty?
|
56
|
+
end
|
57
|
+
end
|
data/lib/cardgame/uno.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module CardGame
|
2
|
+
class Uno < Deck
|
3
|
+
class << self
|
4
|
+
def ranks
|
5
|
+
['0'].concat(%w{1 2 3 4 5 6 7 8 9 Reverse Skip} * 2).concat(['Draw 2'] * 2)
|
6
|
+
end
|
7
|
+
|
8
|
+
def trumps
|
9
|
+
["Wild Draw 4"] * 4 + ["Wild"] * 4
|
10
|
+
end
|
11
|
+
|
12
|
+
def suits
|
13
|
+
%w{Blue Green Red Yellow}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
---
|
2
|
+
:ranks:
|
3
|
+
- '0'
|
4
|
+
- '1'
|
5
|
+
- '2'
|
6
|
+
- '3'
|
7
|
+
- '4'
|
8
|
+
- '5'
|
9
|
+
- '6'
|
10
|
+
- '7'
|
11
|
+
- '8'
|
12
|
+
- '9'
|
13
|
+
- Reverse
|
14
|
+
- Skip
|
15
|
+
- '1'
|
16
|
+
- '2'
|
17
|
+
- '3'
|
18
|
+
- '4'
|
19
|
+
- '5'
|
20
|
+
- '6'
|
21
|
+
- '7'
|
22
|
+
- '8'
|
23
|
+
- '9'
|
24
|
+
- Reverse
|
25
|
+
- Skip
|
26
|
+
- Draw 2
|
27
|
+
- Draw 2
|
28
|
+
:trumps:
|
29
|
+
- Wild Draw 4
|
30
|
+
- Wild Draw 4
|
31
|
+
- Wild Draw 4
|
32
|
+
- Wild Draw 4
|
33
|
+
- Wild
|
34
|
+
- Wild
|
35
|
+
- Wild
|
36
|
+
- Wild
|
37
|
+
:suits:
|
38
|
+
- Blue
|
39
|
+
- Green
|
40
|
+
- Red
|
41
|
+
- Yellow
|
data/test/helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'cardgame'
|
5
|
+
require 'yaml'
|
6
|
+
require 'json'
|
7
|
+
require 'active_support/core_ext/string'
|
8
|
+
|
9
|
+
class Test::Unit::TestCase
|
10
|
+
end
|
11
|
+
|
12
|
+
module CardGameTest
|
13
|
+
class TestCase < Test::Unit::TestCase
|
14
|
+
FIXTURES_DIR = File.join(File.dirname(__FILE__), 'fixtures')
|
15
|
+
|
16
|
+
def fixture(name = self.class.name.underscore.match(/test_(.*)/)[1])
|
17
|
+
file_name = nil
|
18
|
+
|
19
|
+
%w{.yaml .yml .json}.each{|ext|
|
20
|
+
file_name = File.join(FIXTURES_DIR, "#{name}#{ext}")
|
21
|
+
break if File.exist?(file_name)
|
22
|
+
}
|
23
|
+
|
24
|
+
raw = File.read(file_name)
|
25
|
+
|
26
|
+
case File.extname(file_name)
|
27
|
+
when '.yaml'
|
28
|
+
YAML.load(raw)
|
29
|
+
when '.json'
|
30
|
+
JSON.parse(raw, :symbolize_names => true)
|
31
|
+
else
|
32
|
+
raw
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module DeckTests
|
38
|
+
def test_suits
|
39
|
+
assert_equal(fixture[:suits], @deck.class.suits)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_trumps
|
43
|
+
assert_equal(fixture[:trumps], @deck.class.trumps)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_ranks
|
47
|
+
assert_equal(fixture[:ranks], @deck.class.ranks)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cardgame
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nicholas E. Rabenau
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: &2152230380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.2'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2152230380
|
25
|
+
description: This gem is a small exercise in OO modeling. It models card games, especially
|
26
|
+
Uno.
|
27
|
+
email:
|
28
|
+
- nerab@gmx.net
|
29
|
+
executables:
|
30
|
+
- game
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- Gemfile
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- TODO.md
|
39
|
+
- bin/game
|
40
|
+
- cardgame.gemspec
|
41
|
+
- lib/cardgame.rb
|
42
|
+
- lib/cardgame/card.rb
|
43
|
+
- lib/cardgame/deck.rb
|
44
|
+
- lib/cardgame/french.rb
|
45
|
+
- lib/cardgame/player.rb
|
46
|
+
- lib/cardgame/uno.rb
|
47
|
+
- lib/cardgame/version.rb
|
48
|
+
- test/fixtures/french.json
|
49
|
+
- test/fixtures/uno.yaml
|
50
|
+
- test/helper.rb
|
51
|
+
- test/unit/test_card.rb
|
52
|
+
- test/unit/test_french.rb
|
53
|
+
- test/unit/test_uno.rb
|
54
|
+
homepage: ''
|
55
|
+
licenses: []
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project: cardgame
|
74
|
+
rubygems_version: 1.8.15
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: Models card games, e.g. Uno
|
78
|
+
test_files:
|
79
|
+
- test/fixtures/french.json
|
80
|
+
- test/fixtures/uno.yaml
|
81
|
+
- test/helper.rb
|
82
|
+
- test/unit/test_card.rb
|
83
|
+
- test/unit/test_french.rb
|
84
|
+
- test/unit/test_uno.rb
|