poker-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +24 -0
- data/bin/console +9 -0
- data/lib/poker-engine.rb +1 -0
- data/lib/poker_engine.rb +9 -0
- data/lib/poker_engine/card.rb +27 -0
- data/lib/poker_engine/cards.rb +64 -0
- data/lib/poker_engine/game.rb +81 -0
- data/lib/poker_engine/hand_evaluator.rb +32 -0
- data/lib/poker_engine/hand_index.rb +36 -0
- data/lib/poker_engine/hand_levels.rb +107 -0
- data/lib/poker_engine/next_actions.rb +44 -0
- data/lib/poker_engine/reducer.rb +127 -0
- data/lib/poker_engine/state_operations.rb +66 -0
- data/lib/poker_engine/version.rb +3 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bc329a00c9421d0689395ca15d79839d8f7141f7
|
4
|
+
data.tar.gz: f5b4b1d27ee2fcc6dd929587a2336832f73c6037
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbdc36361e03f6919bdbb3cfa729131a893741942afa915a20851482c1f7ca2b44cf2dc90cc2a843f75b97b47541db732d7a8c53ac0f993bd80d963943847ffe
|
7
|
+
data.tar.gz: 52b87a7f764f916f2b0a035c72c6687212f46001c98dc8131a7b9cab4a0d0b78de53f77e9cbc6070578ee9cf71ffa9f04056841a9fd42c57a03cff577786d687
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Kamen Kanev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# poker-engine
|
2
|
+
Poker library written in Ruby :heart:
|
3
|
+
|
4
|
+

|
5
|
+
|
6
|
+
## Try it
|
7
|
+
|
8
|
+
Currently there is simple 74% working console interface `./examples/console-ui`
|
9
|
+
|
10
|
+
## TODOs
|
11
|
+
|
12
|
+
- [ ] Adding validators
|
13
|
+
|
14
|
+
Currently, the user moves are not validated and that can result in breaking the game logic.
|
15
|
+
- [ ] Rethink the concept of aggressor
|
16
|
+
- [ ] Plug-in the hand evaluation logic in to the game cycle
|
17
|
+
|
18
|
+
## Test~~s~~
|
19
|
+
|
20
|
+
Run `rspec`
|
21
|
+
|
22
|
+
## Resources
|
23
|
+
|
24
|
+
- https://www.pokernews.com/pokerterms
|
data/bin/console
ADDED
data/lib/poker-engine.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'poker_engine'
|
data/lib/poker_engine.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'hamster'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'poker_engine/game'
|
4
|
+
require 'poker_engine/next_actions'
|
5
|
+
require 'poker_engine/state_operations'
|
6
|
+
require 'poker_engine/card'
|
7
|
+
require 'poker_engine/cards'
|
8
|
+
require 'poker_engine/hand_evaluator'
|
9
|
+
require 'poker_engine/version'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PokerEngine
|
2
|
+
class Card
|
3
|
+
COLORS = { spade: :spade, club: :club, heart: :heart, diamond: :diamond }.freeze
|
4
|
+
RANKS = (2..10).to_a.map(&:to_s) + %w(J Q K A)
|
5
|
+
|
6
|
+
def self.french_deck
|
7
|
+
Card::RANKS
|
8
|
+
.product(Card::COLORS.values)
|
9
|
+
.map { |rank, color| Card.new rank, color }
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :rank, :color
|
13
|
+
|
14
|
+
def initialize(rank, color)
|
15
|
+
@rank = rank
|
16
|
+
@color = color
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
@value ||= RANKS.index rank
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{@rank}#{@color[0]}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module PokerEngine
|
2
|
+
class Cards
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
COLOR_BY_FIRST_LETTER = {
|
6
|
+
's' => Card::COLORS[:spade],
|
7
|
+
'c' => Card::COLORS[:club],
|
8
|
+
'h' => Card::COLORS[:heart],
|
9
|
+
'd' => Card::COLORS[:diamond],
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def self.parse(str_cards)
|
13
|
+
cards = str_cards
|
14
|
+
.split(',')
|
15
|
+
.map do |str|
|
16
|
+
Card.new str.to_i, COLOR_BY_FIRST_LETTER.fetch(str[-1])
|
17
|
+
end
|
18
|
+
|
19
|
+
new(cards)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :cards
|
23
|
+
|
24
|
+
def initialize(cards)
|
25
|
+
@cards = cards
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
cards.map(&:to_s).join(', ')
|
30
|
+
end
|
31
|
+
|
32
|
+
def each(&block)
|
33
|
+
cards.each(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def +(other)
|
37
|
+
Cards.new(cards + other.cards)
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: Make it work with block, too
|
41
|
+
def sort
|
42
|
+
cards.sort_by(&:value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def sorted_values
|
46
|
+
cards.map(&:value).sort
|
47
|
+
end
|
48
|
+
|
49
|
+
def combination(x)
|
50
|
+
cards.combination(x).map { |c| Cards.new(c) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Make descending order primary by occurency and secondary by value
|
54
|
+
def values_desc_by_occurency
|
55
|
+
values = cards.map(&:value)
|
56
|
+
|
57
|
+
values.sort do |a, b|
|
58
|
+
coefficient_occurency = (values.count(a) <=> values.count(b))
|
59
|
+
|
60
|
+
coefficient_occurency.zero? ? -(a <=> b) : -coefficient_occurency
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'reducer'
|
2
|
+
|
3
|
+
module PokerEngine
|
4
|
+
module Game
|
5
|
+
POSITIONS_ORDER = {
|
6
|
+
preflop: %i(UTG MP CO D SB BB).freeze,
|
7
|
+
postflop: %i(SB BB UTG MP CO D).freeze,
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def start(*args, &handler)
|
13
|
+
state = initial_state(*args)
|
14
|
+
run(state, &handler)
|
15
|
+
end
|
16
|
+
|
17
|
+
def next(state, player_action, &handler)
|
18
|
+
state = Reducer.call state, player_action
|
19
|
+
|
20
|
+
run(state, &handler)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(state, &handler)
|
24
|
+
subscribed_reducer = lambda do |old_state, action|
|
25
|
+
new_state = Reducer.call old_state, action
|
26
|
+
handler&.call [old_state, action], new_state
|
27
|
+
|
28
|
+
new_state
|
29
|
+
end
|
30
|
+
|
31
|
+
loop do
|
32
|
+
break state if state[:pending_request] || state[:game_ended]
|
33
|
+
|
34
|
+
actions = NextActions.call(state)
|
35
|
+
state = actions.reduce(state, &subscribed_reducer)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# TODO: remove blinds defaults
|
40
|
+
def initial_state(players, small_blind: 10, big_blind: 20, deck_seed: 1)
|
41
|
+
reversed_position_order = POSITIONS_ORDER[:preflop].reverse
|
42
|
+
positions = players.map.with_index do |player, index|
|
43
|
+
last_index = players.count - 1
|
44
|
+
|
45
|
+
[player[:id], reversed_position_order[last_index - index]]
|
46
|
+
end.to_h
|
47
|
+
|
48
|
+
normalized_players = players.map do |id:, balance:, **|
|
49
|
+
[
|
50
|
+
id,
|
51
|
+
{
|
52
|
+
id: id,
|
53
|
+
active: true,
|
54
|
+
balance: balance,
|
55
|
+
money_in_pot: 0,
|
56
|
+
position: positions[id],
|
57
|
+
cards: [],
|
58
|
+
last_move: {},
|
59
|
+
},
|
60
|
+
]
|
61
|
+
end.to_h
|
62
|
+
|
63
|
+
Hamster.from(
|
64
|
+
players: normalized_players,
|
65
|
+
aggressor_id: nil,
|
66
|
+
board: [],
|
67
|
+
small_blind: small_blind,
|
68
|
+
big_blind: big_blind,
|
69
|
+
pot: 0,
|
70
|
+
pending_request: false,
|
71
|
+
winner_ids: [],
|
72
|
+
top_hands: {},
|
73
|
+
game_ended: false,
|
74
|
+
last_action: { type: :game_start },
|
75
|
+
current_stage: nil,
|
76
|
+
current_player_id: nil,
|
77
|
+
deck: Card.french_deck.shuffle(random: Random.new(deck_seed))
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'hand_index'
|
2
|
+
|
3
|
+
module PokerEngine
|
4
|
+
module HandEvaluator
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def find_top_hands(players, board)
|
8
|
+
players
|
9
|
+
.map do |id:, cards:, **|
|
10
|
+
cards = board + cards
|
11
|
+
player_top_hand = cards.combination(5)
|
12
|
+
.map do |five_cards|
|
13
|
+
HandIndex.new(Cards.new(five_cards))
|
14
|
+
end
|
15
|
+
.max
|
16
|
+
|
17
|
+
[id, player_top_hand]
|
18
|
+
end
|
19
|
+
.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_winners(players, board)
|
23
|
+
top_hand_per_player_id = find_top_hands(players, board)
|
24
|
+
|
25
|
+
best_hand = top_hand_per_player_id.values.max
|
26
|
+
|
27
|
+
top_hand_per_player_id
|
28
|
+
.map { |player_id, hand| hand == best_hand ? player_id : nil }
|
29
|
+
.compact
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'hand_levels'
|
2
|
+
|
3
|
+
module PokerEngine
|
4
|
+
class HandIndex
|
5
|
+
# The index is equivalent to the level strength
|
6
|
+
RANK_TABLE = [HandLevels::HighCard, HandLevels::OnePair,
|
7
|
+
HandLevels::TwoPairs, HandLevels::ThreeOfAKind,
|
8
|
+
HandLevels::Straight, HandLevels::Flush,
|
9
|
+
HandLevels::FullHouse, HandLevels::FourOfAKind,
|
10
|
+
HandLevels::StraightFlush].freeze
|
11
|
+
|
12
|
+
attr_reader :cards
|
13
|
+
|
14
|
+
def initialize(cards)
|
15
|
+
@cards = cards
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(other)
|
19
|
+
outer_level_compare =
|
20
|
+
(RANK_TABLE.index(level) <=> RANK_TABLE.index(other.level))
|
21
|
+
|
22
|
+
return outer_level_compare unless outer_level_compare.zero?
|
23
|
+
|
24
|
+
level <=> other.level
|
25
|
+
end
|
26
|
+
|
27
|
+
def >(other)
|
28
|
+
level <=> other.level
|
29
|
+
end
|
30
|
+
|
31
|
+
def level
|
32
|
+
@level ||=
|
33
|
+
RANK_TABLE.reverse_each.find { |level| level.owns?(cards) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative 'cards'
|
2
|
+
|
3
|
+
module PokerEngine
|
4
|
+
module HandLevels
|
5
|
+
# Abstract class for Hand level
|
6
|
+
class BaseLevel
|
7
|
+
attr_reader :cards
|
8
|
+
|
9
|
+
def initialize(cards)
|
10
|
+
@cards = cards
|
11
|
+
end
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
unless other.instance_of?(self.class)
|
15
|
+
fail "Can't detail detail hands of different level"
|
16
|
+
end
|
17
|
+
|
18
|
+
detail_compare(other)
|
19
|
+
end
|
20
|
+
|
21
|
+
def detail_compare(other)
|
22
|
+
cards.values_desc_by_occurency <=>
|
23
|
+
other.cards.values_desc_by_occurency
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#============================= Levels ======================================
|
28
|
+
|
29
|
+
HighCard = Class.new(BaseLevel) do
|
30
|
+
def self.owns?(_cards)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
OnePair = Class.new(BaseLevel) do
|
36
|
+
def self.owns?(cards)
|
37
|
+
cards.sorted_values
|
38
|
+
.group_by(&:itself)
|
39
|
+
.any? { |_, group| group.size == 2 }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
TwoPairs = Class.new(BaseLevel) do
|
44
|
+
def self.owns?(cards)
|
45
|
+
cards.sorted_values
|
46
|
+
.group_by(&:itself)
|
47
|
+
.select { |_, group| group.size == 2 }
|
48
|
+
.count
|
49
|
+
.eql?(2)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ThreeOfAKind = Class.new(BaseLevel) do
|
54
|
+
def self.owns?(cards)
|
55
|
+
cards.sorted_values
|
56
|
+
.group_by(&:itself)
|
57
|
+
.one? { |_, group| group.size == 3 }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Straight = Class.new(BaseLevel) do
|
62
|
+
def self.owns?(cards)
|
63
|
+
cards.sorted_values
|
64
|
+
.each_cons(2)
|
65
|
+
.map { |a, b| a - b }
|
66
|
+
.uniq
|
67
|
+
.one?
|
68
|
+
end
|
69
|
+
|
70
|
+
def detail_compare(other)
|
71
|
+
cards.sorted_values.first <=> other.cards.sorted_values.first
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
Flush = Class.new(BaseLevel) do
|
76
|
+
def self.owns?(cards)
|
77
|
+
cards.map(&:color).uniq.one?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
FullHouse = Class.new(BaseLevel) do
|
82
|
+
def self.owns?(cards)
|
83
|
+
cards.map(&:value).uniq.count > 1 &&
|
84
|
+
OnePair.owns?(cards) &&
|
85
|
+
ThreeOfAKind.owns?(cards)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
FourOfAKind = Class.new(BaseLevel) do
|
90
|
+
def self.owns?(cards)
|
91
|
+
cards.sorted_values
|
92
|
+
.group_by(&:itself)
|
93
|
+
.one? { |_, group| group.size == 4 }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
StraightFlush = Class.new(BaseLevel) do
|
98
|
+
def self.owns?(cards)
|
99
|
+
Straight.owns?(cards) && Flush.owns?(cards)
|
100
|
+
end
|
101
|
+
|
102
|
+
def detail_compare(other)
|
103
|
+
cards.sorted_values.first <=> other.cards.sorted_values.first
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module PokerEngine
|
2
|
+
module NextActions
|
3
|
+
def self.call(state)
|
4
|
+
state_operations = StateOperations.new state
|
5
|
+
|
6
|
+
case state.dig :last_action, :type
|
7
|
+
when :game_start
|
8
|
+
[
|
9
|
+
{ type: :next_stage, stage: :preflop },
|
10
|
+
{ type: :take_small_blind, player_id: state_operations.player_id_by(position: :SB) },
|
11
|
+
{ type: :take_big_blind, player_id: state_operations.player_id_by(position: :BB) },
|
12
|
+
]
|
13
|
+
when :take_big_blind
|
14
|
+
state_operations.ordered_player_ids.map do |id|
|
15
|
+
{ type: :distribute_to_player, player_id: id }
|
16
|
+
end
|
17
|
+
when :distribute_to_player, :distribute_to_board
|
18
|
+
[{ type: :move_request, player_id: state_operations.first_player_id }]
|
19
|
+
when :check, :call, :raise, :fold
|
20
|
+
player_id = state_operations.next_player_id
|
21
|
+
|
22
|
+
if player_id == state[:current_player_id]
|
23
|
+
[{ type: :game_end, winner_ids: [player_id] }]
|
24
|
+
elsif player_id == state[:aggressor_id] && !state_operations.next_stage?
|
25
|
+
players = Hamster.to_ruby state[:players].values
|
26
|
+
|
27
|
+
[{
|
28
|
+
type: :game_end,
|
29
|
+
top_hands: HandEvaluator.find_top_hands(players, state[:board].to_a),
|
30
|
+
winner_ids: HandEvaluator.find_winners(players, state[:board].to_a),
|
31
|
+
}]
|
32
|
+
elsif player_id == state[:aggressor_id]
|
33
|
+
[{ type: :next_stage, stage: state_operations.next_stage }]
|
34
|
+
else
|
35
|
+
[{ type: :move_request, player_id: player_id }]
|
36
|
+
end
|
37
|
+
when :next_stage
|
38
|
+
[{ type: :distribute_to_board, cards_count: state_operations.stage_cards_count }]
|
39
|
+
else
|
40
|
+
raise 'error'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module PokerEngine
|
4
|
+
module Reducer
|
5
|
+
def self.call(state, action)
|
6
|
+
raise "Unknown action #{action[:type]}" unless allowed_actions.include? action[:type]
|
7
|
+
|
8
|
+
Actions
|
9
|
+
.public_send(action[:type], state, action)
|
10
|
+
.put(:last_action, action)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.allowed_actions
|
14
|
+
@allowed_actions ||= Actions.methods(false)
|
15
|
+
end
|
16
|
+
|
17
|
+
module Actions
|
18
|
+
module_function
|
19
|
+
|
20
|
+
def game_start(state, **_action)
|
21
|
+
state
|
22
|
+
end
|
23
|
+
|
24
|
+
def take_small_blind(state, **action)
|
25
|
+
take_blind state, action.merge(blind_kind: :small_blind)
|
26
|
+
end
|
27
|
+
|
28
|
+
def take_big_blind(state, **action)
|
29
|
+
take_blind state, action.merge(blind_kind: :big_blind)
|
30
|
+
end
|
31
|
+
|
32
|
+
def distribute_to_player(state, player_id:, **_action)
|
33
|
+
new_deck, poped_cards =
|
34
|
+
state[:deck].partition.with_index { |_, i| i + 1 <= state[:deck].count - 2 }
|
35
|
+
|
36
|
+
state
|
37
|
+
.put(:deck, new_deck)
|
38
|
+
.update_in(:players, player_id, :cards) { poped_cards }
|
39
|
+
end
|
40
|
+
|
41
|
+
def distribute_to_board(state, cards_count:, **_action)
|
42
|
+
new_deck, poped_cards =
|
43
|
+
state[:deck].partition.with_index { |_, i| i + 1 <= state[:deck].count - cards_count }
|
44
|
+
|
45
|
+
state
|
46
|
+
.put(:deck, new_deck)
|
47
|
+
.put(:board) { |board| board + poped_cards }
|
48
|
+
end
|
49
|
+
|
50
|
+
def move_request(state, player_id:, **_action)
|
51
|
+
state
|
52
|
+
.put(:current_player_id, player_id)
|
53
|
+
.put(:pending_request, true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(state, **action)
|
57
|
+
bet = state.dig(:players, state[:aggressor_id], :last_move, :bet) || state[:big_blind]
|
58
|
+
money_to_give = bet - state.dig(:players, action[:player_id], :money_in_pot)
|
59
|
+
|
60
|
+
pay(state, action.merge(money_to_give: money_to_give))
|
61
|
+
.put(:aggressor_id) { |id| id || action[:player_id] } # HACK: try to get rid of it.
|
62
|
+
end
|
63
|
+
|
64
|
+
def raise(state, **action)
|
65
|
+
money_to_give = action[:bet] - state.dig(:players, action[:player_id], :money_in_pot)
|
66
|
+
|
67
|
+
pay(state, action.merge(money_to_give: money_to_give))
|
68
|
+
.put(:aggressor_id, action[:player_id])
|
69
|
+
end
|
70
|
+
|
71
|
+
def check(state, player_id:, **_action)
|
72
|
+
state
|
73
|
+
.put(:pending_request, false)
|
74
|
+
.put(:aggressor_id) { |id| id || player_id } # HACK: try to get rid of it.
|
75
|
+
end
|
76
|
+
|
77
|
+
def fold(state, **action)
|
78
|
+
state
|
79
|
+
.update_in(:players, action[:player_id]) do |player|
|
80
|
+
player.put(:active, false).put(:last_move, action)
|
81
|
+
end
|
82
|
+
.put(:pending_request, false)
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_stage(state, stage:, **_action)
|
86
|
+
state
|
87
|
+
.update_in(:players) do |players|
|
88
|
+
players.map { |id, player| [id, player.put(:money_in_pot, 0)] }
|
89
|
+
end
|
90
|
+
.put(:current_stage, stage)
|
91
|
+
.put(:aggressor_id, nil)
|
92
|
+
end
|
93
|
+
|
94
|
+
def game_end(state, top_hands:, winner_ids:, **_action)
|
95
|
+
state
|
96
|
+
.put(:top_hands, top_hands)
|
97
|
+
.put(:winner_ids, winner_ids)
|
98
|
+
.put(:game_ended, true)
|
99
|
+
end
|
100
|
+
|
101
|
+
private_class_method def take_blind(state, player_id:, blind_kind:, **_action)
|
102
|
+
blind_size = state.fetch blind_kind
|
103
|
+
|
104
|
+
state
|
105
|
+
.update_in(:players, player_id) do |player|
|
106
|
+
player
|
107
|
+
.put(:balance) { |b| b - blind_size }
|
108
|
+
.put(:money_in_pot) { |mip| mip + blind_size }
|
109
|
+
end
|
110
|
+
.put(:pot) { |pot| pot + blind_size }
|
111
|
+
end
|
112
|
+
|
113
|
+
# TODO: handle the case when the bet is higher then the current balance
|
114
|
+
private_class_method def pay(state, money_to_give:, **action)
|
115
|
+
state
|
116
|
+
.update_in(:players, action[:player_id]) do |player|
|
117
|
+
player
|
118
|
+
.put(:balance) { |b| b - money_to_give }
|
119
|
+
.put(:money_in_pot) { |mip| mip + money_to_give }
|
120
|
+
.put(:last_move, action)
|
121
|
+
end
|
122
|
+
.put(:pot) { |pot| pot + money_to_give }
|
123
|
+
.put(:pending_request, false)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module PokerEngine
|
2
|
+
class StateOperations
|
3
|
+
CARDS_COUNT_PER_STAGE_START = { flop: 3, turn: 1, river: 1 }.freeze
|
4
|
+
STAGES = %i(preflop flop turn river).freeze
|
5
|
+
|
6
|
+
attr_reader :state
|
7
|
+
|
8
|
+
def initialize(state)
|
9
|
+
@state = state
|
10
|
+
end
|
11
|
+
|
12
|
+
def player_id_by(position:)
|
13
|
+
players.find { |_id, player| player[:position] == position }.first
|
14
|
+
end
|
15
|
+
|
16
|
+
def next_stage?
|
17
|
+
state[:current_stage] != :river
|
18
|
+
end
|
19
|
+
|
20
|
+
def next_stage
|
21
|
+
STAGES.fetch STAGES.index(state[:current_stage]) + 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def stage_cards_count
|
25
|
+
StateOperations::CARDS_COUNT_PER_STAGE_START.fetch state[:current_stage]
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_player_id
|
29
|
+
ordered_player_ids.cycle.each_with_index.find do |id, order_index|
|
30
|
+
order_index > ordered_player_ids.index(state[:current_player_id]) &&
|
31
|
+
players[id][:active]
|
32
|
+
end.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def one_player_left?
|
36
|
+
players.count { |_, player| player[:active] } == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def active_players
|
40
|
+
players.select { |_, player| player[:active] }
|
41
|
+
end
|
42
|
+
|
43
|
+
NO_ACTIVE_FILTER = Object.new
|
44
|
+
def ordered_player_ids(active: NO_ACTIVE_FILTER)
|
45
|
+
raise 'Unexpected state' unless state[:current_stage]
|
46
|
+
|
47
|
+
positions = Game::POSITIONS_ORDER[state[:current_stage] == :preflop ? :preflop : :postflop]
|
48
|
+
|
49
|
+
players.each_with_object(Array.new(players.count)) do |(id, player), ordered|
|
50
|
+
next if active != NO_ACTIVE_FILTER && active != player[:active]
|
51
|
+
|
52
|
+
ordered[positions.index player[:position]] = id
|
53
|
+
end.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
def first_player_id
|
57
|
+
ordered_player_ids(active: true).first
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def players
|
63
|
+
state[:players]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: poker-engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kamen Kanev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hamster
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.48'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.48'
|
97
|
+
description: " Poker library introducing the game logic with a simple interface.
|
98
|
+
Currently offering only 6-max Holdem games.\n"
|
99
|
+
email: kamen.e.kanev@gmail.com
|
100
|
+
executables:
|
101
|
+
- console
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- LICENSE
|
106
|
+
- README.md
|
107
|
+
- bin/console
|
108
|
+
- lib/poker-engine.rb
|
109
|
+
- lib/poker_engine.rb
|
110
|
+
- lib/poker_engine/card.rb
|
111
|
+
- lib/poker_engine/cards.rb
|
112
|
+
- lib/poker_engine/game.rb
|
113
|
+
- lib/poker_engine/hand_evaluator.rb
|
114
|
+
- lib/poker_engine/hand_index.rb
|
115
|
+
- lib/poker_engine/hand_levels.rb
|
116
|
+
- lib/poker_engine/next_actions.rb
|
117
|
+
- lib/poker_engine/reducer.rb
|
118
|
+
- lib/poker_engine/state_operations.rb
|
119
|
+
- lib/poker_engine/version.rb
|
120
|
+
homepage: https://github.com/kekanev/poker-engine
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata:
|
124
|
+
github: https://github.com/kekanev/poker-engine
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.5.2
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Poker library introducing the game logic into a simple interface.
|
145
|
+
test_files: []
|