ventiuna 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/img/bender.jpg +0 -0
- data/lib/ventiuna.rb +18 -0
- data/lib/ventiuna/card.rb +35 -0
- data/lib/ventiuna/database/connection.rb +9 -0
- data/lib/ventiuna/database/schema.rb +23 -0
- data/lib/ventiuna/database/ventiuna_strategy.db +0 -0
- data/lib/ventiuna/dealer.rb +14 -0
- data/lib/ventiuna/deck.rb +40 -0
- data/lib/ventiuna/game.rb +273 -0
- data/lib/ventiuna/hand.rb +69 -0
- data/lib/ventiuna/player.rb +127 -0
- data/lib/ventiuna/shoe.rb +19 -0
- data/lib/ventiuna/strategies/move.rb +32 -0
- data/lib/ventiuna/strategies/strategy.rb +28 -0
- data/lib/ventiuna/version.rb +3 -0
- data/test/test_card.rb +22 -0
- data/test/test_dealer.rb +153 -0
- data/test/test_deck.rb +39 -0
- data/test/test_game.rb +9 -0
- data/test/test_hand.rb +199 -0
- data/test/test_helper.rb +2 -0
- data/test/test_move.rb +13 -0
- data/test/test_player.rb +38 -0
- data/test/test_shoe.rb +54 -0
- data/test/test_strategy.rb +20 -0
- data/ventiuna.gemspec +26 -0
- metadata +141 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module Ventiuna
|
2
|
+
class Hand
|
3
|
+
attr_accessor :cards
|
4
|
+
|
5
|
+
def initialize(card=[], *other_cards)
|
6
|
+
@cards = Array(card) + other_cards
|
7
|
+
@soft = false
|
8
|
+
@active = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def << (another_card)
|
12
|
+
self.cards << another_card
|
13
|
+
end
|
14
|
+
alias :hit :<<
|
15
|
+
|
16
|
+
def stand
|
17
|
+
@active = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def split
|
21
|
+
return false unless self.pair?
|
22
|
+
[Ventiuna::Hand.new(self.cards.first), Ventiuna::Hand.new(self.cards.last)]
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
sum = 0
|
27
|
+
self.cards.sort.each_with_index do |card, i|
|
28
|
+
# hit low if haven't reached 11 or there are more cards left
|
29
|
+
hit_low = (sum >= 11) || (i+1 != self.cards.size)
|
30
|
+
@soft = card.ace? && !hit_low
|
31
|
+
sum += card.value(low: hit_low)
|
32
|
+
end
|
33
|
+
sum
|
34
|
+
end
|
35
|
+
|
36
|
+
def active?
|
37
|
+
@active
|
38
|
+
end
|
39
|
+
|
40
|
+
def blackjack?
|
41
|
+
self.value == 21 && self.cards.size == 2
|
42
|
+
end
|
43
|
+
|
44
|
+
def bust?
|
45
|
+
self.value > 21 #&& !self.soft?
|
46
|
+
end
|
47
|
+
|
48
|
+
def pair?
|
49
|
+
self.cards.size == 2 && (self.cards.first.value == self.cards.last.value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def soft?
|
53
|
+
self.value
|
54
|
+
@soft
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
"#{self.cards.collect{|c| c.to_s}} = #{self.value}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_db
|
62
|
+
s = "#{value}"
|
63
|
+
s += "I" if cards.size == 2
|
64
|
+
s += "S" if soft?
|
65
|
+
s += "P" if pair?
|
66
|
+
s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Ventiuna
|
2
|
+
class Player
|
3
|
+
attr_accessor :name, :balance, :hands, :bet, :wins, :loses, :ties
|
4
|
+
attr_reader :decisions
|
5
|
+
|
6
|
+
def initialize(name, strategy: "Learning", balance: 1000000.0)
|
7
|
+
@name = name
|
8
|
+
@strategy = Ventiuna::Strategy.find_or_create_by_name(strategy)
|
9
|
+
@balance = balance
|
10
|
+
@hands = [Ventiuna::Hand.new]
|
11
|
+
@bet = 0
|
12
|
+
@wins = 0
|
13
|
+
@loses = 0
|
14
|
+
@ties = 0
|
15
|
+
@decisions = Hash.new([])
|
16
|
+
end
|
17
|
+
|
18
|
+
def stats
|
19
|
+
"Wins: #{self.wins} (#{self.wins_percent.round}%), Loses: #{self.loses} (#{self.loses_percent.round}%), Ties: #{self.ties} (#{self.ties_percent.round}%)"
|
20
|
+
end
|
21
|
+
|
22
|
+
def wins_percent
|
23
|
+
return 0 if total_rounds == 0
|
24
|
+
(self.wins.to_f/self.total_rounds) * 100
|
25
|
+
end
|
26
|
+
|
27
|
+
def loses_percent
|
28
|
+
return 0 if total_rounds == 0
|
29
|
+
(self.loses.to_f/self.total_rounds) * 100
|
30
|
+
end
|
31
|
+
|
32
|
+
def ties_percent
|
33
|
+
return 0 if total_rounds == 0
|
34
|
+
(self.ties.to_f/self.total_rounds) * 100
|
35
|
+
end
|
36
|
+
|
37
|
+
def total_rounds
|
38
|
+
@wins+@loses+@ties
|
39
|
+
end
|
40
|
+
|
41
|
+
def place_bet(min_bet, max_bet)
|
42
|
+
@bet = min_bet
|
43
|
+
@balance -= @bet
|
44
|
+
end
|
45
|
+
|
46
|
+
def active_hand
|
47
|
+
hands.select{ |h| h.active? }.first
|
48
|
+
end
|
49
|
+
alias :hand :active_hand
|
50
|
+
|
51
|
+
def reset_hands
|
52
|
+
@hands = [Ventiuna::Hand.new]
|
53
|
+
end
|
54
|
+
alias :reset_hand :reset_hands
|
55
|
+
|
56
|
+
def decision(dealers_card, strategy: "interactive")
|
57
|
+
define_posible_moves(dealers_card)
|
58
|
+
can_split = hands.size == 1
|
59
|
+
#d = @strategy.decision(dealers_card, hand, can_split)
|
60
|
+
d = nil
|
61
|
+
|
62
|
+
unless d
|
63
|
+
d = "s"
|
64
|
+
case strategy
|
65
|
+
when "random"
|
66
|
+
d = random_strategy
|
67
|
+
when "interactive"
|
68
|
+
d = interactive
|
69
|
+
end
|
70
|
+
end
|
71
|
+
move = Ventiuna::Move.find_by_strategy_id_and_dealers_card_value_and_hand_and_decision(@strategy.id, dealers_card.value, hand.to_db, d)
|
72
|
+
#puts move.inspect
|
73
|
+
@decisions[hand] = decisions[hand] << move
|
74
|
+
#puts @decisions
|
75
|
+
d
|
76
|
+
end
|
77
|
+
|
78
|
+
def reset_decisions
|
79
|
+
@decisions = Hash.new([])
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def posible_moves(dealers_card)
|
84
|
+
moves_list = ["s"]
|
85
|
+
return moves_list if active_hand.bust?
|
86
|
+
|
87
|
+
moves_list.unshift("h") if active_hand.soft? || active_hand.value < 21
|
88
|
+
moves_list.unshift("sp") if active_hand.pair?
|
89
|
+
moves_list.unshift("d") if active_hand.cards.size == 2
|
90
|
+
|
91
|
+
moves_list
|
92
|
+
end
|
93
|
+
|
94
|
+
def define_posible_moves(dealers_card)
|
95
|
+
posible_moves(dealers_card).each do |m|
|
96
|
+
Ventiuna::Move.find_or_create_by_strategy_id_and_dealers_card_value_and_hand_and_decision(@strategy.id, dealers_card.value, hand.to_db, m).save
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def random_strategy
|
101
|
+
return "s" if self.active_hand.bust?
|
102
|
+
return "s" if self.active_hand.value == 21 || self.active_hand.blackjack?
|
103
|
+
return ["s", "h", "d", "sp"].shuffle.pop if self.active_hand.pair?
|
104
|
+
return ["s", "h", "d"].shuffle.pop if self.active_hand.cards.size == 2
|
105
|
+
["s", "h"].shuffle.pop
|
106
|
+
end
|
107
|
+
|
108
|
+
def interactive
|
109
|
+
choice = $stdin.gets.to_s.strip
|
110
|
+
|
111
|
+
case choice
|
112
|
+
when /^hit$|^h$/i
|
113
|
+
"h"
|
114
|
+
when /^stand$|^s$/i
|
115
|
+
"s"
|
116
|
+
when /^double$|^d$/i
|
117
|
+
"d"
|
118
|
+
when /^split$|^sp$/i
|
119
|
+
"sp"
|
120
|
+
else
|
121
|
+
puts "invalid choice!"
|
122
|
+
interactive()
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ventiuna
|
2
|
+
class Shoe < Ventiuna::Deck
|
3
|
+
|
4
|
+
def initialize(deck_count=8)
|
5
|
+
@cards = []
|
6
|
+
suits = @@suits * deck_count
|
7
|
+
suits.each do |suit|
|
8
|
+
@@ranks.each do |rank|
|
9
|
+
@cards << Ventiuna::Card.new(rank: rank, suit: suit)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
@initial_size = @cards.size
|
13
|
+
end
|
14
|
+
|
15
|
+
def needs_shuffling?
|
16
|
+
!self.shuffled? || (self.size.to_f / @initial_size) <= 0.25
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ventiuna
|
2
|
+
class Move < ActiveRecord::Base
|
3
|
+
belongs_to :strategy
|
4
|
+
|
5
|
+
#validates_presence_of :dealers_card, :cards, :hand_value, :decision
|
6
|
+
validates_uniqueness_of :decision, :scope => [:strategy_id, :dealers_card_value, :hand]
|
7
|
+
|
8
|
+
def score
|
9
|
+
#win_count*2 + tie_count
|
10
|
+
(win + tie + progress) - (lose + bust)
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_counts(outcome)
|
14
|
+
value = 1
|
15
|
+
value = 2 if decision == "d"
|
16
|
+
case outcome
|
17
|
+
when "blackjack", "win"
|
18
|
+
self.win += value
|
19
|
+
when "tie"
|
20
|
+
self.tie += value
|
21
|
+
when "bust"
|
22
|
+
self.lose += value
|
23
|
+
when "lose"
|
24
|
+
self.lose += value
|
25
|
+
when "progress"
|
26
|
+
|
27
|
+
self.progress += 1
|
28
|
+
end
|
29
|
+
self.save
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Ventiuna
|
2
|
+
class Strategy < ActiveRecord::Base
|
3
|
+
has_many :moves
|
4
|
+
|
5
|
+
validates_uniqueness_of :name
|
6
|
+
|
7
|
+
def decision(dealers_card, players_hand, can_split)
|
8
|
+
best_score(dealers_card, players_hand, can_split)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def best_score(dealers_card, players_hand, can_split)
|
13
|
+
r = nil
|
14
|
+
best_move = nil
|
15
|
+
past_moves = self.moves.where(:dealers_card_value => dealers_card.value, :hand => players_hand.to_db)
|
16
|
+
#puts past_moves.inspect
|
17
|
+
#past_decisions = past_moves.collect{ |m| m.decision }
|
18
|
+
#return nil unless past_decisions.include?("h") && past_decisions.include?("s")
|
19
|
+
past_moves.each do |m|
|
20
|
+
best_move = m unless best_move
|
21
|
+
next if m.decision == "sp" && !can_split # can't split here
|
22
|
+
best_move = m if m.score > best_move.score
|
23
|
+
end
|
24
|
+
r = best_move.decision if best_move
|
25
|
+
r
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/test/test_card.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestCard < Test::Unit::TestCase
|
4
|
+
def test_init
|
5
|
+
duce_of_clubs = Ventiuna::Card.new(rank: "2", suit: "club")
|
6
|
+
assert_equal("2", duce_of_clubs.rank)
|
7
|
+
assert_equal("club", duce_of_clubs.suit)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_value
|
11
|
+
duce_of_clubs = Ventiuna::Card.new(rank: "2", suit: "club")
|
12
|
+
assert_equal(2, duce_of_clubs.value)
|
13
|
+
|
14
|
+
king_of_hearts = Ventiuna::Card.new(rank: "K", suit: "heart")
|
15
|
+
assert_equal(10, king_of_hearts.value)
|
16
|
+
|
17
|
+
ace_of_spades = Ventiuna::Card.new(rank: "A", suit: "spade")
|
18
|
+
assert_equal(11, ace_of_spades.value)
|
19
|
+
assert_equal(11, ace_of_spades.value(high: true))
|
20
|
+
assert_equal(1, ace_of_spades.value(low: true))
|
21
|
+
end
|
22
|
+
end
|
data/test/test_dealer.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TestDealer < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@dealer = Ventiuna::Dealer.new("TestDealer")
|
6
|
+
|
7
|
+
@duce = Ventiuna::Card.new(rank: "2", suit: "club")
|
8
|
+
@tres = Ventiuna::Card.new(rank: "3", suit: "diamond")
|
9
|
+
@quatro = Ventiuna::Card.new(rank: "4", suit: "heart")
|
10
|
+
@cinco = Ventiuna::Card.new(rank: "5", suit: "diamond")
|
11
|
+
@seis = Ventiuna::Card.new(rank: "6", suit: "heart")
|
12
|
+
|
13
|
+
@seven_of_diamonds = Ventiuna::Card.new(rank: "7", suit: "diamond")
|
14
|
+
@eight_of_spades = Ventiuna::Card.new(rank: "8", suit: "spade")
|
15
|
+
@eight_of_clubs = Ventiuna::Card.new(rank: "8", suit: "club")
|
16
|
+
@nine_of_spades = Ventiuna::Card.new(rank: "9", suit: "spade")
|
17
|
+
@ten_of_clubs = Ventiuna::Card.new(rank: "10", suit: "club")
|
18
|
+
|
19
|
+
@jack_of_clubs = Ventiuna::Card.new(rank: "J", suit: "club")
|
20
|
+
@king_of_hearts = Ventiuna::Card.new(rank: "K", suit: "heart")
|
21
|
+
@ace_of_diamonds = Ventiuna::Card.new(rank: "A", suit: "diamond")
|
22
|
+
@ace_of_spades = Ventiuna::Card.new(rank: "A", suit: "spade")
|
23
|
+
@ace_of_clubs = Ventiuna::Card.new(rank: "A", suit: "club")
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_hit_up_to_17
|
27
|
+
@dealer.hand << @duce
|
28
|
+
@dealer.hand << @duce
|
29
|
+
assert_equal("h", @dealer.decision)
|
30
|
+
@dealer.reset_hand
|
31
|
+
|
32
|
+
@dealer.hand << @duce
|
33
|
+
@dealer.hand << @tres
|
34
|
+
assert_equal("h", @dealer.decision)
|
35
|
+
@dealer.reset_hand
|
36
|
+
|
37
|
+
@dealer.hand << @duce
|
38
|
+
@dealer.hand << @quatro
|
39
|
+
assert_equal("h", @dealer.decision)
|
40
|
+
@dealer.reset_hand
|
41
|
+
|
42
|
+
@dealer.hand << @duce
|
43
|
+
@dealer.hand << @cinco
|
44
|
+
assert_equal("h", @dealer.decision)
|
45
|
+
@dealer.reset_hand
|
46
|
+
|
47
|
+
@dealer.hand << @duce
|
48
|
+
@dealer.hand << @seis
|
49
|
+
assert_equal("h", @dealer.decision)
|
50
|
+
@dealer.reset_hand
|
51
|
+
|
52
|
+
@dealer.hand << @duce
|
53
|
+
@dealer.hand << @seven_of_diamonds
|
54
|
+
assert_equal("h", @dealer.decision)
|
55
|
+
@dealer.reset_hand
|
56
|
+
|
57
|
+
@dealer.hand << @duce
|
58
|
+
@dealer.hand << @eight_of_clubs
|
59
|
+
assert_equal("h", @dealer.decision)
|
60
|
+
@dealer.reset_hand
|
61
|
+
|
62
|
+
@dealer.hand << @duce
|
63
|
+
@dealer.hand << @nine_of_spades
|
64
|
+
assert_equal("h", @dealer.decision)
|
65
|
+
@dealer.reset_hand
|
66
|
+
|
67
|
+
@dealer.hand << @duce
|
68
|
+
@dealer.hand << @ten_of_clubs
|
69
|
+
assert_equal("h", @dealer.decision)
|
70
|
+
@dealer.reset_hand
|
71
|
+
|
72
|
+
@dealer.hand << @tres
|
73
|
+
@dealer.hand << @ten_of_clubs
|
74
|
+
assert_equal("h", @dealer.decision)
|
75
|
+
@dealer.reset_hand
|
76
|
+
|
77
|
+
@dealer.hand << @quatro
|
78
|
+
@dealer.hand << @ten_of_clubs
|
79
|
+
assert_equal("h", @dealer.decision)
|
80
|
+
@dealer.reset_hand
|
81
|
+
|
82
|
+
@dealer.hand << @cinco
|
83
|
+
@dealer.hand << @ten_of_clubs
|
84
|
+
assert_equal("h", @dealer.decision)
|
85
|
+
@dealer.reset_hand
|
86
|
+
|
87
|
+
@dealer.hand << @seis
|
88
|
+
@dealer.hand << @ten_of_clubs
|
89
|
+
assert_equal("h", @dealer.decision)
|
90
|
+
@dealer.reset_hand
|
91
|
+
|
92
|
+
@dealer.hand << @duce
|
93
|
+
assert_equal("h", @dealer.decision)
|
94
|
+
@dealer.hand << @tres
|
95
|
+
assert_equal("h", @dealer.decision)
|
96
|
+
@dealer.hand << @quatro
|
97
|
+
assert_equal("h", @dealer.decision)
|
98
|
+
@dealer.hand << @cinco
|
99
|
+
assert_equal("h", @dealer.decision)
|
100
|
+
@dealer.hand << @ace_of_spades
|
101
|
+
assert_equal("h", @dealer.decision)
|
102
|
+
@dealer.hand << @ace_of_diamonds
|
103
|
+
assert_equal("h", @dealer.decision)
|
104
|
+
@dealer.hand << @ace_of_clubs
|
105
|
+
assert_equal("s", @dealer.decision)
|
106
|
+
@dealer.reset_hand
|
107
|
+
end
|
108
|
+
|
109
|
+
def hit_on_soft_17
|
110
|
+
@dealer.hand << @ace_of_diamonds
|
111
|
+
@dealer.hand << @seis
|
112
|
+
assert_equal("h", @dealer.decision)
|
113
|
+
@dealer.reset_hand
|
114
|
+
|
115
|
+
@dealer.hand << @ace_of_diamonds
|
116
|
+
@dealer.hand << @duce
|
117
|
+
@dealer.hand << @quatro
|
118
|
+
assert_equal("h", @dealer.decision)
|
119
|
+
@dealer.reset_hand
|
120
|
+
end
|
121
|
+
|
122
|
+
def stand_on_17_and_up
|
123
|
+
#17
|
124
|
+
@dealer.hand << @nine_of_spades
|
125
|
+
@dealer.hand << @eight_of_clubs
|
126
|
+
assert_equal("s", @dealer.decision)
|
127
|
+
@dealer.reset_hand
|
128
|
+
|
129
|
+
#18
|
130
|
+
@dealer.hand << @ace_of_diamonds
|
131
|
+
@dealer.hand << @seven_of_diamonds
|
132
|
+
assert_equal("s", @dealer.decision)
|
133
|
+
@dealer.reset_hand
|
134
|
+
|
135
|
+
#19
|
136
|
+
@dealer.hand << @ace_of_diamonds
|
137
|
+
@dealer.hand << @eight_of_clubs
|
138
|
+
assert_equal("s", @dealer.decision)
|
139
|
+
@dealer.reset_hand
|
140
|
+
|
141
|
+
#20
|
142
|
+
@dealer.hand << @ace_of_diamonds
|
143
|
+
@dealer.hand << @nine_of_spades
|
144
|
+
assert_equal("s", @dealer.decision)
|
145
|
+
@dealer.reset_hand
|
146
|
+
|
147
|
+
#21
|
148
|
+
@dealer.hand << @ace_of_diamonds
|
149
|
+
@dealer.hand << @jack_of_clubs
|
150
|
+
assert_equal("s", @dealer.decision)
|
151
|
+
@dealer.reset_hand
|
152
|
+
end
|
153
|
+
end
|