2
\$\begingroup\$

I finished the Python Koans because I wanted to practise Python and I completed the extra assignment which is the Greed Game (see the rules here).

I think there are no flaws but you never know.

import random

class DiceSet(object):
    def __init__(self):
        self._values = None

    @property
    def values(self):
        return self._values

    def roll(self, n):
        self._values = []
        for i in range(0, n):
            val = random.randint(1, 6)
            self._values.append(val)
        return self._values

class Player(object):
    def __init__(self, name=""):
        self.name = name

    def __str__(self):
        if self.name:
            return "'{0}'".format(self.name)
        else:
            return "'Anonymous player'"

    def __repr__(self):
        return self.__str__()


class Game(object):
    def __init__(self, players, dice, goal = 3000):
        if len(players) < 2:
            raise "This game is for 2 or more players."
        self.dice = dice
        self.players_scores = dict((player, 0) for player in players)
        self.number_of_players = len(self.players_scores)
        self.players = [player for player in self.players_scores.keys()]
        self.turn = random.randint(0, len(self.players)-1)
        self.goal = goal

    def play(self):
        final_round = False
        first_player_to_reach_firnal_round = None

        while not final_round:
            self._print_turn()
            won_score = self._turn_score()
            self.players_scores[self.players[self.turn]] += won_score
            print "{0} has won {1} points (total {2}).".format(self.players[self.turn], won_score, self.players_scores[self.players[self.turn]])
            if self.players_scores[self.players[self.turn]] >= self.goal:
                final_round = True
                first_player_to_reach_firnal_round = self.turn
                print "{0} has reached over {1} points. Next round will be the last round.".format(self.players[self.turn], self.goal)
            self.turn = (self.turn + 1) % self.number_of_players

        # Last round. The player that reached the goal number of points doesn't play this round.
        print "\n\nLAST ROUND!!! Let's see who wins!"
        while self.turn != first_player_to_reach_firnal_round:
            self._print_turn()
            won_score = self._turn_score()
            self.players_scores[self.players[self.turn]] += won_score
            print "{0} has won {1} points (total {2}).".format(self.players[self.turn], won_score, self.players_scores[self.players[self.turn]])
            self.turn = (self.turn + 1) % self.number_of_players

        winner = self._winner()
        print "\n\nCongratulations, {0}! You have won the Greed Game!".format(winner)

    def score(self, dice):
        result = 0
        if len(dice) <= 5:
            dice_dict = dict((i, dice.count(i)) for i in dice)
            if dice_dict.get(1) >= 3:
                result += 1000
                dice_dict[1] -= 3
            for number in dice_dict:
                if dice_dict.get(number) >= 3:
                    result += number * 100
                    dice_dict[number] -= 3
            if 1 in dice_dict:
                result += dice_dict[1] * 100
                dice_dict[1] = 0
            if 5 in dice_dict:
                result += dice_dict[5] * 50
                dice_dict[5] = 0

        non_scoring_dice = [die for die in dice_dict.keys() if dice_dict[die] != 0]
        return result, non_scoring_dice

    def _turn_score(self):
        total_score = 0
        keep_rolling = False

        dice = self.dice.roll(5)
        print "\n{0} rolls the dice and gets {1}.".format(self.players[self.turn], dice)
        rolling_score, dice = self.score(dice)

        if rolling_score > 0:
            # If dice is empty it means there were no non-scoring dice after the roll, so the player gets the choice to roll all 5 dice again
            number_of_rollable_dice = 5 if not dice else len(dice)
            numbers = { 1: "one", 2: "two", 3: "three", 4: "four", 5: "five" }
            print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, number_of_rollable_dice, "die" if number_of_rollable_dice == 1 else "dice")

            total_score += rolling_score

            player_choice = raw_input("\n{0}, will you roll again? (y/n): ".format(self.players[self.turn]))
            if player_choice.upper() in ["Y", "YES"]:
                keep_rolling = True

            while keep_rolling:
                dice = self.dice.roll(number_of_rollable_dice)
                print "\n{0} rolls the dice and gets {1}.".format(self.players[self.turn], dice)
                rolling_score, dice = self.score(dice)

                if rolling_score > 0:
                    number_of_rollable_dice = 5 if not dice else len(dice)
                    numbers = { 1: "one", 2: "two", 3: "three", 4: "four", 5: "five" }
                    print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, number_of_rollable_dice, "die" if number_of_rollable_dice == 1 else "dice")

                    if total_score + rolling_score >= 300:
                        total_score += rolling_score
                        print "Since you reached more than 300 points this turn, your turn score is now {0}".format(total_score)
                    else:
                        total_score = rolling_score
                        print "Your turn score is less than 300 points, so now it's just the rolling score, {0} points.".format(total_score)

                    player_choice = raw_input("\n{0}, will you roll again? (y/n): ".format(self.players[self.turn]))
                    if player_choice.upper() in ["Y", "YES"]:
                        keep_rolling = True
                    else:
                        keep_rolling = False
                else:
                    print "That's a zero-point roll, you lost your turn and all your won points in this turn."
                    keep_rolling = False
                    total_score = 0
        else:
            print "That's a zero-point roll, you cannot roll again in this turn."

        return total_score

    def _winner(self):
        return max(self.players_scores, key = self.players_scores.get)

    def _print_turn(self):
        print "\n------------------------------------------\nStarting turn of {0} [Score: {1}]\n".format(self.players[self.turn], self.players_scores[self.players[self.turn]])

    def _print_scores(self):
        print "\n------------------------------------------\nScores table\n"
        for player, score in self.players_scores:
            print "{0}: {1} points".format(player, score)

player1 = Player("Player 1")
player2 = Player("Player 2")
players = [ player1, player2 ]
dice = DiceSet()
game = Game(players, dice, 1500)
game.play()

I am looking for comments and answers in these points:

  • Is the class hierarchy acceptable?
  • Are the methods in the correct classes?
  • Should I have arranged the logic differently?
  • Comments and tips on my code.
  • Suggestions of Python-style ways of doing this
  • Improvement and optimizations of my code
  • Ideas of how to implement this in more interesting, original or interesting ways
\$\endgroup\$

1 Answer 1

8
\$\begingroup\$

About DiceSet

It seems like the values property is never used. Let's get rid of it for the time being. Now, what do we have ? A class with 2 methods one of which is an __init__. I suggest you have a look at this talk called "Stop Writing Classes ". You can easily write this as a simple function :

def roll(n):
    values = []
    for i in range(0, n):
        val = random.randint(1, 6)
        values.append(val)
    return values

It makes everything much simpler. In particular, you don't need to create an instance of DiceSet and you don't need to keep it in your Game object. So much win.

More details :

  • you don't need a val variable.

  • you don't need the 0 as a first parameter to range.

  • it is the convention to name _ throw-away values.

  • this looks like a situation where you could use a list comprehension.

Your whole DiceSet class is now down to this method :

def roll(n):
    return [random.randint(1, 6) for _ in range(n)]

About score

The score method doesn't seem to access self at all. You could make this a classmethod or a simple independant function.

About play

There is a lot of duplicated code here. You can remove a lot out of it by thinking about what the ending condition for the game is : it is when we go back to the first player who has reached the final round (if any). You can simply write this :

def play(self):
    first_player_to_reach_final_round = None

    while self.turn != first_player_to_reach_final_round:
        self._print_turn()
        won_score = self._turn_score()
        self.players_scores[self.players[self.turn]] += won_score
        print "{0} has won {1} points (total {2}).".format(self.players[self.turn], won_score, self.players_scores[self.players[self.turn]])
        if self.players_scores[self.players[self.turn]] >= self.goal:
            if first_player_to_reach_final_round is None:
                first_player_to_reach_final_round = self.turn
            print "{0} has reached over {1} points. Next round will be the last round.".format(self.players[self.turn], self.goal)
            print "\n\nLAST ROUND!!! Let's see who wins!"
        self.turn = (self.turn + 1) % self.number_of_players

    winner = self._winner()
    print "\n\nCongratulations, {0}! You have won the Greed Game!".format(winner)

About self.turn

As far as I can tell, self.turn is interesting only in the play method. You can propagage the relevant information (name and/or score) to the other method needing it.

By doing this, you can again make things a lot simpler by just having a turn variable in play.

Extracting user interaction in separated functions

It might be a good idea to extract the user interactions in separated functions. It makes input checking easier and make the game logic clearer.

Here's what I have written :

def ask_yes_no_question(prompt):
    user_input = raw_input(prompt)
    return user_input.upper() in ["Y", "YES"]

def ask_roll_again(player):
    return ask_yes_no_question("\n{0}, will you roll again? (y/n): ".format(player))

It's simple and clear.

About turn_score

There is still quite a lot going on here. The logic number_of_rollable_dice = 5 if not dice else len(dice) is duplicated looks like maybe it shoud be part of the score function (telling how many dice are left to play). Also, it could be a good idea to have a constant containg 5 to make things less confusing.

At this point, the code looks like :

import random

DEFAULT_DICE_NUMBER = 5

def roll(n):
    return [random.randint(1, 6) for _ in range(n)]

def score(dice):
    result = 0
    if len(dice) <= 5:
        dice_dict = dict((i, dice.count(i)) for i in dice)
        if dice_dict.get(1) >= 3:
            result += 1000
            dice_dict[1] -= 3
        for number in dice_dict:
            if dice_dict.get(number) >= 3:
                result += number * 100
                dice_dict[number] -= 3
        if 1 in dice_dict:
            result += dice_dict[1] * 100
            dice_dict[1] = 0
        if 5 in dice_dict:
            result += dice_dict[5] * 50
            dice_dict[5] = 0

    remaining_dice = len([die for die in dice_dict.keys() if dice_dict[die] != 0])
    remaining_dice = remaining_dice if remaining_dice else DEFAULT_DICE_NUMBER
    return result, remaining_dice


def print_turn(player, score):
    print "\n------------------------------------------\nStarting turn of {0} [Score: {1}]\n".format(player, score)


def ask_yes_no_question(prompt):
    user_input = raw_input(prompt)
    return user_input.upper() in ["Y", "YES"]

def ask_roll_again(player):
    return ask_yes_no_question("\n{0}, will you roll again? (y/n): ".format(player))


class Player(object):
    def __init__(self, name=""):
        self.name = name

    def __str__(self):
        if self.name:
            return "'{0}'".format(self.name)
        else:
            return "'Anonymous player'"

    def __repr__(self):
        return self.__str__()


class Game(object):
    def __init__(self, players, goal = 3000):
        if len(players) < 2:
            raise "This game is for 2 or more players."
        self.players_scores = dict((player, 0) for player in players)
        self.number_of_players = len(self.players_scores)
        self.players = [player for player in self.players_scores.keys()]
        self.goal = goal

    def play(self):
        first_player_to_reach_final_round = None
        turn = random.randint(0, len(self.players)-1)

        while turn != first_player_to_reach_final_round:
            player = self.players[turn]
            score = self.players_scores[player]
            print_turn(player, score)
            won_score = self._turn_score(player)
            self.players_scores[player] += won_score
            print "{0} has won {1} points (total {2}).".format(player, won_score, self.players_scores[player])
            if self.players_scores[player] >= self.goal:
                if first_player_to_reach_final_round is None:
                    first_player_to_reach_final_round = turn
                print "{0} has reached over {1} points. Next round will be the last round.".format(player, self.goal)
                print "\n\nLAST ROUND!!! Let's see who wins!"
            turn = (turn + 1) % self.number_of_players

        winner = self._winner()
        print "\n\nCongratulations, {0}! You have won the Greed Game!".format(winner)

    def _turn_score(self, player):
        total_score = 0
        keep_rolling = False

        dice = roll(DEFAULT_DICE_NUMBER)
        print "\n{0} rolls the dice and gets {1}.".format(player, dice)
        rolling_score, nb_dice = score(dice)

        if rolling_score > 0:
            # If dice is empty it means there were no non-scoring dice after the roll, so the player gets the choice to roll all 5 dice again
            numbers = { 1: "one", 2: "two", 3: "three", 4: "four", 5: "five" }
            print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, nb_dice, "die" if nb_dice == 1 else "dice")

            total_score += rolling_score

            keep_rolling = ask_roll_again(player) 

            while keep_rolling:
                dice = roll(nb_dice)
                print "\n{0} rolls the dice and gets {1}.".format(player, dice)
                rolling_score, nb_dice = score(dice)

                if rolling_score > 0:
                    numbers = { 1: "one", 2: "two", 3: "three", 4: "four", 5: "five" }
                    print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, nb_dice, "die" if nb_dice == 1 else "dice")

                    if total_score + rolling_score >= 300:
                        total_score += rolling_score
                        print "Since you reached more than 300 points this turn, your turn score is now {0}".format(total_score)
                    else:
                        total_score = rolling_score
                        print "Your turn score is less than 300 points, so now it's just the rolling score, {0} points.".format(total_score)

                    keep_rolling = ask_roll_again(player)
                else:
                    print "That's a zero-point roll, you lost your turn and all your won points in this turn."
                    keep_rolling = False
                    total_score = 0
        else:
            print "That's a zero-point roll, you cannot roll again in this turn."

        return total_score

    def _winner(self):
        return max(self.players_scores, key = self.players_scores.get)

    def _print_scores(self):
        print "\n------------------------------------------\nScores table\n"
        for player, score in self.players_scores:
            print "{0}: {1} points".format(player, score)

player1 = Player("Player 1")
player2 = Player("Player 2")
players = [ player1, player2 ]
game = Game(players, 1500)
game.play()

About turn_score again

There is still too much going on here. Also, many things seem to be happening in different places : rolling dices, printing, asking user if he wants to roll, etc.

You can rewrite the logic in a much concise and clearer way :

def _turn_score(self, player):
    total_score = 0
    nb_dice = DEFAULT_DICE_NUMBER

    while True:
        dice = roll(nb_dice)
        print "\n{0} rolls the dice and gets {1}.".format(player, dice)
        rolling_score, nb_dice = score(dice)
        if rolling_score == 0:
            if total_score:
                print "That's a zero-point roll, you lost your turn and all your won points in this turn."
            else:
                print "That's a zero-point roll, you cannot roll again in this turn."
            return 0
        else:
            print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, nb_dice, "die" if nb_dice == 1 else "dice")
            if total_score + rolling_score >= 300:
                total_score += rolling_score
                print "Since you reached more than 300 points this turn, your turn score is now {0}".format(total_score)
            else:
                total_score = rolling_score
                print "Your turn score is less than 300 points, so now it's just the rolling score, {0} points.".format(total_score)
            if not ask_roll_again(player):
                return total_score

You don't need a class (again)

At the end, the Game class is not needed anymore. Everything can happen from the play function.

It would go like this :

import random

DEFAULT_DICE_NUMBER = 5
NB_POINTS_TO_START = 300

class Player(object):
    def __init__(self, name=""):
        self.name = name

    def __str__(self):
        if self.name:
            return "'{0}'".format(self.name)
        else:
            return "'Anonymous player'"

    def __repr__(self):
        return self.__str__()


def roll(n):
    return [random.randint(1, 6) for _ in range(n)]


def score(dice):
    result = 0
    if len(dice) <= 5:
        dice_dict = dict((i, dice.count(i)) for i in dice)
        if dice_dict.get(1) >= 3:
            result += 1000
            dice_dict[1] -= 3
        for number in dice_dict:
            if dice_dict.get(number) >= 3:
                result += number * 100
                dice_dict[number] -= 3
        if 1 in dice_dict:
            result += dice_dict[1] * 100
            dice_dict[1] = 0
        if 5 in dice_dict:
            result += dice_dict[5] * 50
            dice_dict[5] = 0

    remaining_dice = len([die for die in dice_dict.keys() if dice_dict[die] != 0])
    remaining_dice = remaining_dice if remaining_dice else DEFAULT_DICE_NUMBER
    return result, remaining_dice


def print_turn(player, score):
    print "\n------------------------------------------\nStarting turn of {0} [Score: {1}]\n".format(player, score)


def ask_yes_no_question(prompt):
    user_input = raw_input(prompt)
    return user_input.upper() in ["Y", "YES"]


def ask_roll_again(player):
    return ask_yes_no_question("\n{0}, will you roll again? (y/n): ".format(player))


def turn_score(player):
    total_score = 0
    nb_dice = DEFAULT_DICE_NUMBER

    while True:
        dice = roll(nb_dice)
        print "\n{0} rolls the dice and gets {1}.".format(player, dice)
        rolling_score, nb_dice = score(dice)
        if rolling_score == 0:
            if total_score:
                print "That's a zero-point roll, you lost your turn and all your won points in this turn."
            else:
                print "That's a zero-point roll, you cannot roll again in this turn."
            return 0
        else:
            print "That's a score of {0} points and {1} non-scoring {2} can be rolled again.".format(rolling_score, nb_dice, "die" if nb_dice == 1 else "dice")
            if total_score + rolling_score >= NB_POINTS_TO_START:
                total_score += rolling_score
                print "Since you reached more than {0} points this turn, your turn score is now {1}".format(NB_POINTS_TO_START, total_score)
            else:
                total_score = rolling_score
                print "Your turn score is less than {0} points, so now it's just the rolling score, {1} points.".format(NB_POINTS_TO_START, total_score)
            if not ask_roll_again(player):
                return total_score


def get_winner(players_scores):
    return max(players_scores, key = players_scores.get)


def play_game(players, goal = 3000):
    if len(players) < 2:
        raise "This game is for 2 or more players."
    players_scores = dict((player, 0) for player in players)
    number_of_players = len(players_scores)
    players = [player for player in players_scores.keys()]

    first_player_to_reach_final_round = None
    turn = random.randint(0, len(players)-1)

    while turn != first_player_to_reach_final_round:
        player = players[turn]
        score = players_scores[player]
        print_turn(player, score)
        won_score = turn_score(player)
        players_scores[player] += won_score
        print "{0} has won {1} points (total {2}).".format(player, won_score, players_scores[player])
        if players_scores[player] >= goal:
            if first_player_to_reach_final_round is None:
                first_player_to_reach_final_round = turn
            print "{0} has reached over {1} points. Next round will be the last round.".format(player, goal)
            print "\n\nLAST ROUND!!! Let's see who wins!"
        turn = (turn + 1) % number_of_players

    winner = get_winner(players_scores)
    print "\n\nCongratulations, {0}! You have won the Greed Game!".format(winner)


player1 = Player("Player 1")
player2 = Player("Player 2")
play_game([player1, player2], 1500)

More in play_game

You don't need len(players), you could just use number_of_players.

Assuming you are resetting the value of players to ensure we only consider distinct players, if would probably make sense to check the number of players after doing this.

Also, you could consider that it could be played alone too but this is up to you.

A point of rule

At the moment (I think it was the case before my changes), the part :

Before a player is allowed to accumulate points, they must get at least 300 points in a single turn. Once they have achieved 300 points in a single turn, the points earned in that turn and each following turn will be counted toward their total score.

I have the feeling that at the moment, one can start accumulating points even before 300.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.