From d0c124a31257fef31d319ed33f166e8f4c32e87b Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sat, 8 Jan 2022 20:11:47 +0000 Subject: [PATCH 1/7] Initial port Game works, but AI behaves differently to the original. Need to try and remove the globals. --- 04_Awari/ruby/README.md | 2 +- 04_Awari/ruby/awari.rb | 286 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 04_Awari/ruby/awari.rb diff --git a/04_Awari/ruby/README.md b/04_Awari/ruby/README.md index fb32811e..9c8c96bf 100644 --- a/04_Awari/ruby/README.md +++ b/04_Awari/ruby/README.md @@ -1,3 +1,3 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) -Conversion to [Ruby](https://www.ruby-lang.org/en/) +Conversion to [Ruby](https://www.ruby-lang.org/en/) by [Alex Scown](https://github.com/TheScown) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb new file mode 100644 index 00000000..aaf29632 --- /dev/null +++ b/04_Awari/ruby/awari.rb @@ -0,0 +1,286 @@ +require 'strscan' + +def print_beans(beans) + puts + + # Print computer beans + print " " + beans[7..12].reverse.each {|bean_count| print_bean(bean_count)} + puts + + # Print home beans + print_bean(beans[13]) + print ' ' + print_number(beans[6]) # This is not print_bean in line with the original version + puts + + # Print player beans + print " " + beans[0..5].each {|bean_count| print_bean(bean_count)} + puts + + puts +end + +def print_bean(bean_count) + print ' ' if bean_count < 10 + print_number(bean_count) +end + +def print_number(n) + # PRINT adds padding after a number and before a positive number + print ' ' if n >= 0 + print n.to_s + print ' ' +end + +def get_move(prompt, beans) + move = get_integer_input(prompt) + + while move < 1 || move > 6 || beans[move - 1] == 0 + puts "ILLEGAL MOVE" + move = get_integer_input("AGAIN") + end + + move - 1 +end + +def get_integer_input(prompt) + integer_value = nil + + input_values = input(prompt) + + while integer_value.nil? + print '!EXTRA INPUT IGNORED' if (input_values.size > 1) + + value = input_values.first + + begin + integer_value = Integer(value) + rescue + puts '!NUMBER EXPECTED - RETRY INPUT LINE' + input_values = input('') + end + end + + integer_value +end + +def input(prompt) + prompt_suffix = '? ' + print "#{prompt}#{prompt_suffix}" + + input = gets.chomp.strip + scanner = StringScanner.new(input) + input_values = [] + + until scanner.eos? + scanner.scan(/\s+/) + + if scanner.check(/"/) + scanner.scan(/"/) + next_string = scanner.scan_until(/"/) + + if next_string + # Remove the trailing close quote + next_string.chomp!('"') + else + # No close quote – Vintage Basic crashes in this case + raise ArgumentError('Unmatched quotes in input') + end + elsif scanner.exist?(/,/) + next_string = scanner.scan_until(/,/).chomp(',') + else + next_string = scanner.scan_until(/\s+|$/).rstrip + end + + input_values << next_string + end + + input_values << '' if input_values.empty? + + input_values +end + + +def distribute_beans(beans, start_pit, home_pit) + beans_to_distribute = beans[start_pit] + beans[start_pit] = 0 + + current_pit = start_pit + + (0...beans_to_distribute).each do + current_pit = (current_pit + 1) % beans.size + beans[current_pit] += 1 + end + + # If the last pit was empty before we put a bean in it (and it's not a scoring pit), add beans to score + if beans[current_pit] == 1 && current_pit != 6 && current_pit != 13 && beans[12 - current_pit] != 0 + beans[home_pit] = beans[home_pit] + beans[12 - current_pit] + 1 + beans[current_pit] = 0 + beans[12 - current_pit] = 0 + end + + current_pit +end + +def update_internals(beans) + $k = $k - 7 if $k > 6 + $c = $c + 1 + + $f[$n] = $f[$n] * 6 + $k if $c < 9 + + unless beans[0...6].find { |b| b != 0 } && beans[7...13].find { |b| b != 0 } + $game_over = true + end +end + +# @param [Array] beans +# @param [Integer] move +# @param [Integer] home_pit +def perform_move(beans, move, home_pit) + last_pit = distribute_beans(beans, move, home_pit) + + update_internals(beans) + + last_pit +end + +def end_game(beans) + puts + puts "GAME OVER" + + difference = beans[6] - beans[13] + + if difference < 0 + puts "I WIN BY #{-difference} POINTS" + + return + end + + $n += 1 + + puts "YOU WIN BY #{difference} POINTS" if difference > 0 + puts "DRAWN GAME" if difference == 0 +end + +# @param [Array] beans +def get_computer_move(beans) + d = -99 + home_pit = 13 + beans_copy = beans.dup + + move_to_do = 0 + + (7..12).each do |move_under_test| + if beans[move_under_test] == 0 + next + end + + max_score = 0 + + distribute_beans(beans_copy, move_under_test, home_pit) + + (0...6).each do |i| + next if beans_copy[i] == 0 + + l = beans_copy[i] + i + r = l / 14 + l_mod_14 = l % 14 + + r = beans_copy[12 - l_mod_14] + r if beans_copy[l_mod_14] == 0 && l_mod_14 != 6 && l_mod_14 != 13 + + max_score = r if r > max_score + end + + final_score = beans_copy[13] - beans_copy[6] - max_score + + if $c <=8 + $k = move_under_test + $k = $k - 7 if $k > 6 + end + + (1...$n).each do |i| + final_score = final_score - 2 if $f[$n] * 6 + $k == (($f[i]/6 ** (7-$c)) + 0.1).floor + end + + if final_score >= d + move_to_do = move_under_test + d = final_score + end + end + + last_pit = perform_move(beans, move_to_do, home_pit) + + [move_to_do, last_pit] +end + +puts 'AWARI'.center(80) +puts 'CREATIVE COMPUTING MORRISTOWN, NEW JERSEY'.center(80) + +# Initialise stable variables +$f = Array.new(50) +$n = 0 + +# APPLICATION LOOP +:game +while true + print "\n", "\n" + beans = Array.new(13, 3) + beans[6] = 0 + beans[13] = 0 + + $f[$n] = 0 + $game_over = false + $c = 0 + + until $game_over + print_beans(beans) + + move = get_move("YOUR MOVE", beans) + home_pit = 6 + computer_home_pit = 13 + $k = move + + last_pit = perform_move(beans, move, home_pit) + + print_beans(beans) + + if $game_over + end_game(beans) + next :game + end + + if home_pit == last_pit + second_move = get_move("AGAIN", beans) + $k = second_move + + perform_move(beans, second_move, home_pit) + + print_beans(beans) + + if $game_over + end_game(beans) + next :game + end + end + + computer_move, computer_last_pit = get_computer_move(beans) + print "MY MOVE IS #{computer_move - 6}" + + if $game_over + end_game(beans) + next :game + end + + if computer_last_pit == computer_home_pit + second_computer_move, _ = get_computer_move(beans) + print ",#{second_computer_move - 6}" + + if $game_over + end_game(beans) + next :game + end + end + end +end From 7239ca244ef590a1a6c4d28b143ca9c4f764e401 Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 12:41:49 +0000 Subject: [PATCH 2/7] Fix AI The issue was not resetting the clone array for every test move. Clarify some variable names. Shout out to Flavio Poletti for the comments on the Perl translation. --- 04_Awari/ruby/awari.rb | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index aaf29632..fa149d5f 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -125,7 +125,7 @@ def distribute_beans(beans, start_pit, home_pit) end def update_internals(beans) - $k = $k - 7 if $k > 6 + $k = $k % 7 $c = $c + 1 $f[$n] = $f[$n] * 6 + $k if $c < 9 @@ -168,51 +168,65 @@ end def get_computer_move(beans) d = -99 home_pit = 13 - beans_copy = beans.dup - move_to_do = 0 + chosen_move = 7 - (7..12).each do |move_under_test| - if beans[move_under_test] == 0 - next - end + # Test all possible moves + (7...13).each do |move_under_test| + # Create a copy of the beans to test against + beans_copy = beans.dup - max_score = 0 + # If the move is not legal, skip it + next if beans[move_under_test] == 0 + # Determine the best response the player may make to this move + player_max_score = 0 + + # Make the move under test against the copy distribute_beans(beans_copy, move_under_test, home_pit) + # Test every player response (0...6).each do |i| + # Skip the move if it would be illegal next if beans_copy[i] == 0 - l = beans_copy[i] + i - r = l / 14 - l_mod_14 = l % 14 + # Determine the last + landing_with_overflow = beans_copy[i] + i + # If landing > 13 the player has put a bean in both home pits + player_move_score = (landing_with_overflow > 14) ? 1 : 0 + # Find the actual pit + landing = landing_with_overflow % 14 - r = beans_copy[12 - l_mod_14] + r if beans_copy[l_mod_14] == 0 && l_mod_14 != 6 && l_mod_14 != 13 + # If the landing pit is empty, the player will steal beans + if beans_copy[landing] == 0 && landing != 6 && landing != 13 + player_move_score = beans_copy[12 - landing] + player_move_score + end - max_score = r if r > max_score + # Update the max score if this move is the best player move + player_max_score = player_move_score if player_move_score > player_max_score end - final_score = beans_copy[13] - beans_copy[6] - max_score + # Final score for move is computer score, minus the player's score and any player gains from their best move + final_score = beans_copy[13] - beans_copy[6] - player_max_score if $c <=8 - $k = move_under_test - $k = $k - 7 if $k > 6 + $k = move_under_test % 7 end - (1...$n).each do |i| - final_score = final_score - 2 if $f[$n] * 6 + $k == (($f[i]/6 ** (7-$c)) + 0.1).floor + (0...$n).each do |i| + final_score = final_score - 2 if $f[$n] * 6 + $k == ((Float($f[i])/6 ** (7-$c)) + 0.1).floor end + # Choose the move if it is the best move found so far if final_score >= d - move_to_do = move_under_test + chosen_move = move_under_test d = final_score end end - last_pit = perform_move(beans, move_to_do, home_pit) + last_pit = perform_move(beans, chosen_move, home_pit) - [move_to_do, last_pit] + [chosen_move, last_pit] end puts 'AWARI'.center(80) From 2fa142a246be226b4779e93fef5e7d8e9803f075 Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 12:51:48 +0000 Subject: [PATCH 3/7] Remove k global --- 04_Awari/ruby/awari.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index fa149d5f..466f1c71 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -124,11 +124,11 @@ def distribute_beans(beans, start_pit, home_pit) current_pit end -def update_internals(beans) - $k = $k % 7 +def update_internals(beans, current_move) + k = current_move % 7 $c = $c + 1 - $f[$n] = $f[$n] * 6 + $k if $c < 9 + $f[$n] = $f[$n] * 6 + k if $c < 9 unless beans[0...6].find { |b| b != 0 } && beans[7...13].find { |b| b != 0 } $game_over = true @@ -141,7 +141,7 @@ end def perform_move(beans, move, home_pit) last_pit = distribute_beans(beans, move, home_pit) - update_internals(beans) + update_internals(beans, move) last_pit end @@ -210,11 +210,11 @@ def get_computer_move(beans) final_score = beans_copy[13] - beans_copy[6] - player_max_score if $c <=8 - $k = move_under_test % 7 - end + k = move_under_test % 7 - (0...$n).each do |i| - final_score = final_score - 2 if $f[$n] * 6 + $k == ((Float($f[i])/6 ** (7-$c)) + 0.1).floor + (0...$n).each do |i| + final_score = final_score - 2 if $f[$n] * 6 + k == ((Float($f[i])/6 ** (7-$c)) + 0.1).floor + end end # Choose the move if it is the best move found so far @@ -254,7 +254,6 @@ while true move = get_move("YOUR MOVE", beans) home_pit = 6 computer_home_pit = 13 - $k = move last_pit = perform_move(beans, move, home_pit) @@ -267,7 +266,6 @@ while true if home_pit == last_pit second_move = get_move("AGAIN", beans) - $k = second_move perform_move(beans, second_move, home_pit) From a0fdf42166232e7825cbfd7dcbbb6e496f94b084 Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 13:19:18 +0000 Subject: [PATCH 4/7] Initial conversion to class --- 04_Awari/ruby/awari.rb | 150 ++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 68 deletions(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index 466f1c71..cd57132c 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -5,7 +5,7 @@ def print_beans(beans) # Print computer beans print " " - beans[7..12].reverse.each {|bean_count| print_bean(bean_count)} + beans[7..12].reverse.each { |bean_count| print_bean(bean_count) } puts # Print home beans @@ -16,7 +16,7 @@ def print_beans(beans) # Print player beans print " " - beans[0..5].each {|bean_count| print_bean(bean_count)} + beans[0..5].each { |bean_count| print_bean(bean_count) } puts puts @@ -102,7 +102,6 @@ def input(prompt) input_values end - def distribute_beans(beans, start_pit, home_pit) beans_to_distribute = beans[start_pit] beans[start_pit] = 0 @@ -124,15 +123,11 @@ def distribute_beans(beans, start_pit, home_pit) current_pit end -def update_internals(beans, current_move) +def update_history(current_move) k = current_move % 7 $c = $c + 1 - $f[$n] = $f[$n] * 6 + k if $c < 9 - - unless beans[0...6].find { |b| b != 0 } && beans[7...13].find { |b| b != 0 } - $game_over = true - end + $history[$non_win_count] = $history[$non_win_count] * 6 + k if $c < 9 end # @param [Array] beans @@ -141,11 +136,12 @@ end def perform_move(beans, move, home_pit) last_pit = distribute_beans(beans, move, home_pit) - update_internals(beans, move) + update_history(move) last_pit end +# @return [Boolean] True if the computer did not win def end_game(beans) puts puts "GAME OVER" @@ -158,10 +154,12 @@ def end_game(beans) return end - $n += 1 + $non_win_count += 1 puts "YOU WIN BY #{difference} POINTS" if difference > 0 puts "DRAWN GAME" if difference == 0 + + difference >= 0 end # @param [Array] beans @@ -209,11 +207,11 @@ def get_computer_move(beans) # Final score for move is computer score, minus the player's score and any player gains from their best move final_score = beans_copy[13] - beans_copy[6] - player_max_score - if $c <=8 + if $c < 9 k = move_under_test % 7 - (0...$n).each do |i| - final_score = final_score - 2 if $f[$n] * 6 + k == ((Float($f[i])/6 ** (7-$c)) + 0.1).floor + (0...$non_win_count).each do |i| + final_score = final_score - 2 if $history[$non_win_count] * 6 + k == ((Float($history[i]) / 6 ** (7 - $c)) + 0.1).floor end end @@ -229,70 +227,86 @@ def get_computer_move(beans) [chosen_move, last_pit] end +class Game + def initialize(history, non_win_count) + @beans = Array.new(13, 3) + @beans[6] = 0 + @beans[13] = 0 + + @turn_counter = 0 + + @history = history + @non_win_count = non_win_count + end + + def game_over + @beans[0...6].all? { |b| b == 0 } || @beans[7...13].all? { |b| b == 0 } + end + + def play + until game_over + print_beans(@beans) + + move = get_move("YOUR MOVE", @beans) + home_pit = 6 + computer_home_pit = 13 + + last_pit = perform_move(@beans, move, home_pit) + + print_beans(@beans) + + if game_over + return end_game(@beans) + end + + if home_pit == last_pit + second_move = get_move("AGAIN", @beans) + + perform_move(@beans, second_move, home_pit) + + print_beans(@beans) + + if game_over + return end_game(@beans) + end + end + + computer_move, computer_last_pit = get_computer_move(@beans) + print "MY MOVE IS #{computer_move - 6}" + + if game_over + return end_game(@beans) + end + + if computer_last_pit == computer_home_pit + second_computer_move, _ = get_computer_move(@beans) + print ",#{second_computer_move - 6}" + + if game_over + return end_game(@beans) + end + end + end + end + +end + puts 'AWARI'.center(80) puts 'CREATIVE COMPUTING MORRISTOWN, NEW JERSEY'.center(80) # Initialise stable variables -$f = Array.new(50) -$n = 0 +$history = Array.new(50) +$non_win_count = 0 # APPLICATION LOOP -:game while true print "\n", "\n" - beans = Array.new(13, 3) - beans[6] = 0 - beans[13] = 0 - $f[$n] = 0 - $game_over = false + $history[$non_win_count] = 0 $c = 0 - until $game_over - print_beans(beans) + game = Game.new($history, $non_win_count) - move = get_move("YOUR MOVE", beans) - home_pit = 6 - computer_home_pit = 13 - - last_pit = perform_move(beans, move, home_pit) - - print_beans(beans) - - if $game_over - end_game(beans) - next :game - end - - if home_pit == last_pit - second_move = get_move("AGAIN", beans) - - perform_move(beans, second_move, home_pit) - - print_beans(beans) - - if $game_over - end_game(beans) - next :game - end - end - - computer_move, computer_last_pit = get_computer_move(beans) - print "MY MOVE IS #{computer_move - 6}" - - if $game_over - end_game(beans) - next :game - end - - if computer_last_pit == computer_home_pit - second_computer_move, _ = get_computer_move(beans) - print ",#{second_computer_move - 6}" - - if $game_over - end_game(beans) - next :game - end - end - end + computer_didnt_win = game.play + $non_win_count += 1 if computer_didnt_win end From 5195ef74c673094e2290ea38f3f77b5af7102865 Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 13:55:27 +0000 Subject: [PATCH 5/7] Move remaining functions into class --- 04_Awari/ruby/awari.rb | 453 ++++++++++++++++++++--------------------- 1 file changed, 223 insertions(+), 230 deletions(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index cd57132c..a9864dc7 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -1,32 +1,5 @@ require 'strscan' -def print_beans(beans) - puts - - # Print computer beans - print " " - beans[7..12].reverse.each { |bean_count| print_bean(bean_count) } - puts - - # Print home beans - print_bean(beans[13]) - print ' ' - print_number(beans[6]) # This is not print_bean in line with the original version - puts - - # Print player beans - print " " - beans[0..5].each { |bean_count| print_bean(bean_count) } - puts - - puts -end - -def print_bean(bean_count) - print ' ' if bean_count < 10 - print_number(bean_count) -end - def print_number(n) # PRINT adds padding after a number and before a positive number print ' ' if n >= 0 @@ -34,38 +7,6 @@ def print_number(n) print ' ' end -def get_move(prompt, beans) - move = get_integer_input(prompt) - - while move < 1 || move > 6 || beans[move - 1] == 0 - puts "ILLEGAL MOVE" - move = get_integer_input("AGAIN") - end - - move - 1 -end - -def get_integer_input(prompt) - integer_value = nil - - input_values = input(prompt) - - while integer_value.nil? - print '!EXTRA INPUT IGNORED' if (input_values.size > 1) - - value = input_values.first - - begin - integer_value = Integer(value) - rescue - puts '!NUMBER EXPECTED - RETRY INPUT LINE' - input_values = input('') - end - end - - integer_value -end - def input(prompt) prompt_suffix = '? ' print "#{prompt}#{prompt_suffix}" @@ -102,131 +43,6 @@ def input(prompt) input_values end -def distribute_beans(beans, start_pit, home_pit) - beans_to_distribute = beans[start_pit] - beans[start_pit] = 0 - - current_pit = start_pit - - (0...beans_to_distribute).each do - current_pit = (current_pit + 1) % beans.size - beans[current_pit] += 1 - end - - # If the last pit was empty before we put a bean in it (and it's not a scoring pit), add beans to score - if beans[current_pit] == 1 && current_pit != 6 && current_pit != 13 && beans[12 - current_pit] != 0 - beans[home_pit] = beans[home_pit] + beans[12 - current_pit] + 1 - beans[current_pit] = 0 - beans[12 - current_pit] = 0 - end - - current_pit -end - -def update_history(current_move) - k = current_move % 7 - $c = $c + 1 - - $history[$non_win_count] = $history[$non_win_count] * 6 + k if $c < 9 -end - -# @param [Array] beans -# @param [Integer] move -# @param [Integer] home_pit -def perform_move(beans, move, home_pit) - last_pit = distribute_beans(beans, move, home_pit) - - update_history(move) - - last_pit -end - -# @return [Boolean] True if the computer did not win -def end_game(beans) - puts - puts "GAME OVER" - - difference = beans[6] - beans[13] - - if difference < 0 - puts "I WIN BY #{-difference} POINTS" - - return - end - - $non_win_count += 1 - - puts "YOU WIN BY #{difference} POINTS" if difference > 0 - puts "DRAWN GAME" if difference == 0 - - difference >= 0 -end - -# @param [Array] beans -def get_computer_move(beans) - d = -99 - home_pit = 13 - - chosen_move = 7 - - # Test all possible moves - (7...13).each do |move_under_test| - # Create a copy of the beans to test against - beans_copy = beans.dup - - # If the move is not legal, skip it - next if beans[move_under_test] == 0 - - # Determine the best response the player may make to this move - player_max_score = 0 - - # Make the move under test against the copy - distribute_beans(beans_copy, move_under_test, home_pit) - - # Test every player response - (0...6).each do |i| - # Skip the move if it would be illegal - next if beans_copy[i] == 0 - - # Determine the last - landing_with_overflow = beans_copy[i] + i - # If landing > 13 the player has put a bean in both home pits - player_move_score = (landing_with_overflow > 14) ? 1 : 0 - # Find the actual pit - landing = landing_with_overflow % 14 - - # If the landing pit is empty, the player will steal beans - if beans_copy[landing] == 0 && landing != 6 && landing != 13 - player_move_score = beans_copy[12 - landing] + player_move_score - end - - # Update the max score if this move is the best player move - player_max_score = player_move_score if player_move_score > player_max_score - end - - # Final score for move is computer score, minus the player's score and any player gains from their best move - final_score = beans_copy[13] - beans_copy[6] - player_max_score - - if $c < 9 - k = move_under_test % 7 - - (0...$non_win_count).each do |i| - final_score = final_score - 2 if $history[$non_win_count] * 6 + k == ((Float($history[i]) / 6 ** (7 - $c)) + 0.1).floor - end - end - - # Choose the move if it is the best move found so far - if final_score >= d - chosen_move = move_under_test - d = final_score - end - end - - last_pit = perform_move(beans, chosen_move, home_pit) - - [chosen_move, last_pit] -end - class Game def initialize(history, non_win_count) @beans = Array.new(13, 3) @@ -239,74 +55,251 @@ class Game @non_win_count = non_win_count end + def play + while true + print_beans + + move = get_move("YOUR MOVE") + home_pit = 6 + computer_home_pit = 13 + + last_pit = perform_move(move, home_pit) + + print_beans + + break if game_over + + if home_pit == last_pit + second_move = get_move("AGAIN") + + perform_move(second_move, home_pit) + + print_beans + + break if game_over + end + + computer_move, computer_last_pit = get_computer_move + print "MY MOVE IS #{computer_move - 6}" + + break if game_over + + if computer_last_pit == computer_home_pit + second_computer_move, _ = get_computer_move + print ",#{second_computer_move - 6}" + + break if game_over + end + end + + end_game + end + + private + def game_over @beans[0...6].all? { |b| b == 0 } || @beans[7...13].all? { |b| b == 0 } end - def play - until game_over - print_beans(@beans) + # @return [Boolean] True if the computer did not win + def end_game + puts + puts "GAME OVER" - move = get_move("YOUR MOVE", @beans) - home_pit = 6 - computer_home_pit = 13 + difference = @beans[6] - @beans[13] - last_pit = perform_move(@beans, move, home_pit) + if difference < 0 + puts "I WIN BY #{-difference} POINTS" - print_beans(@beans) - - if game_over - return end_game(@beans) - end - - if home_pit == last_pit - second_move = get_move("AGAIN", @beans) - - perform_move(@beans, second_move, home_pit) - - print_beans(@beans) - - if game_over - return end_game(@beans) - end - end - - computer_move, computer_last_pit = get_computer_move(@beans) - print "MY MOVE IS #{computer_move - 6}" - - if game_over - return end_game(@beans) - end - - if computer_last_pit == computer_home_pit - second_computer_move, _ = get_computer_move(@beans) - print ",#{second_computer_move - 6}" - - if game_over - return end_game(@beans) - end - end + return end + + puts "YOU WIN BY #{difference} POINTS" if difference > 0 + puts "DRAWN GAME" if difference == 0 + + difference >= 0 end + # @param [Integer] move + # @param [Integer] home_pit + def perform_move(move, home_pit) + last_pit = distribute_beans(move, home_pit) + + update_history(move) + + last_pit + end + + def update_history(current_move) + k = current_move % 7 + @turn_counter += 1 + + # Add the move to the history + @history[@non_win_count] = @history[@non_win_count] * 6 + k if @turn_counter < 9 + end + + def print_beans + puts + + # Print computer beans + print ' ' * 3 + @beans[7...13].reverse.each { |bean_count| print_bean(bean_count) } + puts + + # Print home beans + print_bean(@beans[13]) + print ' ' * 23 + print_number(@beans[6]) # This is not print_bean in line with the original version + puts + + # Print player beans + print ' ' * 3 + @beans[0...6].each { |bean_count| print_bean(bean_count) } + puts + + puts + end + + def get_move(prompt) + move = get_integer_input(prompt) + + while move < 1 || move > 6 || @beans[move - 1] == 0 + puts "ILLEGAL MOVE" + move = get_integer_input("AGAIN") + end + + move - 1 + end + + def distribute_beans(start_pit, home_pit, beans = @beans) + beans_to_distribute = beans[start_pit] + beans[start_pit] = 0 + + current_pit = start_pit + + (0...beans_to_distribute).each do + current_pit = (current_pit + 1) % beans.size + beans[current_pit] += 1 + end + + # If the last pit was empty before we put a bean in it (and it's not a scoring pit), add beans to score + if beans[current_pit] == 1 && current_pit != 6 && current_pit != 13 && beans[12 - current_pit] != 0 + beans[home_pit] = beans[home_pit] + beans[12 - current_pit] + 1 + beans[current_pit] = 0 + beans[12 - current_pit] = 0 + end + + current_pit + end + + def print_bean(bean_count) + print ' ' if bean_count < 10 + print_number(bean_count) + end + + def get_integer_input(prompt) + integer_value = nil + + input_values = input(prompt) + + while integer_value.nil? + print '!EXTRA INPUT IGNORED' if (input_values.size > 1) + + value = input_values.first + + begin + integer_value = Integer(value) + rescue + puts '!NUMBER EXPECTED - RETRY INPUT LINE' + input_values = input('') + end + end + + integer_value + end + + # @param [Array] beans + def get_computer_move + d = -99 + home_pit = 13 + + chosen_move = 7 + + # Test all possible moves + (7...13).each do |move_under_test| + # Create a copy of the beans to test against + beans_copy = @beans.dup + + # If the move is not legal, skip it + next if beans_copy[move_under_test] == 0 + + # Determine the best response the player may make to this move + player_max_score = 0 + + # Make the move under test against the copy + distribute_beans(move_under_test, home_pit, beans_copy) + + # Test every player response + (0...6).each do |i| + # Skip the move if it would be illegal + next if beans_copy[i] == 0 + + # Determine the last + landing_with_overflow = beans_copy[i] + i + # If landing > 13 the player has put a bean in both home pits + player_move_score = (landing_with_overflow > 14) ? 1 : 0 + # Find the actual pit + landing = landing_with_overflow % 14 + + # If the landing pit is empty, the player will steal beans + if beans_copy[landing] == 0 && landing != 6 && landing != 13 + player_move_score = beans_copy[12 - landing] + player_move_score + end + + # Update the max score if this move is the best player move + player_max_score = player_move_score if player_move_score > player_max_score + end + + # Final score for move is computer score, minus the player's score and any player gains from their best move + final_score = beans_copy[13] - beans_copy[6] - player_max_score + + if @turn_counter < 9 + k = move_under_test % 7 + + (0...@non_win_count).each do |i| + # Penalise move if it was used in a losing game + final_score = final_score - 2 if @history[@non_win_count] * 6 + k == ((Float(@history[i]) / 6 ** (7 - @turn_counter)) + 0.1).floor + end + end + + # Choose the move if it is the best move found so far + if final_score >= d + chosen_move = move_under_test + d = final_score + end + end + + last_pit = perform_move(chosen_move, home_pit) + + [chosen_move, last_pit] + end end puts 'AWARI'.center(80) puts 'CREATIVE COMPUTING MORRISTOWN, NEW JERSEY'.center(80) # Initialise stable variables -$history = Array.new(50) -$non_win_count = 0 +history = Array.new(50) +non_win_count = 0 # APPLICATION LOOP while true print "\n", "\n" - $history[$non_win_count] = 0 - $c = 0 + history[non_win_count] = 0 - game = Game.new($history, $non_win_count) + game = Game.new(history, non_win_count) computer_didnt_win = game.play - $non_win_count += 1 if computer_didnt_win + non_win_count += 1 if computer_didnt_win end From 10a2f535ebed117a68e8782dfe1edd24d612529e Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 14:12:00 +0000 Subject: [PATCH 6/7] Final tidy --- 04_Awari/ruby/awari.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index a9864dc7..65e27c25 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -1,5 +1,7 @@ require 'strscan' +# Prints a number according to Vintage Basic's PRINT statement +# @param n The number to print def print_number(n) # PRINT adds padding after a number and before a positive number print ' ' if n >= 0 @@ -7,6 +9,9 @@ def print_number(n) print ' ' end +# Mimic the INPUT statement using Vintage Basic as a reference +# @param prompt The prompt to show to the user +# @return An array of strings representing the inputted values def input(prompt) prompt_suffix = '? ' print "#{prompt}#{prompt_suffix}" @@ -55,6 +60,7 @@ class Game @non_win_count = non_win_count end + # @return [Boolean] True if the computer did not win the game def play while true print_beans @@ -218,7 +224,6 @@ class Game integer_value end - # @param [Array] beans def get_computer_move d = -99 home_pit = 13 @@ -294,7 +299,8 @@ non_win_count = 0 # APPLICATION LOOP while true - print "\n", "\n" + puts + puts history[non_win_count] = 0 From 63100a55921e863064de7fe7f7e09c848c13b29d Mon Sep 17 00:00:00 2001 From: Alex Scown Date: Sun, 9 Jan 2022 14:29:39 +0000 Subject: [PATCH 7/7] Fix unmatched quotes error --- 04_Awari/ruby/awari.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/04_Awari/ruby/awari.rb b/04_Awari/ruby/awari.rb index 65e27c25..6c68a4e0 100644 --- a/04_Awari/ruby/awari.rb +++ b/04_Awari/ruby/awari.rb @@ -32,7 +32,7 @@ def input(prompt) next_string.chomp!('"') else # No close quote – Vintage Basic crashes in this case - raise ArgumentError('Unmatched quotes in input') + raise 'Unmatched quotes in input' end elsif scanner.exist?(/,/) next_string = scanner.scan_until(/,/).chomp(',')