reachy 1.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/bin/reachy +5 -0
- data/lib/banner +9 -0
- data/lib/reachy/defines.rb +23 -0
- data/lib/reachy/game.rb +222 -0
- data/lib/reachy/game_menu.rb +228 -0
- data/lib/reachy/main_menu.rb +192 -0
- data/lib/reachy/round.rb +242 -0
- data/lib/reachy/scoretable.rb +169 -0
- data/lib/reachy/scoring.rb +86 -0
- data/lib/reachy/util.rb +156 -0
- data/lib/reachy.rb +32 -0
- data/tests/test_reachy.rb +21 -0
- metadata +58 -0
data/lib/reachy/round.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
require_relative 'scoring'
|
2
|
+
|
3
|
+
##############################################
|
4
|
+
# Round record class
|
5
|
+
##############################################
|
6
|
+
module Reachy
|
7
|
+
class Round
|
8
|
+
|
9
|
+
attr_reader :name # Name of round (e.g. E1B2R1)
|
10
|
+
attr_reader :mode # Game mode
|
11
|
+
attr_reader :wind # Round wind
|
12
|
+
attr_reader :number # Round number
|
13
|
+
attr_reader :bonus # Current bonus sticks
|
14
|
+
attr_accessor :riichi # Current riichi sticks
|
15
|
+
attr_accessor :scores # Hash of <player's name> => <score>
|
16
|
+
|
17
|
+
# Initialize round
|
18
|
+
# Param: round - hash of round data
|
19
|
+
# Populate Round object with info from hash
|
20
|
+
def initialize(db)
|
21
|
+
@name = db["name"]
|
22
|
+
@wind = db["wind"]
|
23
|
+
@number = db["number"]
|
24
|
+
@bonus = db["bonus"]
|
25
|
+
@riichi = db["riichi"]
|
26
|
+
@scores = db["scores"]
|
27
|
+
@mode = @scores.length
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return a deep copy of this Round object
|
31
|
+
def clone
|
32
|
+
return Marshal.load(Marshal.dump(self))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return Hash object representing Round object
|
36
|
+
def to_h
|
37
|
+
hash = {}
|
38
|
+
self.instance_variables.each do |var|
|
39
|
+
hash[var.to_s[1..-1]] = self.instance_variable_get(var)
|
40
|
+
end
|
41
|
+
hash.delete("mode")
|
42
|
+
return hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add riichi stick declared by player
|
46
|
+
# Param: player - string of player's name
|
47
|
+
# Return: true if successful, else false
|
48
|
+
# Note: - round name should not be changed
|
49
|
+
# - do not write to file here
|
50
|
+
def add_riichi(player)
|
51
|
+
if @scores[player] >= Scoring::P_RIICHI
|
52
|
+
@riichi += 1
|
53
|
+
@scores[player] -= Scoring::P_RIICHI
|
54
|
+
return true
|
55
|
+
else
|
56
|
+
puts "Unable to declare riichi: not enough points"
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Update wind and round number to next round
|
62
|
+
def next_round
|
63
|
+
if not @wind
|
64
|
+
@wind = "E"
|
65
|
+
@number = 1
|
66
|
+
elsif @number == @mode
|
67
|
+
if @wind == "W"
|
68
|
+
printf "Already in last possible round (West %d)!\n", @number
|
69
|
+
return []
|
70
|
+
else
|
71
|
+
@wind = (@wind == "E" ? "S" : "W")
|
72
|
+
@number = 1
|
73
|
+
end
|
74
|
+
else
|
75
|
+
@number += 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Give bonus and riichi points to player
|
80
|
+
# Param: winner - string of player's name to award points to
|
81
|
+
# loser - list of players who pay for bonus points
|
82
|
+
# dealer - bool indicating whether winner was dealer
|
83
|
+
# Return: true if successful, else false
|
84
|
+
def award_bonus(winner,loser,dealer)
|
85
|
+
if @scores.include?(winner)
|
86
|
+
@scores[winner] += @bonus*Scoring::P_BONUS
|
87
|
+
share_count = loser.length
|
88
|
+
if share_count>1 then share_count += (@mode==3 ? 1 : 0) end
|
89
|
+
bonus_paym = (@bonus*Scoring::P_BONUS)/share_count
|
90
|
+
loser.each do |l|
|
91
|
+
if @scores.include?(l)
|
92
|
+
@scores[l] -= bonus_paym
|
93
|
+
else
|
94
|
+
printf "Error: \"%s\" not in current list of players\n", l
|
95
|
+
end
|
96
|
+
end
|
97
|
+
@bonus = 0 if not dealer
|
98
|
+
@scores[winner] += @riichi*Scoring::P_RIICHI
|
99
|
+
@riichi = 0
|
100
|
+
return true
|
101
|
+
else
|
102
|
+
printf "Error: \"%s\" not in current list of players\n", winner
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Update round name
|
108
|
+
def update_name
|
109
|
+
@name = @wind ? (@wind + @number.to_s) : ""
|
110
|
+
@name += "B" + @bonus.to_s if @bonus > 0
|
111
|
+
@name += "R" + @riichi.to_s if @riichi > 0
|
112
|
+
end
|
113
|
+
|
114
|
+
# Update round data from given input
|
115
|
+
# Param: type - round result type (tsumo/ron/tenpai/noten/chombo)
|
116
|
+
# dealer - string of dealer's name
|
117
|
+
# winner - list of winner's name or players in tenpai
|
118
|
+
# loser - list of players who chombo or did not win
|
119
|
+
# hand - list of list of hand values (e.g. [["mangan"], [2,60]])
|
120
|
+
# Return: true if successful, else false
|
121
|
+
# Usage: This round is a clone of previous round, so just update its values.
|
122
|
+
# Multiple ron winners: - winner and hand lists must match
|
123
|
+
# - first winner in list gets bonus/riichi points
|
124
|
+
def update_round(type,dealer,winner,loser,hand)
|
125
|
+
# Verify inputs
|
126
|
+
if (dealer == nil)
|
127
|
+
puts "Error: Missing dealer's name"
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
if (hand.empty? || hand.first.empty?) && (type==T_TSUMO || type==T_RON)
|
131
|
+
puts "Error: Missing hand value"
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
dealer_flag = winner.include?(dealer)
|
136
|
+
|
137
|
+
# Handle each round type
|
138
|
+
case type
|
139
|
+
|
140
|
+
when T_TSUMO
|
141
|
+
# Tsumo type: loser = everyone else
|
142
|
+
losers = @scores.keys
|
143
|
+
losers -= winner
|
144
|
+
return false if not self.award_bonus(winner.first,losers,dealer_flag)
|
145
|
+
if dealer_flag then @bonus += 1 else self.next_round end
|
146
|
+
score_h = Scoring.get_tsumo(dealer_flag, hand.first)
|
147
|
+
winner.each do |w|
|
148
|
+
@scores[w] += if dealer_flag then score_h["nondealer"]*(@mode-1)
|
149
|
+
else (score_h["dealer"]+score_h["nondealer"]*(@mode-2)) end
|
150
|
+
end
|
151
|
+
losers.each do |l|
|
152
|
+
@scores[l] -= score_h[l==dealer ? "dealer" : "nondealer"]
|
153
|
+
end
|
154
|
+
|
155
|
+
when T_RON
|
156
|
+
# Ron type - can have multiple winners off of same loser
|
157
|
+
return false if not self.award_bonus(winner.first,loser,dealer_flag)
|
158
|
+
if dealer_flag then @bonus += 1 else self.next_round end
|
159
|
+
winner.zip(hand).each do |w,h|
|
160
|
+
paym = Scoring.get_ron((w==dealer),h)
|
161
|
+
@scores[w] += paym
|
162
|
+
@scores[loser.first] -= paym
|
163
|
+
end
|
164
|
+
|
165
|
+
when T_TENPAI
|
166
|
+
# Tenpai type: losers = all - winners
|
167
|
+
losers = @scores.keys
|
168
|
+
losers -= winner
|
169
|
+
if winner.length < @mode
|
170
|
+
if dealer_flag then @bonus += 1 else self.next_round end
|
171
|
+
total = @mode==4 ? Scoring::P_TENPAI_4 : Scoring::P_TENPAI_3
|
172
|
+
paym = total / losers.length
|
173
|
+
recv = total / winner.length
|
174
|
+
winner.each do |w|
|
175
|
+
@scores[w] += recv
|
176
|
+
end
|
177
|
+
losers.each do |l|
|
178
|
+
@scores[l] -= paym
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
when T_NOTEN
|
183
|
+
# Noten type: ignore all other params
|
184
|
+
self.next_round
|
185
|
+
|
186
|
+
when T_CHOMBO
|
187
|
+
# Chombo type: loser = chombo player, winner = everyone else
|
188
|
+
winners = @scores.keys
|
189
|
+
winners -= loser
|
190
|
+
dealer_flag = loser.include?(dealer)
|
191
|
+
score_h = Scoring.get_chombo(dealer_flag)
|
192
|
+
@scores[loser.first] -= if dealer_flag then score_h["nondealer"]*(@mode-1)
|
193
|
+
else (score_h["dealer"] + score_h["nondealer"]*(@mode-2)) end
|
194
|
+
winners.each do |w|
|
195
|
+
@scores[w] += score_h[w==dealer ? "dealer" : "nondealer"]
|
196
|
+
end
|
197
|
+
|
198
|
+
else
|
199
|
+
printf "Invalid round result type\n", type
|
200
|
+
puts nil
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
|
204
|
+
return true
|
205
|
+
end
|
206
|
+
|
207
|
+
# Print single line round scores
|
208
|
+
# Param: delta - hash of score deltas between this round and previous one
|
209
|
+
# ongoing - bool indicating if round is ongoing (optional)
|
210
|
+
def print_scores(delta,ongoing=false)
|
211
|
+
round_name = @name
|
212
|
+
round_name += "*" if ongoing
|
213
|
+
printf "%-#{COL_SPACING}s", round_name
|
214
|
+
@scores.each do |key,val|
|
215
|
+
if delta
|
216
|
+
d = delta[key]
|
217
|
+
d = 0.0 if not d
|
218
|
+
if d == 0 # not sure why one-liner doesn't work here...
|
219
|
+
d = "="
|
220
|
+
elsif d < 0
|
221
|
+
d = d.to_s + "k"
|
222
|
+
else
|
223
|
+
d = "+" + d.to_s + "k"
|
224
|
+
end
|
225
|
+
val = val.to_s + "(" + d + ")"
|
226
|
+
else
|
227
|
+
val = val.to_s
|
228
|
+
end
|
229
|
+
printf "%-#{COL_SPACING}s", val
|
230
|
+
end
|
231
|
+
puts nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# Print bonus and riichi sticks summary
|
235
|
+
def print_sticks
|
236
|
+
printf " %-#{COL_SPACING}s: %d\n", "Bonus sticks", @bonus
|
237
|
+
printf " %-#{COL_SPACING}s: %d\n", "Riichi sticks", @riichi
|
238
|
+
puts nil
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
##############################################
|
2
|
+
# Hash objects containing scoring tables
|
3
|
+
##############################################
|
4
|
+
module Reachy
|
5
|
+
|
6
|
+
# Hash of tsumo scores
|
7
|
+
H_TSUMO = {
|
8
|
+
"dealer" => {
|
9
|
+
"han_1" => {
|
10
|
+
"fu_20" => 400,
|
11
|
+
"fu_30" => 500,
|
12
|
+
"fu_40" => 700,
|
13
|
+
"fu_50" => 800,
|
14
|
+
"fu_60" => 1000,
|
15
|
+
"fu_70" => 1200
|
16
|
+
},
|
17
|
+
"han_2" => {
|
18
|
+
"fu_20" => 700,
|
19
|
+
"fu_30" => 1000,
|
20
|
+
"fu_40" => 1300,
|
21
|
+
"fu_50" => 1600,
|
22
|
+
"fu_60" => 2000,
|
23
|
+
"fu_70" => 2300
|
24
|
+
},
|
25
|
+
"han_3" => {
|
26
|
+
"fu_20" => 1300,
|
27
|
+
"fu_25" => 1600,
|
28
|
+
"fu_30" => 2000,
|
29
|
+
"fu_40" => 2600,
|
30
|
+
"fu_50" => 3200,
|
31
|
+
"fu_60" => 3900
|
32
|
+
},
|
33
|
+
"han_4" => {
|
34
|
+
"fu_20" => 2600,
|
35
|
+
"fu_25" => 3200,
|
36
|
+
"fu_30" => 3900
|
37
|
+
},
|
38
|
+
"mangan" => 4000,
|
39
|
+
"haneman" => 6000,
|
40
|
+
"baiman" => 8000,
|
41
|
+
"sanbaiman" => 12000,
|
42
|
+
"yakuman" => 16000,
|
43
|
+
#"double_yakuman" => 32000
|
44
|
+
},
|
45
|
+
"nondealer" => {
|
46
|
+
"han_1" => {
|
47
|
+
"fu_20" => { "dealer" => 400, "nondealer" => 200 },
|
48
|
+
"fu_30" => { "dealer" => 500, "nondealer" => 300 },
|
49
|
+
"fu_40" => { "dealer" => 700, "nondealer" => 400 },
|
50
|
+
"fu_50" => { "dealer" => 800, "nondealer" => 400 },
|
51
|
+
"fu_60" => { "dealer" => 1000, "nondealer" => 500 },
|
52
|
+
"fu_70" => { "dealer" => 1200, "nondealer" => 600 }
|
53
|
+
},
|
54
|
+
"han_2" => {
|
55
|
+
"fu_20" => { "dealer" => 700, "nondealer" => 400 },
|
56
|
+
"fu_30" => { "dealer" => 1000, "nondealer" => 500 },
|
57
|
+
"fu_40" => { "dealer" => 1300, "nondealer" => 700 },
|
58
|
+
"fu_50" => { "dealer" => 1600, "nondealer" => 800 },
|
59
|
+
"fu_60" => { "dealer" => 2000, "nondealer" => 1000 },
|
60
|
+
"fu_70" => { "dealer" => 2300, "nondealer" => 1200 }
|
61
|
+
},
|
62
|
+
"han_3" => {
|
63
|
+
"fu_20" => { "dealer" => 1300, "nondealer" => 700 },
|
64
|
+
"fu_25" => { "dealer" => 1600, "nondealer" => 800 },
|
65
|
+
"fu_30" => { "dealer" => 2000, "nondealer" => 1000 },
|
66
|
+
"fu_40" => { "dealer" => 2600, "nondealer" => 1300 },
|
67
|
+
"fu_50" => { "dealer" => 3200, "nondealer" => 1600 },
|
68
|
+
"fu_60" => { "dealer" => 3900, "nondealer" => 2000 }
|
69
|
+
},
|
70
|
+
"han_4" => {
|
71
|
+
"fu_20" => { "dealer" => 2600, "nondealer" => 1300 },
|
72
|
+
"fu_25" => { "dealer" => 3200, "nondealer" => 1600 },
|
73
|
+
"fu_30" => { "dealer" => 3900, "nondealer" => 2000 }
|
74
|
+
},
|
75
|
+
"mangan" => { "dealer" => 4000, "nondealer" => 2000 },
|
76
|
+
"haneman" => { "dealer" => 6000, "nondealer" => 3000 },
|
77
|
+
"baiman" => { "dealer" => 8000, "nondealer" => 4000 },
|
78
|
+
"sanbaiman" => { "dealer" => 12000, "nondealer" => 6000 },
|
79
|
+
"yakuman" => { "dealer" => 16000, "nondealer" => 8000 },
|
80
|
+
#"double_yakuman" => { "dealer" => 32000, "nondealer" => 16000 }
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
# Hash of ron scores
|
85
|
+
H_RON = {
|
86
|
+
"dealer" => {
|
87
|
+
"han_1" => {
|
88
|
+
"fu_20" => 1000,
|
89
|
+
"fu_30" => 1500,
|
90
|
+
"fu_40" => 2000,
|
91
|
+
"fu_50" => 2400,
|
92
|
+
"fu_60" => 2900,
|
93
|
+
"fu_70" => 3400
|
94
|
+
},
|
95
|
+
"han_2" => {
|
96
|
+
"fu_20" => 2000,
|
97
|
+
"fu_25" => 2400,
|
98
|
+
"fu_30" => 2900,
|
99
|
+
"fu_40" => 3900,
|
100
|
+
"fu_50" => 4800,
|
101
|
+
"fu_60" => 5800,
|
102
|
+
"fu_70" => 6800
|
103
|
+
},
|
104
|
+
"han_3" => {
|
105
|
+
"fu_20" => 3900,
|
106
|
+
"fu_25" => 4800,
|
107
|
+
"fu_30" => 5800,
|
108
|
+
"fu_40" => 7700,
|
109
|
+
"fu_50" => 9600,
|
110
|
+
"fu_60" => 11600
|
111
|
+
},
|
112
|
+
"han_4" => {
|
113
|
+
"fu_20" => 7700,
|
114
|
+
"fu_25" => 9600,
|
115
|
+
"fu_30" => 11600
|
116
|
+
},
|
117
|
+
"mangan" => 12000,
|
118
|
+
"haneman" => 18000,
|
119
|
+
"baiman" => 24000,
|
120
|
+
"sanbaiman" => 36000,
|
121
|
+
"yakuman" => 48000,
|
122
|
+
#"double_yakuman" => 96000
|
123
|
+
},
|
124
|
+
"nondealer" => {
|
125
|
+
"han_1" => {
|
126
|
+
"fu_20" => 700,
|
127
|
+
"fu_30" => 1000,
|
128
|
+
"fu_40" => 1300,
|
129
|
+
"fu_50" => 1600,
|
130
|
+
"fu_60" => 2000,
|
131
|
+
"fu_70" => 2300
|
132
|
+
},
|
133
|
+
"han_2" => {
|
134
|
+
"fu_20" => 1300,
|
135
|
+
"fu_30" => 2000,
|
136
|
+
"fu_25" => 1600,
|
137
|
+
"fu_40" => 2600,
|
138
|
+
"fu_50" => 3200,
|
139
|
+
"fu_60" => 3900,
|
140
|
+
"fu_70" => 4500
|
141
|
+
},
|
142
|
+
"han_3" => {
|
143
|
+
"fu_20" => 2600,
|
144
|
+
"fu_25" => 3200,
|
145
|
+
"fu_30" => 3900,
|
146
|
+
"fu_40" => 5200,
|
147
|
+
"fu_50" => 6300,
|
148
|
+
"fu_60" => 7700
|
149
|
+
},
|
150
|
+
"han_4" => {
|
151
|
+
"fu_20" => 5200,
|
152
|
+
"fu_25" => 6400,
|
153
|
+
"fu_30" => 7700
|
154
|
+
},
|
155
|
+
"mangan" => 8000,
|
156
|
+
"haneman" => 12000,
|
157
|
+
"baiman" => 16000,
|
158
|
+
"sanbaiman" => 24000,
|
159
|
+
"yakuman" => 32000,
|
160
|
+
#"double_yakuman" => 64000
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
# Hash of chombo scores
|
165
|
+
H_CHOMBO = {
|
166
|
+
"dealer" => 4000,
|
167
|
+
"nondealer" => { "dealer" => 4000, "nondealer" => 2000 }
|
168
|
+
}
|
169
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative 'defines'
|
2
|
+
require_relative 'scoretable'
|
3
|
+
|
4
|
+
##############################################
|
5
|
+
# Scoring class
|
6
|
+
##############################################
|
7
|
+
module Reachy
|
8
|
+
module Scoring
|
9
|
+
|
10
|
+
# Point constants
|
11
|
+
P_BONUS = 300
|
12
|
+
P_RIICHI = 1000
|
13
|
+
P_TENPAI_3 = 2000
|
14
|
+
P_TENPAI_4 = 3000
|
15
|
+
P_START_3 = 35000
|
16
|
+
P_START_4 = 25000
|
17
|
+
|
18
|
+
# Convert han, fu input to hash keys
|
19
|
+
# Param: han - fixnum of han's
|
20
|
+
# fu - fixnum of fu's
|
21
|
+
# Return: list of keys
|
22
|
+
def self.to_keys(han,fu)
|
23
|
+
keys = []
|
24
|
+
if ((han<3 && fu>70) || (han==3 && fu>60) || (han==4 && fu>30) || (han==5))
|
25
|
+
keys = ["mangan"]
|
26
|
+
elsif (han==6 || han==7)
|
27
|
+
keys = ["haneman"]
|
28
|
+
elsif (han==8 || han==9 || han==10)
|
29
|
+
keys = ["baiman"]
|
30
|
+
elsif (han==11 || han==12)
|
31
|
+
keys = ["sanbaiman"]
|
32
|
+
elsif (han==13)
|
33
|
+
keys = ["yakuman"]
|
34
|
+
else
|
35
|
+
keys << "han_" + han.to_s
|
36
|
+
keys << "fu_" + fu.to_s
|
37
|
+
end
|
38
|
+
return keys
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get Tsumo settlements
|
42
|
+
# Param: dealer - bool indicating if dealer won
|
43
|
+
# hand - list representing hand value (e.g. ["mangan"], [2,60])
|
44
|
+
# Return: hash of points
|
45
|
+
def self.get_tsumo(dealer,hand)
|
46
|
+
keys_h = if hand.first.instance_of?(String) then hand
|
47
|
+
else Scoring.to_keys(hand[0],hand[1]) end
|
48
|
+
if dealer
|
49
|
+
val = if keys_h.length == 2 then H_TSUMO["dealer"][keys_h[0]][keys_h[1]]
|
50
|
+
else H_TSUMO["dealer"][keys_h[0]] end
|
51
|
+
if not val then return nil end
|
52
|
+
return { "nondealer" => val }
|
53
|
+
else
|
54
|
+
val = if keys_h.length == 2 then H_TSUMO["nondealer"][keys_h[0]][keys_h[1]]
|
55
|
+
else H_TSUMO["nondealer"][keys_h[0]] end
|
56
|
+
return val
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get Ron settlements
|
61
|
+
# Param: dealer - bool indicating if dealer won
|
62
|
+
# hand - list representing hand value (e.g. ["mangan"], [2,60])
|
63
|
+
# Return: fixnum of points
|
64
|
+
def self.get_ron(dealer,hand)
|
65
|
+
keys_h = if hand.first.instance_of?(String) then hand
|
66
|
+
else Scoring.to_keys(hand[0],hand[1]) end
|
67
|
+
val = if keys_h[1] then
|
68
|
+
H_RON[dealer ? "dealer" : "nondealer"][keys_h[0]][keys_h[1]]
|
69
|
+
else H_RON[dealer ? "dealer" : "nondealer"][keys_h[0]] end
|
70
|
+
return val
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get Chombo settlements
|
74
|
+
# Param: dealer - bool indicating if dealer chombo
|
75
|
+
# Return: hash of points
|
76
|
+
def self.get_chombo(dealer)
|
77
|
+
if dealer
|
78
|
+
val = H_CHOMBO["dealer"]
|
79
|
+
if not val then return nil end
|
80
|
+
return { "nondealer" => val }
|
81
|
+
else
|
82
|
+
return H_CHOMBO["nondealer"]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/reachy/util.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
require_relative 'defines'
|
4
|
+
|
5
|
+
##############################################
|
6
|
+
# Reachy utilities
|
7
|
+
##############################################
|
8
|
+
module Reachy
|
9
|
+
|
10
|
+
# Prompt for user input with a message.
|
11
|
+
# If EOF is entered, aborts program.
|
12
|
+
# Param: message - string to display
|
13
|
+
# downcase - whether to downcase input
|
14
|
+
# Return: User input
|
15
|
+
# Note: always strips input!
|
16
|
+
def self.prompt(message, downcase=true)
|
17
|
+
print message
|
18
|
+
input = gets
|
19
|
+
goodbye if !input
|
20
|
+
if downcase
|
21
|
+
return input.strip.downcase
|
22
|
+
else
|
23
|
+
return input.strip
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Prompt for one character, downcased.
|
28
|
+
# If EOF is entered, aborts program.
|
29
|
+
# Param: message - string to display
|
30
|
+
# Return: User input character, downcased
|
31
|
+
def self.prompt_ch(message)
|
32
|
+
print message
|
33
|
+
input = STDIN.getch
|
34
|
+
goodbye if input == SIGINT_CH || input == EOF_CH
|
35
|
+
print input
|
36
|
+
return input.downcase
|
37
|
+
end
|
38
|
+
|
39
|
+
# Read all games in data dir, and store in @games array
|
40
|
+
def self.read_all_games
|
41
|
+
@games = []
|
42
|
+
Dir.foreach(File.expand_path("../../../data/", __FILE__)) do |filename|
|
43
|
+
# Skip . and .. dir entries, and trash dir
|
44
|
+
next if filename == '.' or filename == '..' or filename == "trash"
|
45
|
+
|
46
|
+
# Create game objects
|
47
|
+
game = Game.new(filename)
|
48
|
+
@games << game
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Print out all games in database
|
53
|
+
def self.display_all_games
|
54
|
+
if @games.empty?
|
55
|
+
puts " No game currently in database. Please add a new game."
|
56
|
+
puts nil
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
@games.each_with_index do |game, index|
|
60
|
+
printf " %d) ", index + 1
|
61
|
+
game.print_title
|
62
|
+
end
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Confirm game deletion
|
67
|
+
def self.confirm_delete(chosen_game)
|
68
|
+
printf "---> Deleting game \"%s\". This action cannot be undone.\n", chosen_game.filename
|
69
|
+
conf = prompt " Are you sure? (y/N) "
|
70
|
+
if conf == "y"
|
71
|
+
# Move associated json file to trash.
|
72
|
+
chosen_game.delete_from_disk
|
73
|
+
# Delete from @games array
|
74
|
+
@games.delete(chosen_game)
|
75
|
+
printf "*** Game \"%s\" deleted from database.\n\n", chosen_game.filename
|
76
|
+
return true
|
77
|
+
else
|
78
|
+
puts "You changed your mind? Fine.\n\n"
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Ensure that the data and data/trash directory exist, if not create them
|
84
|
+
def self.ensure_data_dir
|
85
|
+
if not File.directory?(File.expand_path("../../../data/", __FILE__))
|
86
|
+
Dir.mkdir File.expand_path("../../../data/", __FILE__)
|
87
|
+
end
|
88
|
+
if not File.directory?(File.expand_path("../../../data/trash/", __FILE__))
|
89
|
+
Dir.mkdir File.expand_path("../../../data/trash/", __FILE__)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Validate hand input
|
94
|
+
# Param: hand - string of hand value input
|
95
|
+
# Return: reformated hand value or empty list if input invalid
|
96
|
+
def self.validate_hand(hand)
|
97
|
+
split_hand = hand.split
|
98
|
+
hand = []
|
99
|
+
i = 0
|
100
|
+
flag = true
|
101
|
+
while i < split_hand.length # Did this C-style AKA imperatively.. how to ruby
|
102
|
+
if split_hand[i].match(/^\d+$/)
|
103
|
+
han = split_hand[i]
|
104
|
+
fu = split_hand[i+1]
|
105
|
+
if fu.match(/^\d+$/) && (fu.to_i==25 || fu.to_i%10 == 0)
|
106
|
+
hand << [han.to_i, fu.to_i]
|
107
|
+
i += 2
|
108
|
+
else
|
109
|
+
flag = false
|
110
|
+
hand = []
|
111
|
+
break
|
112
|
+
end
|
113
|
+
elsif L_HANDS.include?(split_hand[i])
|
114
|
+
hand << [split_hand[i]]
|
115
|
+
i += 1
|
116
|
+
else
|
117
|
+
flag = false
|
118
|
+
hand = []
|
119
|
+
break
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if not flag
|
123
|
+
printf "Hand value malformed: \"%s\"\n", hand
|
124
|
+
end
|
125
|
+
return hand
|
126
|
+
end
|
127
|
+
|
128
|
+
# Display winners of every game
|
129
|
+
def self.display_all_winners
|
130
|
+
puts " Current winners:"
|
131
|
+
puts " ----------------"
|
132
|
+
@games.each do |game|
|
133
|
+
high_score = game.scoreboard.last.scores.values.max
|
134
|
+
winners = game.scoreboard.last.scores.select{ |player, score| score == high_score}
|
135
|
+
printf " * %s: %s - %d points\n", game.filename, winners.keys.join(", "), high_score
|
136
|
+
end
|
137
|
+
puts nil
|
138
|
+
end
|
139
|
+
|
140
|
+
# Call system cowsay if available
|
141
|
+
def self.cowsay
|
142
|
+
system("cowsay Bye!") if system("which cowsay >/dev/null 2>&1")
|
143
|
+
end
|
144
|
+
|
145
|
+
# Message to print when quitting program
|
146
|
+
def self.goodbye
|
147
|
+
puts "\n\n"
|
148
|
+
printf " -------------------------------\n" \
|
149
|
+
" | Thanks for flying reachy! |\n" \
|
150
|
+
" -------------------------------\n\n"
|
151
|
+
display_all_winners
|
152
|
+
cowsay
|
153
|
+
exit 0
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
data/lib/reachy.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'date'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
require_relative 'reachy/game'
|
8
|
+
require_relative 'reachy/util'
|
9
|
+
require_relative 'reachy/main_menu'
|
10
|
+
|
11
|
+
##############################################
|
12
|
+
# Main driver functionality
|
13
|
+
##############################################
|
14
|
+
module Reachy
|
15
|
+
|
16
|
+
# Display initial screen (complete with banner, list of games)
|
17
|
+
def self.start_screen
|
18
|
+
# Display banner
|
19
|
+
File.open(File.expand_path("../banner", __FILE__), "r"){ |file| puts file.read }
|
20
|
+
puts nil
|
21
|
+
|
22
|
+
# Display all games in db
|
23
|
+
ensure_data_dir
|
24
|
+
read_all_games
|
25
|
+
puts "*** Current existing game(s) in database:"
|
26
|
+
puts nil if display_all_games
|
27
|
+
|
28
|
+
# Display main menu options
|
29
|
+
main_menu
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|