tundengine 0.0.1
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/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +29 -0
- data/Rakefile +8 -0
- data/lib/tundengine.rb +67 -0
- data/lib/tundengine/algebraic_data_type.rb +33 -0
- data/lib/tundengine/card_percolator.rb +9 -0
- data/lib/tundengine/cards/card.rb +63 -0
- data/lib/tundengine/cards/null.rb +18 -0
- data/lib/tundengine/dealer.rb +52 -0
- data/lib/tundengine/deck.rb +41 -0
- data/lib/tundengine/declarations/base.rb +24 -0
- data/lib/tundengine/declarations/las_cuarenta.rb +15 -0
- data/lib/tundengine/declarations/las_veinte.rb +29 -0
- data/lib/tundengine/declarations/null.rb +18 -0
- data/lib/tundengine/declarations/tute.rb +23 -0
- data/lib/tundengine/declarations/void.rb +15 -0
- data/lib/tundengine/hand.rb +35 -0
- data/lib/tundengine/move.rb +13 -0
- data/lib/tundengine/null_move.rb +13 -0
- data/lib/tundengine/player/in_match.rb +29 -0
- data/lib/tundengine/player/in_round.rb +71 -0
- data/lib/tundengine/player/in_turn.rb +31 -0
- data/lib/tundengine/ranks/base.rb +31 -0
- data/lib/tundengine/ranks/cinco.rb +10 -0
- data/lib/tundengine/ranks/cuatro.rb +10 -0
- data/lib/tundengine/ranks/diez.rb +10 -0
- data/lib/tundengine/ranks/doce.rb +10 -0
- data/lib/tundengine/ranks/dos.rb +10 -0
- data/lib/tundengine/ranks/null.rb +10 -0
- data/lib/tundengine/ranks/once.rb +10 -0
- data/lib/tundengine/ranks/seis.rb +10 -0
- data/lib/tundengine/ranks/siete.rb +10 -0
- data/lib/tundengine/ranks/tres.rb +10 -0
- data/lib/tundengine/ranks/uno.rb +10 -0
- data/lib/tundengine/ranks/white.rb +9 -0
- data/lib/tundengine/round_analyzer.rb +67 -0
- data/lib/tundengine/stages/base.rb +74 -0
- data/lib/tundengine/stages/match.rb +80 -0
- data/lib/tundengine/stages/null.rb +13 -0
- data/lib/tundengine/stages/round.rb +85 -0
- data/lib/tundengine/stages/tournament.rb +72 -0
- data/lib/tundengine/stages/trick.rb +90 -0
- data/lib/tundengine/stages/turn.rb +44 -0
- data/lib/tundengine/strategies/automatic.rb +27 -0
- data/lib/tundengine/strategies/base.rb +8 -0
- data/lib/tundengine/strategies/manual.rb +33 -0
- data/lib/tundengine/strategies/random.rb +15 -0
- data/lib/tundengine/stringifiable_by_class.rb +9 -0
- data/lib/tundengine/suits/base.rb +19 -0
- data/lib/tundengine/suits/basto.rb +7 -0
- data/lib/tundengine/suits/copa.rb +7 -0
- data/lib/tundengine/suits/espada.rb +7 -0
- data/lib/tundengine/suits/null.rb +14 -0
- data/lib/tundengine/suits/oro.rb +7 -0
- data/lib/tundengine/tute_values/base.rb +17 -0
- data/lib/tundengine/tute_values/capotes.rb +22 -0
- data/lib/tundengine/tute_values/nothing.rb +13 -0
- data/lib/tundengine/tute_values/victory.rb +17 -0
- data/lib/tundengine/version.rb +17 -0
- data/test/test_card_strength.rb +58 -0
- data/test/test_helper.rb +35 -0
- data/test/test_playable_cards.rb +66 -0
- data/test/test_random_tournaments.rb +31 -0
- data/tundengine.gemspec +28 -0
- metadata +154 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Tundengine
|
2
|
+
module Declarations
|
3
|
+
class Tute < Base
|
4
|
+
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
ROUND_POINTS = :no_round_points
|
8
|
+
|
9
|
+
def is_declarable?(hand, trump_suit)
|
10
|
+
[Ranks::Once, Ranks::Doce].any? { |rank| hand.has_all_of? rank.instance }
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_tute_effect?(tute_value)
|
14
|
+
tute_value.has_effect?
|
15
|
+
end
|
16
|
+
|
17
|
+
def finishes_round?(tute_value)
|
18
|
+
tute_value.finishes_round?
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Tundengine
|
2
|
+
class Hand < SimpleDelegator
|
3
|
+
|
4
|
+
# returns the playable cards and whether any of them would beat the current trick
|
5
|
+
def playable_cards(trick)
|
6
|
+
|
7
|
+
suit_followers = trick.first_suit.percolate(self)
|
8
|
+
trick_beaters = trick .percolate(self)
|
9
|
+
|
10
|
+
suit_followers_trick_beaters = suit_followers & trick_beaters
|
11
|
+
|
12
|
+
[
|
13
|
+
[suit_followers_trick_beaters, true ],
|
14
|
+
[suit_followers, false],
|
15
|
+
[trick_beaters, true ],
|
16
|
+
[self, false]
|
17
|
+
]
|
18
|
+
.find { |cards_subset, _| not cards_subset.empty? }
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_knight_and_king_of?(suit)
|
23
|
+
has_all_of?([Ranks::Once, Ranks::Doce].map(&:instance), suit)
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_all_of?(ranks = Deck::RANKS, suits = Deck::SUITS)
|
27
|
+
Deck.cards_of(Array(ranks), Array(suits)).all? { |c| self.include? c }
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"[#{map(&:to_s).join(', ')}]"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tundengine
|
2
|
+
module Player
|
3
|
+
class InMatch
|
4
|
+
|
5
|
+
include AlgebraicDataType
|
6
|
+
|
7
|
+
attr_reader :name, :strategy
|
8
|
+
|
9
|
+
def initialize(name, strategy)
|
10
|
+
@name = name
|
11
|
+
@strategy = strategy
|
12
|
+
@match_points = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@name
|
17
|
+
end
|
18
|
+
|
19
|
+
def identifier
|
20
|
+
[@name]
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_new_round
|
24
|
+
InRound.new(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Tundengine
|
2
|
+
module Player
|
3
|
+
class InRound
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@in_match, :name, :strategy
|
7
|
+
|
8
|
+
attr_reader :in_match, :hand, :declarations
|
9
|
+
attr_writer :bonus_points
|
10
|
+
attr_accessor :round
|
11
|
+
|
12
|
+
def initialize(player_in_match)
|
13
|
+
@in_match = player_in_match
|
14
|
+
@round = :no_round_set
|
15
|
+
@hand = Hand.new([])
|
16
|
+
@baza = []
|
17
|
+
@declarations = []
|
18
|
+
@round_points = 0
|
19
|
+
@bonus_points = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def take_card!(card)
|
23
|
+
hand << card
|
24
|
+
end
|
25
|
+
|
26
|
+
def declare!(declaration = Declarations::Null.instance)
|
27
|
+
strategy.declare!(self, declaration)
|
28
|
+
end
|
29
|
+
|
30
|
+
def after_declaring!(declaration)
|
31
|
+
if declaration.is_declarable?(hand, round.trump_suit)
|
32
|
+
declarations << declaration
|
33
|
+
round.after_declaring!(declaration)
|
34
|
+
else
|
35
|
+
raise "cannot declare #{declaration} after trick #{round.current_trick.summary} with hand #{hand}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_winning_trick!(trick)
|
40
|
+
take! trick
|
41
|
+
declare!
|
42
|
+
end
|
43
|
+
|
44
|
+
def take!(trick)
|
45
|
+
@baza = @baza.concat trick.cards
|
46
|
+
@round_points += trick.points
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_empty_hand?
|
50
|
+
hand.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_empty_baza?
|
54
|
+
@baza.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
def total_round_points
|
58
|
+
@round_points + @bonus_points
|
59
|
+
end
|
60
|
+
|
61
|
+
def summary
|
62
|
+
{
|
63
|
+
name: name,
|
64
|
+
hand: hand.map(&:to_s),
|
65
|
+
baza: @baza.map(&:to_s)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tundengine
|
2
|
+
module Player
|
3
|
+
class InTurn
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@in_round, :name, :strategy, :hand
|
7
|
+
|
8
|
+
attr_reader :turn, :in_round
|
9
|
+
|
10
|
+
def initialize(turn, player_in_round)
|
11
|
+
@turn = turn
|
12
|
+
@in_round = player_in_round
|
13
|
+
end
|
14
|
+
|
15
|
+
def play!(card = Cards::Null.instance)
|
16
|
+
strategy.play!(self, card)
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_playing!(card)
|
20
|
+
playable_cards, beats = hand.playable_cards(turn.trick)
|
21
|
+
if playable_cards.include? card
|
22
|
+
hand.delete card
|
23
|
+
turn.on_completed!(card, beats)
|
24
|
+
else
|
25
|
+
raise "cannot play card #{card} in trick with cards #{turn.trick.cards.map(&:to_s)} when your hand is #{@hand.map(&:to_s)}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tundengine
|
2
|
+
module Ranks
|
3
|
+
class Base
|
4
|
+
|
5
|
+
include Singleton
|
6
|
+
include StringifiableByClass
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
attr_reader :round_points, :power
|
10
|
+
|
11
|
+
def self.de(suit)
|
12
|
+
instance.de(suit)
|
13
|
+
end
|
14
|
+
|
15
|
+
def de(suit)
|
16
|
+
s = suit.is_a?(Suits::Base) ? suit : suit.instance
|
17
|
+
Cards::Card.new(self, s)
|
18
|
+
end
|
19
|
+
|
20
|
+
def <=>(other_rank)
|
21
|
+
@power <=> other_rank.power
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@round_points = self.class::ROUND_POINTS
|
26
|
+
@power = self.class::POWER
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Tundengine
|
2
|
+
class RoundAnalyzer < SimpleDelegator
|
3
|
+
|
4
|
+
def result
|
5
|
+
if last_declaration.has_tute_effect?(tute_value)
|
6
|
+
losers_in_round_by_tute = players - last_winner_player
|
7
|
+
losers_by_tute = in_match(losers_in_round_by_tute)
|
8
|
+
losers_by_tute.each_with_object({}) do |k, h|
|
9
|
+
h[k] = tute_value.points_for_loser(k, max_match_points)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
points_for_losers = 1
|
13
|
+
losers = in_match(losers_in_round)
|
14
|
+
losers.each_with_object({}) do |k, h|
|
15
|
+
h[k] = points_for_losers
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def in_match(players_in_round)
|
23
|
+
players_in_round.map(&:in_match)
|
24
|
+
end
|
25
|
+
|
26
|
+
def losers_in_round
|
27
|
+
capote_result = all_tricks_winner.map do |p|
|
28
|
+
p.made_declarations? ? [p] : players - [p]
|
29
|
+
end
|
30
|
+
|
31
|
+
capote_result.empty? ? losers_in_round_by_points : capote_result.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def all_tricks_winner
|
35
|
+
players.select { |pl| (players - [pl]).all?(&:has_empty_baza?) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def losers_in_round_by_points
|
39
|
+
scores = players.each_with_object(Hash.new(:empty_baza)) do |p, h|
|
40
|
+
h[p] = p.total_round_points unless p.has_empty_baza?
|
41
|
+
end
|
42
|
+
|
43
|
+
highest_score = scores.values.sort.last
|
44
|
+
|
45
|
+
first_place, not_first_place = scores
|
46
|
+
.partition { |_, s| s == highest_score }
|
47
|
+
.map { |pairs| Hash[pairs] }
|
48
|
+
|
49
|
+
scores_without_highest = not_first_place.values.sort
|
50
|
+
|
51
|
+
case losing_position
|
52
|
+
when :second
|
53
|
+
second_highest_score = scores_without_highest.last
|
54
|
+
in_loser_place = scores.select { |_, s| s == second_highest_score }
|
55
|
+
when :not_first_or_last
|
56
|
+
lowest_score = scores_without_highest.first
|
57
|
+
in_loser_place = scores.reject { |_, s| [highest_score, lowest_score].include? s }
|
58
|
+
else
|
59
|
+
raise "invalid match option: losing_position '#{losing_position}' (:second, :not_first_or_last)"
|
60
|
+
end
|
61
|
+
ret = [in_loser_place, first_place].map(&:keys).find { |ps| not ps.empty? }
|
62
|
+
ret = [] if ret.length == players.length
|
63
|
+
ret
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|