functional-yahtzee 0.0.3
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/.bundle/config +1 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +71 -0
- data/Guardfile +6 -0
- data/README.md +115 -0
- data/Rakefile +13 -0
- data/artifacts/score_card.jpg +0 -0
- data/bin/console +31 -0
- data/functional_yahtzee.gemspec +25 -0
- data/lib/dice.rb +19 -0
- data/lib/errors.rb +5 -0
- data/lib/game.rb +33 -0
- data/lib/probability.rb +32 -0
- data/lib/round.rb +11 -0
- data/lib/score_card.rb +59 -0
- data/lib/scoring.rb +48 -0
- data/lib/scoring/lower_card.rb +62 -0
- data/lib/scoring/upper_card.rb +34 -0
- data/lib/yahtzee.rb +13 -0
- data/test/integration/dice_test.rb +20 -0
- data/test/integration/game_test.rb +431 -0
- data/test/integration/scoring_test.rb +20 -0
- data/test/test_helper.rb +19 -0
- data/test/unit/dice_test.rb +40 -0
- data/test/unit/game_test.rb +27 -0
- data/test/unit/probability_test.rb +35 -0
- data/test/unit/score_card_test.rb +77 -0
- data/test/unit/scoring/lower_card_test.rb +71 -0
- data/test/unit/scoring/upper_card_test.rb +53 -0
- data/test/unit/scoring_test.rb +169 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ee9515ae7d6e62b2c338eaa390b462f0a00fd47f
|
4
|
+
data.tar.gz: 41387d21afb1951765a3d9c8cf5aee97ada9ec8a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 763b40ce6d6b310e281eb5b8ad5c56f50827c06048576d6313f2c3ffb836c480c5c7fa869cdb74e76b3450c1e52222c2851330ae0217c8cd9db296c8d849074b
|
7
|
+
data.tar.gz: 806e1457646e3748eb377893e0f34b55c38fecc7fa9b351aa8905e4c31efee611124ad191efa19533231116de11eecc89aa6bd62e9254a1e9ad592058f32ead2
|
data/.bundle/config
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--- {}
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.0-dev
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
functional-yahtzee (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
celluloid (0.15.2)
|
10
|
+
timers (~> 1.1.0)
|
11
|
+
coderay (1.1.0)
|
12
|
+
coveralls (0.7.0)
|
13
|
+
multi_json (~> 1.3)
|
14
|
+
rest-client
|
15
|
+
simplecov (>= 0.7)
|
16
|
+
term-ansicolor
|
17
|
+
thor
|
18
|
+
docile (1.1.0)
|
19
|
+
ffi (1.9.3)
|
20
|
+
formatador (0.2.4)
|
21
|
+
guard (2.2.3)
|
22
|
+
formatador (>= 0.2.4)
|
23
|
+
listen (~> 2.1)
|
24
|
+
lumberjack (~> 1.0)
|
25
|
+
pry (>= 0.9.12)
|
26
|
+
thor (>= 0.18.1)
|
27
|
+
guard-minitest (2.0.0)
|
28
|
+
guard (~> 2.0)
|
29
|
+
minitest (>= 3.0)
|
30
|
+
listen (2.2.0)
|
31
|
+
celluloid (>= 0.15.2)
|
32
|
+
rb-fsevent (>= 0.9.3)
|
33
|
+
rb-inotify (>= 0.9)
|
34
|
+
lockfile (2.1.0)
|
35
|
+
lumberjack (1.0.4)
|
36
|
+
method_source (0.8.2)
|
37
|
+
mime-types (2.0)
|
38
|
+
minitest (5.0.8)
|
39
|
+
multi_json (1.8.2)
|
40
|
+
pry (0.9.12.3)
|
41
|
+
coderay (~> 1.0)
|
42
|
+
method_source (~> 0.8)
|
43
|
+
slop (~> 3.4)
|
44
|
+
rake (10.1.0)
|
45
|
+
rb-fsevent (0.9.3)
|
46
|
+
rb-inotify (0.9.2)
|
47
|
+
ffi (>= 0.5.0)
|
48
|
+
rest-client (1.6.7)
|
49
|
+
mime-types (>= 1.16)
|
50
|
+
simplecov (0.8.1)
|
51
|
+
docile (~> 1.1.0)
|
52
|
+
lockfile (>= 2.1.0)
|
53
|
+
multi_json
|
54
|
+
simplecov-html (~> 0.8.0)
|
55
|
+
simplecov-html (0.8.0)
|
56
|
+
slop (3.4.7)
|
57
|
+
term-ansicolor (1.2.2)
|
58
|
+
tins (~> 0.8)
|
59
|
+
thor (0.18.1)
|
60
|
+
timers (1.1.0)
|
61
|
+
tins (0.13.1)
|
62
|
+
|
63
|
+
PLATFORMS
|
64
|
+
ruby
|
65
|
+
|
66
|
+
DEPENDENCIES
|
67
|
+
bundler (~> 1.3)
|
68
|
+
coveralls (~> 0.7)
|
69
|
+
functional-yahtzee!
|
70
|
+
guard-minitest (~> 2.0)
|
71
|
+
rake (~> 10.1)
|
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# Functional Yahztee in RUBY
|
2
|
+
|
3
|
+
The goal of this project is to build the dice game Yahtzee in a functional, concise, testable manner. The code within is
|
4
|
+
highly opinionated, and is ahallmark of my style. This is my perfect world where code and beauty collide.
|
5
|
+
|
6
|
+
If you are like me, and I know I am... you have something to learn from this code here. This is how you write clean, decoupled, testable, functional ruby code.
|
7
|
+
|
8
|
+
Expect a series of screencasts discussing the choices I have made here to get the results I ended up with.
|
9
|
+
|
10
|
+
[rubyquiz #19](http://rubyquiz.com/quiz19.html) proposed this
|
11
|
+
problem, and a very imperative solution was provided by James
|
12
|
+
Edward Gray II, one my ruby heros. I doubt he would write this the same way could he revisit the problem.
|
13
|
+
|
14
|
+
This is my contribution to the same problem, with the additional task of being as functional as possible and downright pretty code.
|
15
|
+
|
16
|
+
## Ruby is dead
|
17
|
+
|
18
|
+
1.8 was the Ruby of your Father. It was for a world with single cores and CPUs growing at a predictable rate each year. Ruby 1.8 is dead....
|
19
|
+
|
20
|
+
## Long Live Ruby
|
21
|
+
|
22
|
+
Thankfully the teams behind Rubinius, JRuby, and even MRI have been
|
23
|
+
working on this problem. I am happy to say that Ruby 2.0+ is
|
24
|
+
perfectly suited as a stepping stone to get you into the new
|
25
|
+
world, a world where CPU matters not, and cores own.
|
26
|
+
|
27
|
+
## Requirements
|
28
|
+
|
29
|
+
This is being developed in Ruby 2.1 with CI running on 2.0 and 1.9.3. My style tends to stay away from external dependencies whenever I can. (read: I am not afraid to code what I need)
|
30
|
+
|
31
|
+
I haven't gemmified this as of yet, so a clone and bundle with ruby
|
32
|
+
1.9.3+ and you can run the tests. I would love to get this running on rubinius as well.
|
33
|
+
|
34
|
+
```bash
|
35
|
+
cd <cloned_path>
|
36
|
+
rake
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
score_card = ScoreCard.new
|
43
|
+
writer = ScoreCard.persist(score_card)
|
44
|
+
|
45
|
+
Round.new do
|
46
|
+
roll1 = first_roll # => [1,2,3,5,5]
|
47
|
+
roll2 = second_roll([1,2,3,5]) # => [1,2,3,5,3]
|
48
|
+
roll3 = third_roll([1,2,3,5]) # => [1,2,3,5,4]
|
49
|
+
|
50
|
+
score_card = Scoring.score(roll3, :large_straight, &writer)
|
51
|
+
end
|
52
|
+
|
53
|
+
Round.new do
|
54
|
+
roll1 = first_roll # => [1,1,3,5,5]
|
55
|
+
roll2 = second_roll([1,1,5,5]) # => [1,1,5,5,1]
|
56
|
+
|
57
|
+
score_card = Scoring.score(roll2, :full_house, &writer)
|
58
|
+
end
|
59
|
+
|
60
|
+
# ...
|
61
|
+
|
62
|
+
upper_scores = score_card.to_hash.select do |k,_|
|
63
|
+
ScoreCard.upper_keys.include? k
|
64
|
+
end
|
65
|
+
score_card = Scoring.score_upper_total(upper_scores, &writer)
|
66
|
+
|
67
|
+
lower_scores = score_card.to_hash.select do |k,_|
|
68
|
+
ScoreCard.lower_keys.include? k
|
69
|
+
end
|
70
|
+
|
71
|
+
upper_total = score_card.upper_total
|
72
|
+
game_scores = lower_scores.merge(upper_total: upper_total)
|
73
|
+
score_card = Scoring.score_game_total(game_scores, &writer)
|
74
|
+
|
75
|
+
puts "Grand total: #{score_card.game_total}"
|
76
|
+
|
77
|
+
```
|
78
|
+
|
79
|
+
## Contributions
|
80
|
+
|
81
|
+
I am not accepting contributions at this time, this is my showcase code. This is my own highly opinionated software, inspired by all those listed in the section below *Many Thanks*.
|
82
|
+
|
83
|
+
## Many Thanks
|
84
|
+
|
85
|
+
I would like to say thank you to all the people I have sucked off of like a vampiric zombie since 2007. Some of these were from videos of talks, some were pairing sessions, some were just friends on irc who liked to bang Ruby.
|
86
|
+
|
87
|
+
* James Edward Gray II
|
88
|
+
* Josh Susser
|
89
|
+
* Ben Curtis
|
90
|
+
* Avdi Grimm
|
91
|
+
* Zed
|
92
|
+
* Gregg Pollack
|
93
|
+
* Daniel Fischer
|
94
|
+
* Jose Valim
|
95
|
+
* Aaron Patterson
|
96
|
+
* Corey Haines
|
97
|
+
* Ryan Bates
|
98
|
+
* Steve Klabnik
|
99
|
+
* Brian Guthrie
|
100
|
+
* Fabio Akita
|
101
|
+
* Rick Olsen
|
102
|
+
* Obie Fernandez
|
103
|
+
* Sandi Metz
|
104
|
+
* Phil Cohen
|
105
|
+
* Jessica Kerr
|
106
|
+
|
107
|
+
Whew.. Sorry if I missed you!
|
108
|
+
|
109
|
+
## Health
|
110
|
+
|
111
|
+
* My code can beat your code up
|
112
|
+
|
113
|
+
[](https://travis-ci.org/dreamr/functional_yahtzee)
|
115
|
+
[](https://coveralls.io/r/dreamr/functional_yahtzee?branch=master) [](https://codeclimate.com/github/dreamr/functional_yahtzee/code)
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
desc "Open a console with the basic libs loaded"
|
4
|
+
task :console do
|
5
|
+
sh "irb -rubygems -Ilib -r game.rb"
|
6
|
+
end
|
7
|
+
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
t.libs = ["lib", "test"]
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => :test
|
Binary file
|
data/bin/console
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
lib = File.expand_path("../../lib",__FILE__)
|
4
|
+
$:.unshift lib
|
5
|
+
|
6
|
+
module Yahtzee
|
7
|
+
require 'game'
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def repl
|
12
|
+
-> prompt do
|
13
|
+
print prompt
|
14
|
+
handle_input(gets.chomp!)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_input(input)
|
19
|
+
begin
|
20
|
+
result = eval(input)
|
21
|
+
rescue Exception => e
|
22
|
+
result = e.message
|
23
|
+
end
|
24
|
+
puts result.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
loop do
|
29
|
+
Yahtzee.repl[">> "]
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'yahtzee'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "functional-yahtzee"
|
8
|
+
spec.version = Yahtzee::VERSION
|
9
|
+
spec.authors = ["Dreamr"]
|
10
|
+
spec.email = ["dreamr.okelly@gmail.com"]
|
11
|
+
spec.description = %q{Functional yahtzee}
|
12
|
+
spec.summary = %q{Yahztee in functional ruby}
|
13
|
+
spec.homepage = "http://github.com/dreamr/functional_yahtzee"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.1"
|
23
|
+
spec.add_development_dependency "guard-minitest", "~> 2.0"
|
24
|
+
spec.add_development_dependency "coveralls", "~> 0.7"
|
25
|
+
end
|
data/lib/dice.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'yahtzee'
|
2
|
+
|
3
|
+
# Yahtzee::Dice is just a dice dsl for yahtzee
|
4
|
+
module Yahtzee::Dice
|
5
|
+
DIE_FACES = 6
|
6
|
+
MAX_DIE_COUNT = 5
|
7
|
+
|
8
|
+
def self.roll(num)
|
9
|
+
limit = num > MAX_DIE_COUNT ? MAX_DIE_COUNT : num
|
10
|
+
1.upto(limit).map {|die| rand(1..DIE_FACES) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.reroll(keepers)
|
14
|
+
new_roll = keepers + roll(MAX_DIE_COUNT-keepers.count)
|
15
|
+
new_roll[0..(MAX_DIE_COUNT-1)]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
data/lib/errors.rb
ADDED
data/lib/game.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'yahtzee'
|
2
|
+
require 'dice'
|
3
|
+
require 'round'
|
4
|
+
require 'scoring'
|
5
|
+
|
6
|
+
# This module is the imperative shell of sorts. It provides
|
7
|
+
# the 'DSL' of the Yahtzee library and is mostly a wrapper
|
8
|
+
# for internals, 'gluing' the application together from a
|
9
|
+
# bunch of functions into a cohesive playable game.
|
10
|
+
#
|
11
|
+
module Yahtzee
|
12
|
+
module Game
|
13
|
+
include Yahtzee::Errors
|
14
|
+
include Yahtzee::Scoring
|
15
|
+
include Yahtzee::Dice
|
16
|
+
|
17
|
+
module_function
|
18
|
+
|
19
|
+
def first_roll
|
20
|
+
Dice.roll(5)
|
21
|
+
end
|
22
|
+
|
23
|
+
def second_roll(keepers)
|
24
|
+
Dice.reroll(keepers)
|
25
|
+
end
|
26
|
+
# tricky - todo: alias_method :third_roll, :second_roll
|
27
|
+
|
28
|
+
def third_roll(keepers)
|
29
|
+
Dice.reroll(keepers)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
data/lib/probability.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'yahtzee'
|
2
|
+
require 'dice'
|
3
|
+
|
4
|
+
# I will need some kind of UI, Yahtzee::Probability aids in
|
5
|
+
# the decisions an AI will have to make based on their score
|
6
|
+
# card, the dice, and the probability vs possible score
|
7
|
+
module Yahtzee::Probability
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def small_straight(dice, rolls_left)
|
11
|
+
possibilities = dice.sort.each_cons(3).select do |a,b,c|
|
12
|
+
a+1==b && b+1==c
|
13
|
+
end.flatten
|
14
|
+
straight_possibility_from(dice, possibilities, rolls_left)
|
15
|
+
end
|
16
|
+
|
17
|
+
def large_straight(dice, rolls_left)
|
18
|
+
possibilities = dice.sort.each_cons(4).select do |a,b,c,d|
|
19
|
+
a+1==b && b+1==c && c+1==d
|
20
|
+
end.flatten
|
21
|
+
straight_possibility_from(dice, possibilities, rolls_left)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def self.straight_possibility_from(dice, possibilities, rolls_left)
|
27
|
+
dice_left = (dice-possibilities).count
|
28
|
+
possible_faces = dice_left*Yahtzee::Dice::DIE_FACES
|
29
|
+
rolls_left.to_f/possible_faces.to_f
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/round.rb
ADDED
data/lib/score_card.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'yahtzee'
|
2
|
+
|
3
|
+
# Immutable via API, I will work on freezing this later,
|
4
|
+
# but notice that when you 'save' you are not altering
|
5
|
+
# THIS instance, you return a new instance with the old
|
6
|
+
# and the new attributes
|
7
|
+
module Yahtzee
|
8
|
+
SCORINGS = [:aces, :twos, :threes,
|
9
|
+
:fours, :fives, :sixes,
|
10
|
+
:upper_subtotal, :upper_total,
|
11
|
+
:small_straight, :large_straight,
|
12
|
+
:full_house, :three_of_a_kind, :four_of_a_kind,
|
13
|
+
:yahtzee, :chance, :bonus_yahtzee_1,
|
14
|
+
:bonus_yahtzee_2, :bonus_yahtzee_3,
|
15
|
+
:lower_subtotal, :game_total]
|
16
|
+
|
17
|
+
class ScoreCard < Struct.new(*SCORINGS)
|
18
|
+
def initialize(attrs={})
|
19
|
+
super *attrs.values_at(*self.class.members)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
Yahtzee::SCORINGS.reduce({}) do |hash, attr|
|
24
|
+
hash.merge(attr => send(attr))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.upper_keys
|
29
|
+
[:aces, :twos, :threes, :fours, :fives, :sixes]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.lower_keys
|
33
|
+
[:yahtzee, :bonus_yahtzee_1, :bonus_yahtzee_2,
|
34
|
+
:bonus_yahtzee_3, :three_of_a_kind,
|
35
|
+
:four_of_a_kind, :full_house, :small_straight,
|
36
|
+
:large_straight, :chance]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def self.persist(score_card)
|
41
|
+
->(placement, value) {
|
42
|
+
old_values = score_card.to_hash
|
43
|
+
new_values = { placement => value }
|
44
|
+
score_card = new(old_values.merge(new_values))
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.placement_keys
|
49
|
+
Yahtzee::SCORINGS - meta_keys
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.meta_keys
|
53
|
+
[:bonus_yahtzee_1, :bonus_yahtzee_2,
|
54
|
+
:bonus_yahtzee_3, :upper_subtotal,
|
55
|
+
:upper_total, :lower_subtotal,
|
56
|
+
:game_total]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|