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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +29 -0
  5. data/Rakefile +8 -0
  6. data/lib/tundengine.rb +67 -0
  7. data/lib/tundengine/algebraic_data_type.rb +33 -0
  8. data/lib/tundengine/card_percolator.rb +9 -0
  9. data/lib/tundengine/cards/card.rb +63 -0
  10. data/lib/tundengine/cards/null.rb +18 -0
  11. data/lib/tundengine/dealer.rb +52 -0
  12. data/lib/tundengine/deck.rb +41 -0
  13. data/lib/tundengine/declarations/base.rb +24 -0
  14. data/lib/tundengine/declarations/las_cuarenta.rb +15 -0
  15. data/lib/tundengine/declarations/las_veinte.rb +29 -0
  16. data/lib/tundengine/declarations/null.rb +18 -0
  17. data/lib/tundengine/declarations/tute.rb +23 -0
  18. data/lib/tundengine/declarations/void.rb +15 -0
  19. data/lib/tundengine/hand.rb +35 -0
  20. data/lib/tundengine/move.rb +13 -0
  21. data/lib/tundengine/null_move.rb +13 -0
  22. data/lib/tundengine/player/in_match.rb +29 -0
  23. data/lib/tundengine/player/in_round.rb +71 -0
  24. data/lib/tundengine/player/in_turn.rb +31 -0
  25. data/lib/tundengine/ranks/base.rb +31 -0
  26. data/lib/tundengine/ranks/cinco.rb +10 -0
  27. data/lib/tundengine/ranks/cuatro.rb +10 -0
  28. data/lib/tundengine/ranks/diez.rb +10 -0
  29. data/lib/tundengine/ranks/doce.rb +10 -0
  30. data/lib/tundengine/ranks/dos.rb +10 -0
  31. data/lib/tundengine/ranks/null.rb +10 -0
  32. data/lib/tundengine/ranks/once.rb +10 -0
  33. data/lib/tundengine/ranks/seis.rb +10 -0
  34. data/lib/tundengine/ranks/siete.rb +10 -0
  35. data/lib/tundengine/ranks/tres.rb +10 -0
  36. data/lib/tundengine/ranks/uno.rb +10 -0
  37. data/lib/tundengine/ranks/white.rb +9 -0
  38. data/lib/tundengine/round_analyzer.rb +67 -0
  39. data/lib/tundengine/stages/base.rb +74 -0
  40. data/lib/tundengine/stages/match.rb +80 -0
  41. data/lib/tundengine/stages/null.rb +13 -0
  42. data/lib/tundengine/stages/round.rb +85 -0
  43. data/lib/tundengine/stages/tournament.rb +72 -0
  44. data/lib/tundengine/stages/trick.rb +90 -0
  45. data/lib/tundengine/stages/turn.rb +44 -0
  46. data/lib/tundengine/strategies/automatic.rb +27 -0
  47. data/lib/tundengine/strategies/base.rb +8 -0
  48. data/lib/tundengine/strategies/manual.rb +33 -0
  49. data/lib/tundengine/strategies/random.rb +15 -0
  50. data/lib/tundengine/stringifiable_by_class.rb +9 -0
  51. data/lib/tundengine/suits/base.rb +19 -0
  52. data/lib/tundengine/suits/basto.rb +7 -0
  53. data/lib/tundengine/suits/copa.rb +7 -0
  54. data/lib/tundengine/suits/espada.rb +7 -0
  55. data/lib/tundengine/suits/null.rb +14 -0
  56. data/lib/tundengine/suits/oro.rb +7 -0
  57. data/lib/tundengine/tute_values/base.rb +17 -0
  58. data/lib/tundengine/tute_values/capotes.rb +22 -0
  59. data/lib/tundengine/tute_values/nothing.rb +13 -0
  60. data/lib/tundengine/tute_values/victory.rb +17 -0
  61. data/lib/tundengine/version.rb +17 -0
  62. data/test/test_card_strength.rb +58 -0
  63. data/test/test_helper.rb +35 -0
  64. data/test/test_playable_cards.rb +66 -0
  65. data/test/test_random_tournaments.rb +31 -0
  66. data/tundengine.gemspec +28 -0
  67. 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,15 @@
1
+ module Tundengine
2
+ module Declarations
3
+ class Void < Base
4
+
5
+ include Singleton
6
+
7
+ ROUND_POINTS = 0
8
+
9
+ def is_declarable?(hand, trump_suit)
10
+ true
11
+ end
12
+
13
+ end
14
+ end
15
+ 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,13 @@
1
+ module Tundengine
2
+ module Move
3
+
4
+ def yield_self_or_lock!(node)
5
+ yield self
6
+ end
7
+
8
+ def self_or_yield
9
+ self
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Tundengine
2
+ module NullMove
3
+
4
+ def yield_self_or_lock!(node)
5
+ node.lock!
6
+ end
7
+
8
+ def self_or_yield
9
+ yield
10
+ end
11
+
12
+ end
13
+ 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,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Cinco < Base
4
+
5
+ include White
6
+ POWER = 3
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Cuatro < Base
4
+
5
+ include White
6
+ POWER = 2
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Diez < Base
4
+
5
+ ROUND_POINTS = 2
6
+ POWER = 6
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Doce < Base
4
+
5
+ ROUND_POINTS = 4
6
+ POWER = 8
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Dos < Base
4
+
5
+ include White
6
+ POWER = 1
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Null < Base
4
+
5
+ ROUND_POINTS = :no_round_points
6
+ POWER = 0
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Once < Base
4
+
5
+ ROUND_POINTS = 3
6
+ POWER = 7
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Seis < Base
4
+
5
+ include White
6
+ POWER = 4
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Siete < Base
4
+
5
+ include White
6
+ POWER = 5
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Tres < Base
4
+
5
+ ROUND_POINTS = 10
6
+ POWER = 9
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Tundengine
2
+ module Ranks
3
+ class Uno < Base
4
+
5
+ ROUND_POINTS = 11
6
+ POWER = 10
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Tundengine
2
+ module Ranks
3
+ module White
4
+
5
+ ROUND_POINTS = 0
6
+
7
+ end
8
+ end
9
+ 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