reachy 1.1

Sign up to get free protection for your applications and to get access to all the features.
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