module Reachy

Main driver functionality

Reachy constants

Scoreboard record class

Game menu and specific game interactions

Main menu and top level interactions

Round record class

Hash objects containing scoring tables

Scoring class

Reachy utilities

Constants

APP_NAME

Path

COL_SPACING

Scoreboard formatting

DATA_PATH
EOF_CH
H_CHOMBO

Hash of chombo scores

H_RON

Hash of ron scores

H_TSUMO

Hash of tsumo scores

L_HANDS

List of named hand values

L_NEWLINE

Special characters

ROOT_PATH
SIGINT_CH
TRASH_PATH
T_CHOMBO
T_NOTEN
T_RESET
T_RON
T_TENPAI
T_TSUMO

Round result types

Public Class Methods

add_game() click to toggle source

Add a game. Main menu option 2.

# File lib/reachy/main_menu.rb, line 95
def self.add_game
  puts "(Enter \"x\" to go back to previous menu.)"
  puts nil

  # Ask for unique game name.
  unique = false
  until unique do
    name = prompt("---> Game name: ", false)
    return false if name == "x"
    next if name.length == 0
    if not /\A\w+\z/.match(name)
      printf "Please enter only alphanumeric characters and underscores.\n"
      next
    end
    unique = true
    @games.each do |game|
      if game.filename == name
        unique = false
        printf "Already exists a game of name: %s!\n", name
        break
      end
    end
  end

  # Ask for number of players
  good = false
  until good do
    nump = prompt "---> Number of players (3 or 4): "
    return false if nump == "x"
    nump = nump.to_i
    if nump == 3 or nump == 4
      good = true
    else
      puts "Invalid number of players"
    end
  end

  # Ask for unique player handles
  good = false
  until good do
    players = prompt "---> Player names (separated by spaces, in ESWN order): "
    return false if nump == "x"
    players = players.split
    if players.length == nump and players.uniq.length == players.length
      good = true
    else
      printf "Must input %d unique player handles\n", nump
    end
  end

  newgame = Game.new(name, false, players)

  # Add to @games array and go to its menu.
  @games << newgame
  puts "\n*** New game created! Scoreboard:"
  puts nil
  newgame.print_scoreboard
  @selected_game_index = @games.length - 1 # last entry is the new game
  return true
end
add_round(game) click to toggle source

Add a new round to the current game. Sub menu option 1.

# File lib/reachy/game_menu.rb, line 71
def self.add_round(game)
  puts "(Enter \"x\" to return to game options.)"
  puts nil
  dealer = prompt "---> Dealer's name: "
  return if dealer == "x"
  puts nil

  loop do
    printf "*** Round result type:\n" \
      "  1) Tsumo\n" \
      "  2) Ron\n" \
      "  3) Tenpai\n" \
      "  4) Noten\n" \
      "  5) Chombo\n" \
      "  6) Round reset\n"
    choice = prompt "---> Select round result: "
    case choice
    when "x"
      puts nil
      return
    when "1"
      # Tsumo
      type = T_TSUMO
      winner = prompt "---> Winner's name: "
      return if winner == "x"
      winner = winner.split
      if winner.length > 1
        puts "  Assuming \"%s\" is the winner, ignoring remaining players.",
          winner.first
        winner = [winner.first]
      end
      next if not game.validate_players(winner)

      hand = prompt "---> Hand value (e.g. \"2 30\" or \"mangan\"): "
      return if hand == "x"
      hand = validate_hand(hand)

      loser = []  # Round::update_round will set loser = all - winner
      game.add_round(type, dealer, winner, loser, hand)
      break
    when "2"
      # Ron
      type = T_RON
      puts nil
      winner = prompt "---> Winner(s) (first winner gets bonus and riichi sticks): "
      return if winner == "x"
      winner = winner.split
      next if not game.validate_players(winner)

      hand = prompt "---> Hand value(s) (e.g. \"2 30 yakuman\" or \"mangan\"): "
      puts nil
      return if hand == "x"
      hand = validate_hand(hand)
      if hand.length != winner.length
        printf "The number of winners and winning hands do not match. " \
               "Please try again.\n\n"
      end

      loser = prompt "---> Player who dealt into winning hand(s): "
      return if loser == "x"
      loser = loser.split
      if loser.length > 1
        puts "  Assuming \"%s\" is the player who dealt into winning hand.",
          loser.first
        loser = [loser.first]
      end
      next if not game.validate_players(loser)
      if winner.include? loser.first
        puts "Loser can't be a winner..."
        next
      end

      game.add_round(type, dealer, winner, loser, hand)
      break
    when "3"
      # Tenpai
      type = T_TENPAI
      winner = prompt "---> Player(s) in tenpai (separated by space): "
      return if winner == "x"
      winner = winner.split
      next if not game.validate_players(winner)

      loser = []  # Round::update_round will set losers = all - winners
      hand = []
      game.add_round(type, dealer, winner, loser, hand)
      break
    when "4"
      # Noten
      type = T_NOTEN
      winner = []
      loser = []
      hand = []
      game.add_round(type, dealer, winner, loser, hand)
      break
    when "5"
      # Chombo
      type = T_CHOMBO
      loser = prompt "---> Player who chombo'd: "
      return if loser == "x"
      loser = loser.split
      if loser.length > 1
        puts "  Assuming \"%s\" is the player who dealt into winning hand.",
          loser.first
        loser = [loser.first]
      end
      next if not game.validate_players(loser)

      winner = [] # Round::update_round will set winners = all - loser
      hand = []
      game.add_round(type, dealer, winner, loser, hand)
      break
    when "6"
      # Round reset
      type = T_RESET
      winner = []
      loser = []
      hand = []
      game.add_round(type, dealer, winner, loser, hand)
      puts "*** Round reset."
      puts nil
      break
    when ""
      puts "Enter a choice... >_>"
      puts nil
    else
      printf "Invalid choice: %s\n", choice
      puts nil
    end
  end

  puts "*** Game scoreboard updated."
  puts nil
  game.print_scoreboard
end
choice_match() click to toggle source

Find number of prefix-match game choices

# File lib/reachy/main_menu.rb, line 49
def self.choice_match
  return (@games[@choice_buf.to_i] ? 1 : 0) if L_NEWLINE.include?(@choice_buf[-1])
  l = (1..@games.length).map{|i| i if /\A#{@choice_buf}\d*\z/.match(i.to_s)}.compact
  ret = l.length
  return ret
end
confirm_delete(chosen_game) click to toggle source

Confirm game deletion

# File lib/reachy/util.rb, line 67
def self.confirm_delete(chosen_game)
  printf "---> Deleting game \"%s\". This action cannot be undone.\n", chosen_game.filename
  conf = prompt "  Are you sure? (y/N) "
  if conf == "y"
    # Move associated json file to trash.
    chosen_game.delete_from_disk
    # Delete from @games array
    @games.delete(chosen_game)
    printf "*** Game \"%s\" deleted from database.\n\n", chosen_game.filename
    return true
  else
    puts "You changed your mind? Fine.\n\n"
    return false
  end
end
cowsay() click to toggle source

Call system cowsay if available

# File lib/reachy/util.rb, line 144
def self.cowsay
  system("cowsay Bye!") if system("which cowsay >/dev/null 2>&1")
end
declare_riichi(game) click to toggle source

Update riichi sticks. Sub menu option 2.

# File lib/reachy/game_menu.rb, line 207
def self.declare_riichi(game)
  puts "(Enter \"x\" to return to game options.)"
  puts nil
  player = prompt "---> Player(s) who declared riichi: "
  player = player.split
  return if not game.validate_players(player)

  player.each do |p|
    if game.add_riichi(p)
      printf "\n*** Riichi stick added by %s.\n", p
      game.print_current_sticks
    end
  end
end
delete_game() click to toggle source

Delete a game. Main menu option 3.

# File lib/reachy/main_menu.rb, line 157
def self.delete_game
  loop do
    puts "(Enter \"x\" to go back to main menu.)"
    puts nil
    puts "*** Choose existing game to delete:"
    return if not display_all_games
    choice = prompt "---> Enter your choice: "
    case choice
    when "x"
      return # to main menu
    when ""
      puts "\nEnter a choice... >_>"
    else
      # Check that choice consists only of digits and within @games bounds
      if /\A\d+\z/.match(choice) and choice.to_i <= @games.length and choice.to_i > 0
        # Ask for confirmation
        chosen_game = @games[choice.to_i - 1]
        puts nil
        confirm_delete(chosen_game)
        return # to main menu
      else
        printf "Invalid choice: %s\n", choice
        puts nil
      end
    end
  end
end
display_all_games() click to toggle source

Print out all games in database

# File lib/reachy/util.rb, line 53
def self.display_all_games
  if @games.empty?
    puts "  No game currently in database. Please add a new game."
    puts nil
    return false
  end
  @games.each_with_index do |game, index|
    printf "  %d) ", index + 1
    game.print_title
  end
  return true
end
display_all_scoreboards() click to toggle source

Display all scoreboards. Main menu option 4.

# File lib/reachy/main_menu.rb, line 186
def self.display_all_scoreboards
  @games.each do |game|
    game.print_scoreboard
  end
end
display_all_winners() click to toggle source

Display winners of every game

# File lib/reachy/util.rb, line 132
def self.display_all_winners
  puts " Current winners:"
  puts " ----------------"
  @games.each do |game|
    high_score = game.scoreboard.last.scores.values.max
    winners = game.scoreboard.last.scores.select{ |player, score| score == high_score}
    printf "  * %s: %s - %d points\n", game.filename, winners.keys.join(", "), high_score
  end
  puts nil
end
ensure_data_dir() click to toggle source

Ensure that the data and data/trash directory exist, if not create them

# File lib/reachy/util.rb, line 84
def self.ensure_data_dir
  if not File.directory?(ROOT_PATH)
    Dir.mkdir ROOT_PATH
  end
  if not File.directory?(DATA_PATH)
    Dir.mkdir DATA_PATH
  end
  if not File.directory?(TRASH_PATH)
    Dir.mkdir TRASH_PATH
  end
end
game_menu() click to toggle source

Game menu for a particular game

# File lib/reachy/game_menu.rb, line 11
def self.game_menu
  loop do
    game = @games[@selected_game_index]
    puts "(Enter \"x\" to go back to main menu.)\n"
    puts nil
    printf "*** Game \"%s\" Options:\n" \
      "  1) Add next round result\n" \
      "  2) Declare riichi\n" \
      "  3) View current scoreboard\n" \
      "  4) Remove last round entry\n" \
      "  5) Delete current game\n" \
      "  6) Choose a different game\n" \
      "  7) Add new game\n", game.filename
    choice = prompt_ch "---> Enter your choice: "
    puts nil
    case choice
    when "x"
      puts nil
      return # to main menu
    when "1"
      puts "\n[Add next round result]"
      puts nil
      add_round(game)
    when "2"
      puts "\n[Declare riichi]"
      puts nil
      declare_riichi(game)
    when "3"
      puts "\n[View current scoreboard]"
      puts nil
      game.print_scoreboard
      puts "(Press any key to continue)"
      STDIN.getch
    when "4"
      puts "\n[Remove last round entry]"
      puts nil
      remove_last_round(game)
    when "5"
      puts "\n[Delete current game]"
      puts nil
      return if confirm_delete(game) # main menu if current game deleted
    when "6"
      puts "\n[Choose a different game]"
      puts nil
      view_game
    when "7"
      puts "\n[Add new game]"
      puts nil
      add_game
    when ""
      puts "\nEnter a choice... >_>"
      puts nil
    else
      printf "\nInvalid choice: %s\n", choice
      puts nil
    end
  end
end
goodbye() click to toggle source

Message to print when quitting program

# File lib/reachy/util.rb, line 149
def self.goodbye
  puts "\n\n"
  printf "  -------------------------------\n" \
    "  |  Thanks for flying reachy!  |\n" \
    "  -------------------------------\n\n"
  display_all_winners
  cowsay
  exit 0
end
main_menu() click to toggle source

Main menu

prompt(message, downcase=true) click to toggle source

Prompt for user input with a message. If EOF is entered, aborts program. Param: message - string to display

downcase - whether to downcase input

Return: User input Note: always strips input!

# File lib/reachy/util.rb, line 16
def self.prompt(message, downcase=true)
  print message
  input = $stdin.gets
  goodbye if !input
  if downcase
    return input.strip.downcase
  else
    return input.strip
  end
end
prompt_ch(message) click to toggle source

Prompt for one character, downcased. If EOF is entered, aborts program. Param: message - string to display Return: User input character, downcased

# File lib/reachy/util.rb, line 31
def self.prompt_ch(message)
  print message
  input = STDIN.getch
  goodbye if input == SIGINT_CH || input == EOF_CH
  print input
  return input.downcase
end
read_all_games() click to toggle source

Read all games in data dir, and store in @games array

# File lib/reachy/util.rb, line 40
def self.read_all_games
  @games = []
  Dir.foreach(DATA_PATH) do |filename|
    # Skip . and .. dir entries, and trash dir
    next if filename == '.' or filename == '..' or filename == "trash"

    # Create game objects
    game = Game.new(filename)
    @games << game
  end
end
remove_last_round(game) click to toggle source

Remove last round from scoreboard. Sub menu option 3.

# File lib/reachy/game_menu.rb, line 223
def self.remove_last_round(game)
  printf "---> Removing last round entry:\n"
  game.print_last_round
  conf = prompt "  Are you sure? (y/N) "
  if conf == "y"
    game.remove_last_round
    puts nil
    puts "*** Game scoreboard updated."
    puts nil
    game.print_scoreboard
    return true
  else
    puts "You changed your mind? Fine.\n\n"
    return false
  end
end
start_screen() click to toggle source

Display initial screen (complete with banner, list of games)

# File lib/reachy.rb, line 17
def self.start_screen
  # Display banner
  File.open(File.expand_path("../banner", __FILE__), "r"){ |file| puts file.read }
  puts nil

  # Display all games in db
  ensure_data_dir
  read_all_games
  puts "*** Current existing game(s) in database:"
  puts nil if display_all_games

  # Display main menu options
  main_menu
end
validate_hand(hand) click to toggle source

Validate hand input Param: hand - string of hand value input Return: reformated hand value or empty list if input invalid

# File lib/reachy/util.rb, line 99
def self.validate_hand(hand)
  split_hand = hand.split
  hand = []
  i = 0
  flag = true
  while i < split_hand.length   # Did this C-style AKA imperatively.. how to ruby
    if split_hand[i].match(/^\d+$/)
      han = split_hand[i]
      fu = split_hand[i+1]
      if fu.match(/^\d+$/) && (fu.to_i==25 || fu.to_i%10 == 0)
        hand << [han.to_i, fu.to_i]
        i += 2
      else
        flag = false
        hand = []
        break
      end
    elsif L_HANDS.include?(split_hand[i])
      hand << [split_hand[i]]
      i += 1
    else
      flag = false
      hand = []
      break
    end
  end
  if not flag
    printf "Hand value malformed: \"%s\"\n", hand
  end
  return hand
end
view_game() click to toggle source

View/update an existing game. Main menu option 1.

# File lib/reachy/main_menu.rb, line 57
def self.view_game
  loop do
    puts "(Enter \"x\" to go back to main menu.)"
    puts nil
    puts "*** Choose existing game:"
    return if not display_all_games

    @choice_buf = prompt_ch "---> Enter your choice: "
    if @choice_buf == "x"
      puts nil
      return false # to main menu
    elsif L_NEWLINE.include?(@choice_buf)
      puts "\n\nEnter a choice... >_>"
      puts nil
    else
      # Check if current input buffer represents a unique game
      matches = choice_match
      while matches > 1
        @choice_buf += prompt_ch ""
        matches = choice_match
      end
      puts nil
      puts nil
      if choice_match == 0
        printf "Invalid choice: %s\n", @choice_buf
        puts nil
      else
        c = @choice_buf.to_i
        # Print scoreboard for this game
        @games[c - 1].print_scoreboard
        @selected_game_index = c - 1
        return true # to main menu
      end
    end
  end
end