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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 08b598e9df22e627af1198a45436f0883644347b
4
+ data.tar.gz: c2005872367ba2345d1462c0a3bba7c046003cee
5
+ SHA512:
6
+ metadata.gz: b7cd060809d5ef86258ca046328fd40cfe3ea75dbb08da932aa38eb1a842192f3e3f615b74a061ad80e613fe9169b33869c37abc79b62308381691ec2f4044ac
7
+ data.tar.gz: f8dc5d0ee3560fb31ee171e4954dfb9af7901f83164190e48228835b17df858b66ca1a815027d3de3e66907f67320645855ae615d6c474a4dc5518cd21c86944
data/bin/reachy ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/reachy.rb"
4
+
5
+ Reachy.start_screen
data/lib/banner ADDED
@@ -0,0 +1,9 @@
1
+ ############################################
2
+ ### RIICHI SCORE TRACKER ###
3
+ ############################################
4
+ By: Thao Truong (Kainu) & Joshua Tang
5
+ Last Updated: 15 Jul 2016
6
+
7
+ A demo program that helps you keep track of
8
+ Japanese mahjong (Riichi) score as you play.
9
+ ____________________________________________
@@ -0,0 +1,23 @@
1
+ ##############################################
2
+ # Reachy constants
3
+ ##############################################
4
+ module Reachy
5
+
6
+ # Scoreboard formatting
7
+ COL_SPACING = 15
8
+
9
+ # Round result types
10
+ T_TSUMO = 1
11
+ T_RON = 2
12
+ T_TENPAI = 3
13
+ T_NOTEN = 4
14
+ T_CHOMBO = 5
15
+
16
+ # List of named hand values
17
+ L_HANDS = ["mangan","haneman","baiman","sanbaiman","yakuman"]
18
+
19
+ # Special characters
20
+ L_NEWLINE = ["\r","\n","\r\n"]
21
+ SIGINT_CH = "\u0003"
22
+ EOF_CH = "\u0004"
23
+ end
@@ -0,0 +1,222 @@
1
+ require 'json'
2
+ require 'date'
3
+ require 'pp'
4
+
5
+ require_relative 'round'
6
+
7
+ ##############################################
8
+ # Scoreboard record class
9
+ ##############################################
10
+ module Reachy
11
+ class Game
12
+
13
+ attr_reader :filename # Name of JSON file
14
+ attr_reader :created_at # Timestamp at creation
15
+ attr_accessor :last_updated # Timestamp at last updated
16
+ attr_reader :mode # Game mode
17
+ attr_reader :players # List of player names
18
+ attr_accessor :scoreboard # List of round records
19
+
20
+ # Initialize game from given hash
21
+ # Param: filename - name of json file
22
+ # ondisk - whether Game object exists on disk
23
+ # players - array of player names associatied with this game
24
+ def initialize(filename, ondisk=true, players=[])
25
+ if ondisk
26
+ # Game object exists on disk. Read it from file.
27
+ self.read_data(filename)
28
+ else
29
+ # Create new Game object with starting values.
30
+ @filename = filename
31
+ @created_at = DateTime.now
32
+ @last_updated = DateTime.now
33
+ @mode = players.length
34
+ @players = players
35
+ self.initialize_scoreboard
36
+ self.write_data
37
+ end
38
+ @plist = @players.map {|x| x.downcase}
39
+ end
40
+
41
+ # Populate @scoreboard with starting Round objects
42
+ def initialize_scoreboard
43
+ # Make initial scores e.g. { "joshua" => 35000, "kenta" => 35000, "thao" => 35000 }
44
+ start_score = mode == 3 ? Scoring::P_START_3 : Scoring::P_START_4
45
+ init_scores = Hash[ @players.map{ |p| [p.downcase, start_score] } ]
46
+ init_round = {"name" => "",
47
+ "wind" => nil,
48
+ "number" => 0,
49
+ "bonus" => 0,
50
+ "riichi" => 0,
51
+ "scores" => init_scores}
52
+ @scoreboard = [Round.new(init_round)]
53
+ self.clone_last_round(true)
54
+ end
55
+
56
+ # Populate Game object with info from hash
57
+ # Param: db - hash of game data
58
+ def populate(db)
59
+ @filename = db["filename"]
60
+ @created_at = DateTime.parse(db["created_at"])
61
+ @last_updated = DateTime.parse(db["last_updated"])
62
+ @mode = db["mode"]
63
+ @players = db["players"]
64
+ @scoreboard = []
65
+ db["scoreboard"].each do |round|
66
+ @scoreboard << Round.new(round)
67
+ end
68
+ end
69
+
70
+ # Return Hash object representing Game object
71
+ def to_h
72
+ hash = {}
73
+ self.instance_variables.each do |var|
74
+ hash[var.to_s[1..-1]] = self.instance_variable_get(var)
75
+ end
76
+ hash["scoreboard"] = []
77
+ @scoreboard.each do |r|
78
+ hash["scoreboard"] << r.to_h
79
+ end
80
+ hash.delete("plist")
81
+ return hash
82
+ end
83
+
84
+ # Read JSON database file and repopulate object
85
+ def read_data(filename)
86
+ filepath = File.expand_path("../../../data/" + filename, __FILE__)
87
+ file = File.read(filepath)
88
+ db = JSON.parse(file)
89
+ self.populate(db)
90
+ end
91
+
92
+ # Write Game object to JSON database file
93
+ def write_data
94
+ @last_updated = DateTime.now
95
+ hash = self.to_h
96
+ filepath = File.expand_path("../../../data/" + @filename, __FILE__)
97
+ File.open(filepath, "w") do |f|
98
+ f.write(JSON.pretty_generate(hash))
99
+ end
100
+ end
101
+
102
+ # Add new round result
103
+ def add_round(type, dealer, winner, loser, hand)
104
+ if not @scoreboard.last.update_round(type, dealer, winner, loser, hand)
105
+ printf " An error occurred while updating round score.\n" \
106
+ " Please check your input (winner, hand) and try again.\n\n"
107
+ return
108
+ end
109
+ self.clone_last_round
110
+ self.write_data
111
+ end
112
+
113
+ # Clone last round as next round and add to scoreboard
114
+ # Param: to_next - bool indicating whether to move to next round (optional)
115
+ def clone_last_round(to_next=false)
116
+ new_round = @scoreboard.last.clone
117
+ if (to_next || new_round.name == "") then new_round.next_round end
118
+ new_round.update_name
119
+ @scoreboard << new_round
120
+ end
121
+
122
+ # Remove latest round from scoreboard
123
+ def remove_last_round
124
+ if @scoreboard.length > 2
125
+ @scoreboard.pop
126
+ @scoreboard.pop
127
+ self.clone_last_round
128
+ self.write_data
129
+ else
130
+ printf "Error: Current game already in initial state. " \
131
+ "No round deleted.\n"
132
+ end
133
+ end
134
+
135
+ # Move data file of this game to trash
136
+ def delete_from_disk
137
+ FileUtils.mv(File.expand_path("../../../data/" + @filename, __FILE__),
138
+ File.expand_path("../../../data/trash/" + @filename, __FILE__))
139
+ end
140
+
141
+ # Validate players input
142
+ # Param: players - list of players to check
143
+ # Return: true if all players in list are in this game, else false
144
+ def validate_players(players)
145
+ flag = true
146
+ players.each do |p|
147
+ if not @plist.include?(p)
148
+ printf "Error: Player \"%s\" not in current list of players\n", p
149
+ flag = false
150
+ end
151
+ end
152
+ return flag
153
+ end
154
+
155
+ # Add riichi stick declared by player
156
+ # Param: player - string of player's name
157
+ # Return: true if successful, else false
158
+ def add_riichi(player)
159
+ if @players.map(&:downcase).include? player
160
+ ret = @scoreboard.last.add_riichi(player)
161
+ if ret then self.write_data end
162
+ return ret
163
+ else
164
+ printf "Error: \"%s\" not in current game's players list\n", player
165
+ return false
166
+ end
167
+ end
168
+
169
+ # Print scoreboard header
170
+ # Round Joshua Kenta Thao
171
+ def print_header
172
+ #maxlen = [@players.max_by(&:length).length, 5].max
173
+ printf "%-#{COL_SPACING}s", "Round"
174
+ @players.each do |p|
175
+ printf "%-#{COL_SPACING}s", p
176
+ end
177
+ puts nil
178
+ end
179
+
180
+ # Print entire scoreboard
181
+ def print_scoreboard
182
+ self.print_title
183
+ (COL_SPACING * (@mode+1)).times { printf "-" }
184
+ puts nil
185
+ self.print_header
186
+ @scoreboard[0].print_scores(nil) # print init scores
187
+ @scoreboard[1..-2].each.with_index(1) do |curr,i|
188
+ delta = {}
189
+ prev = @scoreboard[i-1]
190
+ prev.scores.each do |k,v|
191
+ delta[k] = (curr.scores[k]-prev.scores[k]).to_f/1000
192
+ end
193
+ curr.print_scores(delta)
194
+ end
195
+ # print ongoing round
196
+ @scoreboard.last.print_scores(nil,true)
197
+ puts nil
198
+ end
199
+
200
+ # Print last round scores
201
+ # Round Joshua Kenta Thao
202
+ # E1B1R2 33400 39800 31800
203
+ def print_last_round
204
+ self.print_header
205
+ @scoreboard[-2].print_scores(nil)
206
+ end
207
+
208
+ # Print 1-line game title
209
+ # game1: 3P (E1B1R1) ~ Joshua, Kenta, Thao ~ 2016-11-05T14:05:00-07:00
210
+ def print_title
211
+ printf "%s: %dP (%s) ~ %s ~ %s", @filename, @mode, @scoreboard[-2].name,
212
+ @players.join(", "), @last_updated
213
+ puts nil
214
+ end
215
+
216
+ # Print current round sticks
217
+ def print_current_sticks
218
+ @scoreboard.last.print_sticks
219
+ end
220
+
221
+ end
222
+ end
@@ -0,0 +1,228 @@
1
+ require_relative 'game'
2
+ require_relative 'round'
3
+ require_relative 'util'
4
+
5
+ ##############################################
6
+ # Game menu and specific game interactions
7
+ ##############################################
8
+ module Reachy
9
+
10
+ # Game menu for a particular game
11
+ def self.game_menu
12
+ loop do
13
+ game = @games[@selected_game_index]
14
+ puts "(Enter \"x\" to go back to main menu.)\n"
15
+ puts nil
16
+ printf "*** Game \"%s\" Options:\n" \
17
+ " 1) Add next round result\n" \
18
+ " 2) Declare riichi\n" \
19
+ " 3) View current scoreboard\n" \
20
+ " 4) Remove last round entry\n" \
21
+ " 5) Delete current game\n" \
22
+ " 6) Choose a different game\n" \
23
+ " 7) Add new game\n", game.filename
24
+ choice = prompt_ch "---> Enter your choice: "
25
+ puts nil
26
+ case choice
27
+ when "x"
28
+ puts nil
29
+ return # to main menu
30
+ when "1"
31
+ puts "\n[Add next round result]"
32
+ puts nil
33
+ add_round(game)
34
+ when "2"
35
+ puts "\n[Declare riichi]"
36
+ puts nil
37
+ declare_riichi(game)
38
+ when "3"
39
+ puts "\n[View current scoreboard]"
40
+ puts nil
41
+ game.print_scoreboard
42
+ puts "(Press any key to continue)"
43
+ STDIN.getch
44
+ when "4"
45
+ puts "\n[Remove last round entry]"
46
+ puts nil
47
+ remove_last_round(game)
48
+ when "5"
49
+ puts "\n[Delete current game]"
50
+ puts nil
51
+ return if confirm_delete(game) # main menu if current game deleted
52
+ when "6"
53
+ puts "\n[Choose a different game]"
54
+ puts nil
55
+ view_game
56
+ when "7"
57
+ puts "\n[Add new game]"
58
+ puts nil
59
+ add_game
60
+ when ""
61
+ puts "\nEnter a choice... >_>"
62
+ puts nil
63
+ else
64
+ printf "\nInvalid choice: %s\n", choice
65
+ puts nil
66
+ end
67
+ end
68
+ end
69
+
70
+ # Add a new round to the current game. Sub menu option 1.
71
+ def self.add_round(game)
72
+ puts "(Enter \"x\" to return to game options.)"
73
+ puts nil
74
+ dealer = prompt "---> Dealer's name: "
75
+ return if dealer == "x"
76
+ puts nil
77
+
78
+ loop do
79
+ printf "*** Round result type:\n" \
80
+ " 1) Tsumo\n" \
81
+ " 2) Ron\n" \
82
+ " 3) Tenpai\n" \
83
+ " 4) Noten\n" \
84
+ " 5) Chombo\n"
85
+ choice = prompt "---> Select round result: "
86
+ case choice
87
+ when "x"
88
+ puts nil
89
+ return
90
+ when "1"
91
+ # Tsumo
92
+ type = T_TSUMO
93
+ winner = prompt "---> Winner's name: "
94
+ return if winner == "x"
95
+ winner = winner.split
96
+ if winner.length > 1
97
+ puts " Assuming \"%s\" is the winner, ignoring remaining players.",
98
+ winner.first
99
+ winner = [winner.first]
100
+ end
101
+ next if not game.validate_players(winner)
102
+
103
+ hand = prompt "---> Hand value(s) (e.g. \"2 30\" or \"mangan\"): "
104
+ return if hand == "x"
105
+ hand = validate_hand(hand)
106
+
107
+ loser = [] # Round::update_round will set loser = all - winner
108
+ game.add_round(type, dealer, winner, loser, hand)
109
+ break
110
+ when "2"
111
+ # Ron
112
+ type = T_RON
113
+ puts nil
114
+ winner = prompt "---> Winner(s) (first winner gets bonus and riichi sticks): "
115
+ return if winner == "x"
116
+ winner = winner.split
117
+ next if not game.validate_players(winner)
118
+
119
+ loser = prompt "---> Player who dealt into winning hand(s): "
120
+ return if loser == "x"
121
+ loser = loser.split
122
+ if loser.length > 1
123
+ puts " Assuming \"%s\" is the player who dealt into winning hand.",
124
+ loser.first
125
+ loser = [loser.first]
126
+ end
127
+ next if not game.validate_players(loser)
128
+ if winner.include? loser.first
129
+ puts "Loser can't be a winner..."
130
+ next
131
+ end
132
+
133
+ hand = prompt "---> Hand value(s) (e.g. \"2 30 yakuman\" or \"mangan\"): "
134
+ puts nil
135
+ return if hand == "x"
136
+ hand = validate_hand(hand)
137
+
138
+ if hand.length != winner.length
139
+ printf "The number of winners and winning hands do not match. " \
140
+ "Please try again.\n\n"
141
+ end
142
+ game.add_round(type, dealer, winner, loser, hand)
143
+ break
144
+ when "3"
145
+ # Tenpai
146
+ type = T_TENPAI
147
+ winner = prompt "---> Player(s) in tenpai (separated by space): "
148
+ return if winner == "x"
149
+ winner = winner.split
150
+ next if not game.validate_players(winner)
151
+
152
+ loser = [] # Round::update_round will set losers = all - winners
153
+ hand = []
154
+ game.add_round(type, dealer, winner, loser, hand)
155
+ break
156
+ when "4"
157
+ # Noten
158
+ type = T_NOTEN
159
+ winner = []
160
+ loser = []
161
+ hand = []
162
+ game.add_round(type, dealer, winner, loser, hand)
163
+ break
164
+ when "5"
165
+ # Chombo
166
+ type = T_CHOMBO
167
+ loser = prompt "---> Player who chombo'd: "
168
+ return if loser == "x"
169
+ loser = loser.split
170
+ if loser.length > 1
171
+ puts " Assuming \"%s\" is the player who dealt into winning hand.",
172
+ loser.first
173
+ loser = [loser.first]
174
+ end
175
+ next if not game.validate_players(loser)
176
+
177
+ winner = [] # Round::update_round will set winners = all - loser
178
+ hand = []
179
+ game.add_round(type, dealer, winner, loser, hand)
180
+ break
181
+ when ""
182
+ puts "Enter a choice... >_>"
183
+ puts nil
184
+ else
185
+ printf "Invalid choice: %s\n", choice
186
+ puts nil
187
+ end
188
+ end
189
+
190
+ puts "*** Game scoreboard updated."
191
+ puts nil
192
+ game.print_scoreboard
193
+ end
194
+
195
+ # Update riichi sticks. Sub menu option 2.
196
+ def self.declare_riichi(game)
197
+ puts "(Enter \"x\" to return to game options.)"
198
+ puts nil
199
+ player = prompt "---> Player(s) who declared riichi: "
200
+ player = player.split
201
+ return if not game.validate_players(player)
202
+
203
+ player.each do |p|
204
+ if game.add_riichi(p)
205
+ printf "\n*** Riichi stick added by %s.\n", p
206
+ game.print_current_sticks
207
+ end
208
+ end
209
+ end
210
+
211
+ # Remove last round from scoreboard. Sub menu option 3.
212
+ def self.remove_last_round(game)
213
+ printf "---> Removing last round entry:\n"
214
+ game.print_last_round
215
+ conf = prompt " Are you sure? (y/N) "
216
+ if conf == "y"
217
+ game.remove_last_round
218
+ puts nil
219
+ puts "*** Game scoreboard updated."
220
+ puts nil
221
+ game.print_scoreboard
222
+ return true
223
+ else
224
+ puts "You changed your mind? Fine.\n\n"
225
+ return false
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,192 @@
1
+ require_relative 'game'
2
+ require_relative 'round'
3
+ require_relative 'util'
4
+ require_relative 'game_menu'
5
+
6
+ ##############################################
7
+ # Main menu and top level interactions
8
+ ##############################################
9
+ module Reachy
10
+
11
+ # Main menu
12
+ def self.main_menu
13
+ loop do
14
+ puts "*** Main menu:\n" \
15
+ " 1) View or update existing game scoreboard\n" \
16
+ " 2) Add new game\n" \
17
+ " 3) Delete existing game\n" \
18
+ " 4) Display all scoreboards"
19
+ choice = prompt_ch "---> Enter your choice: "
20
+ puts nil
21
+ case choice
22
+ when "1"
23
+ puts "\n[View or update existing game scoreboard]"
24
+ puts nil
25
+ if view_game then game_menu else puts nil end
26
+ when "2"
27
+ puts "\n[Add new game]"
28
+ puts nil
29
+ if add_game then game_menu else puts nil end
30
+ when "3"
31
+ puts "\n[Delete existing game]"
32
+ puts nil
33
+ delete_game
34
+ when "4"
35
+ puts "\n[Display all scoreboards]"
36
+ puts nil
37
+ display_all_scoreboards
38
+ when ""
39
+ puts "\nEnter a choice... >_>"
40
+ puts nil
41
+ else
42
+ printf "\nInvalid choice: %s\n", choice
43
+ puts nil
44
+ end
45
+ end
46
+ end
47
+
48
+ # Find number of prefix-match game choices
49
+ def self.choice_match
50
+ return (@games[@choice_buf.to_i] ? 1 : 0) if L_NEWLINE.include?(@choice_buf[-1])
51
+ l = (1..@games.length).map{|i| i if /\A#{@choice_buf}\d*\z/.match(i.to_s)}.compact
52
+ ret = l.length
53
+ return ret
54
+ end
55
+
56
+ # View/update an existing game. Main menu option 1.
57
+ def self.view_game
58
+ loop do
59
+ puts "(Enter \"x\" to go back to main menu.)"
60
+ puts nil
61
+ puts "*** Choose existing game:"
62
+ return if not display_all_games
63
+
64
+ @choice_buf = prompt_ch "---> Enter your choice: "
65
+ if @choice_buf == "x"
66
+ puts nil
67
+ return false # to main menu
68
+ elsif L_NEWLINE.include?(@choice_buf)
69
+ puts "\n\nEnter a choice... >_>"
70
+ puts nil
71
+ else
72
+ # Check if current input buffer represents a unique game
73
+ matches = choice_match
74
+ while matches > 1
75
+ @choice_buf += prompt_ch ""
76
+ matches = choice_match
77
+ end
78
+ puts nil
79
+ puts nil
80
+ if choice_match == 0
81
+ printf "Invalid choice: %s\n", @choice_buf
82
+ puts nil
83
+ else
84
+ c = @choice_buf.to_i
85
+ # Print scoreboard for this game
86
+ @games[c - 1].print_scoreboard
87
+ @selected_game_index = c - 1
88
+ return true # to main menu
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # Add a game. Main menu option 2.
95
+ def self.add_game
96
+ puts "(Enter \"x\" to go back to previous menu.)"
97
+ puts nil
98
+
99
+ # Ask for unique game name.
100
+ unique = false
101
+ until unique do
102
+ name = prompt("---> Game name: ", false)
103
+ return false if name == "x"
104
+ next if name.length == 0
105
+ if not /\A\w+\z/.match(name)
106
+ printf "Please enter only alphanumeric characters and underscores.\n"
107
+ next
108
+ end
109
+ unique = true
110
+ @games.each do |game|
111
+ if game.filename == name
112
+ unique = false
113
+ printf "Already exists a game of name: %s!\n", name
114
+ break
115
+ end
116
+ end
117
+ end
118
+
119
+ # Ask for number of players
120
+ good = false
121
+ until good do
122
+ nump = prompt "---> Number of players (3 or 4): "
123
+ return false if nump == "x"
124
+ nump = nump.to_i
125
+ if nump == 3 or nump == 4
126
+ good = true
127
+ else
128
+ puts "Invalid number of players"
129
+ end
130
+ end
131
+
132
+ # Ask for unique player handles
133
+ good = false
134
+ until good do
135
+ players = prompt "---> Player names (separated by spaces, in ESWN order): "
136
+ return false if nump == "x"
137
+ players = players.split
138
+ if players.length == nump and players.uniq.length == players.length
139
+ good = true
140
+ else
141
+ printf "Must input %d unique player handles\n", nump
142
+ end
143
+ end
144
+
145
+ newgame = Game.new(name, false, players)
146
+
147
+ # Add to @games array and go to its menu.
148
+ @games << newgame
149
+ puts "\n*** New game created! Scoreboard:"
150
+ puts nil
151
+ newgame.print_scoreboard
152
+ @selected_game_index = @games.length - 1 # last entry is the new game
153
+ return true
154
+ end
155
+
156
+ # Delete a game. Main menu option 3.
157
+ def self.delete_game
158
+ loop do
159
+ puts "(Enter \"x\" to go back to main menu.)"
160
+ puts nil
161
+ puts "*** Choose existing game to delete:"
162
+ return if not display_all_games
163
+ choice = prompt "---> Enter your choice: "
164
+ case choice
165
+ when "x"
166
+ return # to main menu
167
+ when ""
168
+ puts "\nEnter a choice... >_>"
169
+ else
170
+ # Check that choice consists only of digits and within @games bounds
171
+ if /\A\d+\z/.match(choice) and choice.to_i <= @games.length and choice.to_i > 0
172
+ # Ask for confirmation
173
+ chosen_game = @games[choice.to_i - 1]
174
+ puts nil
175
+ confirm_delete(chosen_game)
176
+ return # to main menu
177
+ else
178
+ printf "Invalid choice: %s\n", choice
179
+ puts nil
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ # Display all scoreboards. Main menu option 4.
186
+ def self.display_all_scoreboards
187
+ @games.each do |game|
188
+ game.print_scoreboard
189
+ end
190
+ end
191
+
192
+ end