The previous failed attempt
This question derives from this other question I made. In the previous question I tried to use a Monte Carlo algorithm; sadly it did not work.
I am doing an hardcoded AI
I therefore decided to try an hardcoded AI, but not for 21 game because it would have been too trivial, for a more general game similar to it. With the default settings you see below the game is equal to the 21 game.
Starting assumption
This AI was built completely by me, knowing from the start only that in the 21 game to play the best possible you have to always make the number dividble by 4. Ex (2 -> 4) (13 -> 16). I tried to come up with a more general answer.
Sometimes plays well sometimes not
This AI mysteriously plays well only in some cases, (example: END_VALUE = 21 MINIMUM_PLAYABLE_NUMBER = 1 MAX_PLAYABLE_NUMBER = 4
).
Coding style part
If you know little about AI's and Math games, feel free to comment only about my coding style.
import random
END_VALUE = 21
MINIMUM_PLAYABLE_NUMBER = 1
MAX_PLAYABLE_NUMBER = 3
INTRODUCTION = """
In this game you and the computer
alternate counting up to """ + str(END_VALUE) + """ chosing a number
from """+ str(MINIMUM_PLAYABLE_NUMBER) + """ to """ + str(MAX_PLAYABLE_NUMBER) + """.
The first that arrives to """ + str(END_VALUE) + """ LOSES.
"""
def dividers(n):
"""
Given a number n returns all its dividers from 2 to the number
>>> dividers(46)
[2, 23, 46]
>>> dividers(100)
[2, 4, 5, 10, 20, 25, 50, 100]
"""
return [i for i in range(2,n+1) if n%i==0]
def max_divider_less_than_or_equal_max_playable_number(n):
"""
Core component of the AI.
Tries to get the max divider less than MAX_PLAYABLE_NUMBER.
If fails plays random.
>>> MAX_PLAYABLE_NUMBER = 6
>>> max_divider_less_than_or_equal_max_playable_number(65)
5
>>> MAX_PLAYABLE_NUMBER = 8
>>> max_divider_less_than_or_equal_max_playable_number(17)
[random] # note that 1 is not included in the dividers
"""
try:
return max([i for i in dividers(n) if i <= MAX_PLAYABLE_NUMBER])
except ValueError: # There are no divisors
return random.randint(MINIMUM_PLAYABLE_NUMBER,MAX_PLAYABLE_NUMBER)
def AI(current_value):
"""
Plays a move trying to make the user have to play
at END_VALUE - 1 so that the player loses and he wins.
He does so by playing the so that
(current_value + move) % max_divider_less_ \
than_or_equal_max_playable_number(END_VALUE - 1) == 0:
is True.
This AI misteriously works only for some values of the
constants at the start.
"""
moves = range(MINIMUM_PLAYABLE_NUMBER,MAX_PLAYABLE_NUMBER + 1)
for move in moves:
if (current_value + move) % max_divider_less_than_or_equal_max_playable_number(END_VALUE - 1) == 0:
return move
return random.randint(1,MAX_PLAYABLE_NUMBER)
def get_input_and_handle_errors():
"""
Asks the user for input.
Checks if it is a number, if it isn't raises error with message.
Checks if it is in the correct range, sif it isn't raises error with message.
"""
try:
temp = int(input("Enter a number "))
except ValueError:
raise TypeError("You must enter a number.")
if MINIMUM_PLAYABLE_NUMBER <= temp <= MAX_PLAYABLE_NUMBER:
player_move = temp
else:
raise ValueError("You must play a number from " + str(MINIMUM_PLAYABLE_NUMBER) + " to " + str(MAX_PLAYABLE_NUMBER) + " both limits included.")
return temp
def show_information_after_turn(value,who):
print("After " + who + "'s turn value is " + str(value))
def check_if_game_over(value):
if value >= END_VALUE:
return True
def ask_user_if_he_wants_to_be_first():
temp = input("Do you want to be first? ")
# I check "y" in temp.lower()
# instead of temp.lower() == "yes"
# To give more freedom to the user
if "y" in temp.lower():
return True
elif "n" in temp.lower():
return False
def main():
"""
Main interface.
"""
value = 0
print(INTRODUCTION)
player_is_first = ask_user_if_he_wants_to_be_first()
while 1:
if not player_is_first:
#############################################
# Computer's turn
value += AI(value)
show_information_after_turn(value,"computer")
if check_if_game_over(value):
print("Computer has lost")
break
player_is_first = False
##############################################
# Player's turn
player_move = get_input_and_handle_errors()
value += player_move
show_information_after_turn(value,"player")
if check_if_game_over(value):
print("You have lost")
break
if __name__ == "__main__":
main()
seed
it for testing). \$\endgroup\$ – jonrsharpe Nov 3 '14 at 21:33