Tim Stack
Tim Stack

Reputation: 3248

Logically determine game outcome with formula

I am trying to become a better coder, which includes getting rid of my 'hard-coding' habits to keep my programs dynamic and easy to maintain.

Right now I am writing a simple Rock, Paper, Scissors game as practice:

import time

validoptions = ["rock", "paper", "scissors"]

u1input = input("User 1, do you want to choose rock, paper or scissors?").lower()
if not u1input in(validoptions):
    u1input = input("What the hell User 1, choose a valid option: rock, paper or scissors?").lower()
u2input = input("User 2, do you want to choose rock, paper or scissors?").lower()
if not u2input in(validoptions):
    u2input = input("What the hell User 2, choose a valid option: rock, paper or scissors?").lower()

u1 = validoptions.index(u1input)
u2 = validoptions.index(u2input)

if u1 - u2 == 0:
    result = "It's a draw! Both parties have proven to be of equal strength."


print("Stakes are high... The battle is on... Calculating losses...")
for i in range(1,4):
    time.sleep(1)
    print("...")

time.sleep(1)
print(result)

For such a game like Rock, Paper, Scissors, there aren't many outcomes (6 wins/losses, and 1 draw outcome, or 3^2 individual outcomes). I could easily code all possible outcomes next to the draw outcome I have already coded. However, what if the game expands with 3 more options? Say: Rock, Paper, Scissors, Pans, Swords, and Rifles: that would be 6^2 = 36 outcomes!

As this list expands, the amount of potential outcomes becomes harder to hard-code. I want to use a formula or another 'smart', dynamic method of determining the game's outcome, in a similar fashion to the if u1 - u2 == 0: line.

Is this in any way possible?

Upvotes: 2

Views: 288

Answers (4)

CristiFati
CristiFati

Reputation: 41137

What I initially thought about the Rock–Paper–Scissors (RPS) rules:

  • It's a cyclic relation between the elements where each element beats the one in before it (Scissors beats (cuts) Paper)
    • The 1st element (that doesn't have anything before) beats the last one (and the cycle is complete)
  • When adding more elements, it would only be to allow more users to play (as long as the user count is smaller by 1 than the element count), but now I see that it's wrong as there are cases when the outcome can be undefined (actually the only working case is when there are no 2 players that selected the same option)

Apparently (thanks to [Wikipedia]: Rock–paper–scissors), for a balanced game (odd number of elements):

  • Each element beats half of the other ones (and as a consequence, loses to the other half)

    • The (1st) one before it
    • The 3rd one before it
    • The 5th one before it
    • ...
    • When reaching beginning of the list, jump to its end (wrap-around)

    This is a generalization of the 3 element (RPS) game (and also applies to RPSLS)

Here's what the above rule looks like when put into code (I've also redesigned it to correct some errors in your snippet). All the "magic" happens in outcome.

code00.py:

#!/usr/bin/env python3

import sys


_elements_list = [
    ["Rock", "Paper", "Scissors"],
    ["Rock", "Paper", "Scissors", "Spock", "Lizard"],  # !!! The order is DIFFERENT (RPSSL) than the name of the game: RPSLS !!!
]

elements_dict = {len(item): item for item in _elements_list}
del _elements_list


def get_users_choices(valid_options):
    ret = [-1] * 2
    for i in (0, 1):
        user_choice = None
        while user_choice not in valid_options:
            user_choice = input("Enter user {0:d} option (out of {1:}): ".format(i + 1, valid_options))
        ret[i] = valid_options.index(user_choice)
    return ret


def outcome(idx0, idx1, count):  # Returns -1 when 1st player wins, 0 on draw and 1 when 2nd player wins
    if idx0 == idx1:
        return 0
    index_steps = [-i * 2 - 1 for i in range(count // 2)]  # Index steps (n // 2 items) from current index: {-1, -3, -5, ...} (negative values mean: before)
    idx0_beat_idxes = [(idx0 + i + count) % count for i in index_steps]  # Wrap around when reaching the beginning of the list
    if idx1 in idx0_beat_idxes:
        return -1
    return 1


def main():
    element_count = 3  # Change it to 5 for RPSLS
    if element_count <= 2:
        raise ValueError("Can't play game")
    elements = elements_dict.get(element_count)
    if not elements:
        raise ValueError("Invalid option count")
    choices = get_users_choices(elements)
    res = outcome(*choices, element_count)
    if res == 0:
        print("'{0:s}' and '{1:s}' are DRAW.".format(elements[choices[0]], elements[choices[1]]))
    elif res < 0:
        print("'{0:s}' WINS over '{1:s}'.".format(elements[choices[0]], elements[choices[1]]))
    else:
        print("'{0:s}' LOSES to '{1:s}'.".format(elements[choices[0]], elements[choices[1]]))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main()
    print("\nDone.")

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q057491776]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Enter user 1 option (out of ['Rock', 'Paper', 'Scissors']): Rock
Enter user 2 option (out of ['Rock', 'Paper', 'Scissors']): Scissors
'Rock' WINS over 'Scissors'.

Done.

Upvotes: 1

erdelyia
erdelyia

Reputation: 51

List is a good idea. In your case validoptions = ["rock", "paper", "scissors"] you can see, everything beats the only one before it (the "paper" beats the "rock", the "rock" beats the "scissors" and the "scissors" beats the "paper". So if you sort it that way, it is solvable with the use of the indexes only. If you want to increase the choices, you can, but tke care, only the odd numbers will provide fair game.

In general if you make a list of options, with a length of length, then:

if u1 == u2:
    #it is a draw
elif u2input in validoptions[u1 - int((length-1)/2):u1]:
    #player1 has won
else:
    #player2 has won

Upvotes: 1

Sander
Sander

Reputation: 71

As the rules are not clearly defined, it is not trivial to give a one-size-fits-all-solution. I would probably assume there is some cyclic definition if "win/lose", giving me modulo-calculus such as e.g.:

winner = ["None", "Player 1", "Player 2"]
win_index = (u1 - u2) % len(validoptions)
print("Winner: " + winner[win_index])

Perhaps it is also interesting to take a look at: https://en.wikipedia.org/wiki/Rock%E2%80%93paper%E2%80%93scissors#Additional_weapons.

Upvotes: 1

Lucas Ramos
Lucas Ramos

Reputation: 449

This is really cool! So, I think I would use a dictionary to control what loses to what:

dict_loss = dict()
dict_loss['paper']='scissors'
dict_loss['scissors']='rock'
dict_loss['rock']='paper'

Then the players make a choice and you just check if their choices fall into the dictionaries:

player_1='paper'
player_2='rock'

if player_2 in dict_loss[player_1]:
    print("Player 2 Wins")
else:
    if player_1 in dict_loss[player_2]:
        print("Player 1 Wins")
    else:
        print("DRAW")

You can extend the dictionary with the new objects you get, I'm not sure how Pans, swords and riffles work, but you can do:

dict_loss['paper']=['scissors', 'riffle']

if paper loses to riffles, and so on...

Hope this helps, if you have any "data structure" restrictions let me know and I will try to think of something different.

Upvotes: 1

Related Questions