Chess Tournament Simulator I wrote a simulator that simulates chess games. The game uses probability to determine the player that is most likely to win. though they might be upsets(a low rated player winning the game). I would love insights and suggestions on how to improve on the algorithm, general review is also welcomed.
NOTE: The names and rating were gotten from chess.com application and the code were written in jupyter-notebook
&quot;&quot;&quot;Chess Game simulator&quot;&quot;&quot;

import random
import time

NAMES = ['Jimmy', 'Samie', 'Elani', 'Aron', 'Emir', 'Sven', 'Nelson',
         'Antonio', 'Isabel', 'Wally', 'Li', 'Noam', 'Francis', 'Danya',
         'Danny', 'Engine']
         
RATINGS = [600, 900, 400, 700, 1000, 1100,
           1300, 1500, 1600, 1800, 2000,
           2200, 2300, 2650, 2500, 1000]

class Player:
    &quot;&quot;&quot;This class holds the player information&quot;&quot;&quot;

    def __init__(self, name, rating):
        self.name = name
        self.rating = rating
        self.opponent = None

    def __repr__(self):
        return 'Player({}, {})'.format(self.name, self.rating)

class ChessGame():

    def __init__(self, players):
        self.players = players
        self.tournament_rounds = {1: 'Round 1', 2: 'Round 2', 3: 'Semi-final', 4: 'Final'}
        self.game_round = 0

    def next_round(self):
        &quot;&quot;&quot;Go the next round of the tournament&quot;&quot;&quot;

        self.game_round += 1

        try:
            print(self.tournament_rounds[self.game_round])
        except KeyError:
            return

    def make_pair(self):
        &quot;&quot;&quot;Combine two players randomly to play a chess game&quot;&quot;&quot;

        players = self.players[:]
        new_players = []

        while players:
            player1 = random.choice(players)
            players.remove(player1)
            player2 = random.choice(players)
            players.remove(player2)

            player1.opponent = player2
            player2.opponent = player1
            new_players.append(player1)

        self.players = new_players

    def game(self):
        &quot;&quot;&quot;run the simulation&quot;&quot;&quot;

        if len(self.players) &lt; 2:
            print('Exception: Not enough players to play a game')
            return

        self.next_round()
        print('--------------------------------------------')
        self.make_pair()
        for player in self.players:
            print(f'{player.name}({player.rating}) vs {player.opponent.name}({player.opponent.rating})')
        time.sleep(3)
        print('--------------------------------------------')
        self.players = self.score_game()

    def score_game(self):
        &quot;&quot;&quot;Score the game using probabilities&quot;&quot;&quot;

        winners = []
        for player in self.players:
            total_rating = player.rating + player.opponent.rating
            player_chance = player.rating / total_rating

            maximum_range = 1000
            player_range = [i for i in range(0, int(player_chance * maximum_range))]
            choice = random.randint(0, maximum_range)
            if choice in player_range:
                print(f'{player.name} wins this round by checkmate')
                player.rating += self.game_round * 200
                winners.append(player)
            else:
                print(f'{player.opponent.name} wins this round by checkmate')
                player.opponent.rating += self.game_round * 200
                winners.append(player.opponent)

        return winners

CHESS_PLAYERS = [Player(name, rating) for name, rating in zip(NAMES, RATINGS)]

CHESS_SIM = ChessGame(CHESS_PLAYERS)
CHESS_SIM.game()
print()
CHESS_SIM.game()
print()
CHESS_SIM.game()
print()
CHESS_SIM.game()
Use immutable sequences
Turn
NAMES = ['Jimmy', 'Samie', 'Elani', 'Aron', 'Emir', 'Sven', 'Nelson',
         'Antonio', 'Isabel', 'Wally', 'Li', 'Noam', 'Francis', 'Danya',
         'Danny', 'Engine']
         
RATINGS = [600, 900, 400, 700, 1000, 1100,
           1300, 1500, 1600, 1800, 2000,
           2200, 2300, 2650, 2500, 1000]

into tuples instead of lists, because they're constants.
Use data classes
class Player:
    &quot;&quot;&quot;This class holds the player information&quot;&quot;&quot;

    def __init__(self, name, rating):
        self.name = name
        self.rating = rating
        self.opponent = None

    def __repr__(self):
        return 'Player({}, {})'.format(self.name, self.rating)

does not need an explicit __init__ if you make it a @dataclass:
@dataclass
class Player:
    &quot;&quot;&quot;This class holds the player information&quot;&quot;&quot;
    name: str
    rating: int
    opponent: Optional['Player']

though I don't think that it's appropriate to represent opponent as an attribute of a player. That belongs to the game state.
Sequence representation
Consider changing
    self.tournament_rounds = {1: 'Round 1', 2: 'Round 2', 3: 'Semi-final', 4: 'Final'}

into a global (or class-static) tuple of strings:
TOURNAMENT_ROUNDS = ('Round 1', 'Round 2', 'Semi-final', 'Final')

The indices as keys are redundant.
Iterators and responsibilities
def next_round(self):
    &quot;&quot;&quot;Go the next round of the tournament&quot;&quot;&quot;

    self.game_round += 1

    try:
        print(self.tournament_rounds[self.game_round])
    except KeyError:
        return

and
            print(f'{player.name} wins this round by checkmate')
            player.rating += self.game_round * 200

seem dicey to me in terms of OOP representation. You'd be better-off having the game class yield a generator of rounds. This will make the game instance reusable.
Your score_game has the following responsibilities baked in - game simulation, and game presentation. These need to be separated - your score_game should not be printing. Also, score_game is itself not a great name, since it's not just scoring the game - it's actually running the game simulation.
Random selection
        maximum_range = 1000
        player_range = [i for i in range(0, int(player_chance * maximum_range))]
        choice = random.randint(0, maximum_range)
        if choice in player_range:

needs to be re-thought. Currently you're constructing a list, in memory, of every single number from 0 through (up to) 1000 only to do a membership test and then throw the whole thing away. Instead, just generate a random number and compare it to an upper bound.
Sleepy code
Don't arbitrarily add sleep. If you want to space out your console interface between logical operations, wait for a keypress.
Suggested
&quot;&quot;&quot;Chess Game simulator&quot;&quot;&quot;

from dataclasses import dataclass
from random import shuffle, randrange
from typing import ClassVar, Tuple, Iterable, Dict


@dataclass
class Player:
    name: str
    rating: int

    def __str__(self):
        return f'{self.name}({self.rating})'


CHESS_PLAYERS = [
    Player(name, rating)
    for rating, name in (
        ( 600, 'Jimmy'),
        ( 900, 'Samie'),
        ( 400, 'Elani'),
        ( 700, 'Aron'),
        (1000, 'Emir'),
        (1100, 'Sven'),
        (1300, 'Nelson'),
        (1500, 'Antonio'),
        (1600, 'Isabel'),
        (1800, 'Wally'),
        (2000, 'Li'),
        (2200, 'Noam'),
        (2300, 'Francis'),
        (2650, 'Danya'),
        (2500, 'Danny'),
        (1000, 'Engine'),
    )
]


class Match:
    def __init__(self, p1: Player, p2: Player):
        self.p1, self.p2 = p1, p2
        total = self.p1.rating + self.p2.rating
        if randrange(total) &lt; self.p1.rating:
            self.winner = self.p1
        else:
            self.winner = self.p2

    def __str__(self):
        return f'{self.p1} vs {self.p2}'

    @property
    def outcome(self) -&gt; str:
        return f'{self.winner} wins this round by checkmate'


class Round:
    def __init__(self, name: str, matches: Iterable[Match]):
        self.name = name
        self.matches = tuple(matches)

    def print(self):
        print(self.name)
        print('-'*45)
        print('\n'.join(str(match) for match in self.matches))
        print('-'*45)
        print('\n'.join(match.outcome for match in self.matches))


class ChessGame:
    ROUND_NAMES: ClassVar[Tuple[str]] = ('Round 1', 'Round 2', 'Semi-final', 'Final')

    def __init__(self, players: Iterable[Player]):
        self.players: Tuple[Player] = tuple(players)

    def __iter__(self) -&gt; Iterable[Round]:
        players = list(self.players)

        for i_round, name in enumerate(self.ROUND_NAMES):
            shuffle(players)
            half = len(players)//2
            round = Round(
                name,
                (
                    Match(p1, p2)
                    for p1, p2 in zip(players[:half], players[half:])
                )
            )
            yield round

            for match in round.matches:
                match.winner.rating += (i_round + 1) * 200

            players = [match.winner for match in round.matches]

    def print_run(self):
        for round in self:
            round.print()
            print()


def main():
    sim = ChessGame(CHESS_PLAYERS)
    sim.print_run()


if __name__ == '__main__':
    main()

The biggest 'gotcha' here is that, whereas the ChessGame is now reusable, player instances (just as in your original code) are mutated, so if you want to do repeated simulations you have to make copies of your player instances.