Repository: skent259/crapssim (language: python)

Issue:
Changes for Type Hinting
I made Bet an Abstract Base Class (ABC) since I think that better represents that it is an Abstract class that is not initialized directly.

I added total and result attributes to the __init__ method of Dice so that those variables are initialized prior to roll or fixed_roll being run. 

Then I added Type Hinting to all of crapssim for better documentation of methods and for easier error checking of misstyped parameters or variables. This passes the mypy check I ran on it.

I also used PyCharm's Optimize Imports function to optimize the imports.

This passed all tests in the tests module. 

I also did some of the TODO's that were in there (should have done these separately but still getting the hang of Github stuff.):
- I fixed the check in the table if there are no players and it tries to Run. Previously it added a player with None strategy which didn't work, now it adds a player with the default strategy.
- I changed it so if a bet is already on the table it just adds to that bet instead of appending a new bet.
- I changed player so if a bet is not removable it doesn't remove the bet.

I added new tests for all of these things and they all passed the previous tests. 


Discussion / hints:

Fix patch:
diff --git a/Python/run_simulations.py b/Python/run_simulations.py
index e9f75304..3158b635 100644
--- a/Python/run_simulations.py
+++ b/Python/run_simulations.py
@@ -82,7 +82,7 @@ def run_multi_simulation(n_sim, n_roll, n_shooter, bankroll, strategy, name, run
             # write data to file
             # TODO: get actual player cash 
             for bank, s in zip(bankroll, strategy):
-                out = f"{i},{s},{table._get_player(s).bankroll},{bank},{table.dice.n_rolls}"
+                out = f"{i},{s},{table.get_player(s).bankroll},{bank},{table.dice.n_rolls}"
                 # out = "{},{},{},{}".format(s, table.total_player_cash, bank, table.dice.n_rolls_)
                 f_out.write(str(out))
                 f_out.write(str('\n'))
diff --git a/README.md b/README.md
index 3e2bac12..af93535a 100644
--- a/README.md
+++ b/README.md
@@ -52,23 +52,23 @@ table.run(max_rolls=20)
 To evaluate a couple of strategies across many table sessions, you can run:
 
 ```python
-import crapssim as craps 
+import crapssim as craps
 
 n_sim = 20
 bankroll = 300
 strategies = {
-    "place68": craps.strategy.place68, 
-    "ironcross": craps.strategy.ironcross 
+    "place68": craps.strategy.place68,
+    "ironcross": craps.strategy.ironcross
 }
 
 for i in range(n_sim):
-    table = craps.Table() 
+    table = craps.Table()
     for s in strategies:
         table.add_player(craps.Player(bankroll, strategies[s], s))
 
     table.run(max_rolls=float("inf"), max_shooter=10, verbose=False)
     for s in strategies:
-        print(f"{i}, {s}, {table._get_player(s).bankroll}, {bankroll}, {table.dice.n_rolls}")
+        print(f"{i}, {s}, {table.get_player(s).bankroll}, {bankroll}, {table.dice.n_rolls}")
 ```
 
 For more advanced strategies, you need to write a custom function that can perform the strategy.  Some building blocks and examples can be found in [strategy.py](./crapssim/strategy.py)
diff --git a/crapssim/__init__.py b/crapssim/__init__.py
index 7e5b119d..7d7e3d7d 100644
--- a/crapssim/__init__.py
+++ b/crapssim/__init__.py
@@ -1,8 +1,7 @@
 __all__ = ["table", "player", "dice", "strategy", "bet"]
 
-from crapssim.table import Table
-from crapssim.player import Player
 from crapssim.dice import Dice
-
+from crapssim.player import Player
+from crapssim.table import Table
 from . import bet
 from . import strategy
diff --git a/crapssim/bet.py b/crapssim/bet.py
index e02567ea..cfada51b 100644
--- a/crapssim/bet.py
+++ b/crapssim/bet.py
@@ -1,17 +1,25 @@
+import typing
+from abc import ABC
+
 from crapssim.dice import Dice
 
+if typing.TYPE_CHECKING:
+    from crapssim.table import Table
+
 
-class Bet(object):
+class Bet(ABC):
     """
     A generic bet for the craps table
 
     Parameters
     ----------
-    bet_amount : float
+    bet_amount : typing.SupportsFloat
         Wagered amount for the bet
 
     Attributes
     ----------
+    bet_amount: float
+        Wagered amount for the bet
     name : string
         Name for the bet
     subname : string
@@ -22,25 +30,43 @@ class Bet(object):
         Numbers to roll that cause this bet to lose
     payoutratio : float
         Ratio that bet pays out on a win
-
+    removable : bool
+        Whether the bet can be removed or not
+    can_be_placed_point_on : bool
+        Whether the bet can be placed when the point is on
+    can_be_placed_point_off : bool
+        Whether the bet can be placed when the point is off
     """
 
-    name = None
-    subname = ""
-    winning_numbers = []
-    losing_numbers = []
-    payoutratio = float(1)
-    # TODO: add whether bet can be removed
-
-    def __init__(self, bet_amount):
-        self.bet_amount = float(bet_amount)
-
-    # def __eq__(self, other):
-    #     return self.name == other.name
-
-    def _update_bet(self, table_object, dice_object: Dice):
-        status = None
-        win_amount = 0
+    def __init__(self, bet_amount: typing.SupportsFloat):
+        self.bet_amount: float = float(bet_amount)
+        self.name: str = str()
+        self.subname: str = str()
+        self.winning_numbers: list[int] = []
+        self.losing_numbers: list[int] = []
+        self.payoutratio: float = float(1)
+        self.removable: bool = True
+        self.can_be_placed_point_on = True
+        self.can_be_placed_point_off = True
+
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        """
+        Returns whether the bets status is win, lose or None and if win the amount won.
+
+        Parameters
+        ----------
+        table_object : Table
+            The table to check the bet was made on.
+        dice_object : Dice
+            The dice to check the bet against.
+
+        Returns
+        -------
+        tuple[str | None, float]
+            The status of the bet and the amount of the winnings.
+        """
+        status: str | None = None
+        win_amount: float = 0.0
 
         if dice_object.total in self.winning_numbers:
             status = "win"
@@ -57,19 +83,18 @@ def _update_bet(self, table_object, dice_object: Dice):
 
 
 class PassLine(Bet):
-    # TODO: make this require that table_object.point = "Off",
-    # probably better in the player module
-    def __init__(self, bet_amount):
-        self.name = "PassLine"
-        self.winning_numbers = [7, 11]
-        self.losing_numbers = [2, 3, 12]
-        self.payoutratio = 1.0
-        self.prepoint = True
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "PassLine"
+        self.winning_numbers: list[int] = [7, 11]
+        self.losing_numbers: list[int] = [2, 3, 12]
+        self.payoutratio: float = 1.0
+        self.prepoint: bool = True
+        self.can_be_placed_point_on = False
 
-    def _update_bet(self, table_object, dice_object):
-        status = None
-        win_amount = 0
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        status: str | None = None
+        win_amount: float = 0.0
 
         if dice_object.total in self.winning_numbers:
             status = "win"
@@ -80,16 +105,19 @@ def _update_bet(self, table_object, dice_object):
             self.winning_numbers = [dice_object.total]
             self.losing_numbers = [7]
             self.prepoint = False
+            self.removable = False
 
         return status, win_amount
 
 
 class Come(PassLine):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Come"
+        self.name: str = "Come"
+        self.can_be_placed_point_off = False
+        self.can_be_placed_point_on = True
 
-    def _update_bet(self, table_object, dice_object):
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
         status, win_amount = super()._update_bet(table_object, dice_object)
         if not self.prepoint and self.subname == "":
             self.subname = "".join(str(e) for e in self.winning_numbers)
@@ -102,12 +130,18 @@ def _update_bet(self, table_object, dice_object):
 
 
 class Odds(Bet):
-    def __init__(self, bet_amount, bet_object):
+    def __init__(self, bet_amount: typing.SupportsFloat, bet_object: Bet):
         super().__init__(bet_amount)
-        self.name = "Odds"
-        self.subname = "".join(str(e) for e in bet_object.winning_numbers)
-        self.winning_numbers = bet_object.winning_numbers
-        self.losing_numbers = bet_object.losing_numbers
+
+        if not isinstance(bet_object, PassLine) and not isinstance(bet_object, Come):
+            raise TypeError('bet_object must be either a PassLine or Come Bet.')
+        if isinstance(bet_object, PassLine):
+            self.can_be_placed_point_off = False
+
+        self.name: str = "Odds"
+        self.subname: str = "".join(str(e) for e in bet_object.winning_numbers)
+        self.winning_numbers: list[int] = bet_object.winning_numbers
+        self.losing_numbers: list[int] = bet_object.losing_numbers
 
         if self.winning_numbers == [4] or self.winning_numbers == [10]:
             self.payoutratio = 2 / 1
@@ -123,7 +157,7 @@ def __init__(self, bet_amount, bet_object):
 
 
 class Place(Bet):
-    def _update_bet(self, table_object, dice_object):
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
         # place bets are inactive when point is "Off"
         if table_object.point == "On":
             return super()._update_bet(table_object, dice_object)
@@ -132,57 +166,57 @@ def _update_bet(self, table_object, dice_object):
 
 
 class Place4(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place4"
-        self.winning_numbers = [4]
-        self.losing_numbers = [7]
-        self.payoutratio = 9 / 5
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place4"
+        self.winning_numbers: list[int] = [4]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 9 / 5
 
 
 class Place5(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place5"
-        self.winning_numbers = [5]
-        self.losing_numbers = [7]
-        self.payoutratio = 7 / 5
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place5"
+        self.winning_numbers: list[int] = [5]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 7 / 5
 
 
 class Place6(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place6"
-        self.winning_numbers = [6]
-        self.losing_numbers = [7]
-        self.payoutratio = 7 / 6
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place6"
+        self.winning_numbers: list[int] = [6]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 7 / 6
 
 
 class Place8(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place8"
-        self.winning_numbers = [8]
-        self.losing_numbers = [7]
-        self.payoutratio = 7 / 6
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place8"
+        self.winning_numbers: list[int] = [8]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 7 / 6
 
 
 class Place9(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place9"
-        self.winning_numbers = [9]
-        self.losing_numbers = [7]
-        self.payoutratio = 7 / 5
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place9"
+        self.winning_numbers: list[int] = [9]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 7 / 5
 
 
 class Place10(Place):
-    def __init__(self, bet_amount):
-        self.name = "Place10"
-        self.winning_numbers = [10]
-        self.losing_numbers = [7]
-        self.payoutratio = 9 / 5
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Place10"
+        self.winning_numbers: list[int] = [10]
+        self.losing_numbers: list[int] = [7]
+        self.payoutratio: float = 9 / 5
 
 
 """
@@ -200,17 +234,17 @@ class Field(Bet):
         Set of numbers that pay triple on the field bet (default = [])
     """
 
-    def __init__(self, bet_amount, double=[2, 12], triple=[]):
-        self.name = "Field"
-        self.double_winning_numbers = double
-        self.triple_winning_numbers = triple
-        self.winning_numbers = [2, 3, 4, 9, 10, 11, 12]
-        self.losing_numbers = [5, 6, 7, 8]
+    def __init__(self, bet_amount: float, double: list[int] = [2, 12], triple: list[int] = []):
         super().__init__(bet_amount)
+        self.name: str = "Field"
+        self.double_winning_numbers: list[int] = double
+        self.triple_winning_numbers: list[int] = triple
+        self.winning_numbers: list[int] = [2, 3, 4, 9, 10, 11, 12]
+        self.losing_numbers: list[int] = [5, 6, 7, 8]
 
-    def _update_bet(self, table_object, dice_object):
-        status = None
-        win_amount = 0
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        status: str | None = None
+        win_amount: float = 0
 
         if dice_object.total in self.triple_winning_numbers:
             status = "win"
@@ -233,20 +267,19 @@ def _update_bet(self, table_object, dice_object):
 
 
 class DontPass(Bet):
-    # TODO: make this require that table_object.point = "Off",
-    #  probably better in the player module
-    def __init__(self, bet_amount):
-        self.name = "DontPass"
-        self.winning_numbers = [2, 3]
-        self.losing_numbers = [7, 11]
-        self.push_numbers = [12]
-        self.payoutratio = 1.0
-        self.prepoint = True
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-
-    def _update_bet(self, table_object, dice_object):
-        status = None
-        win_amount = 0
+        self.name: str = "DontPass"
+        self.winning_numbers: list[int] = [2, 3]
+        self.losing_numbers: list[int] = [7, 11]
+        self.push_numbers: list[int] = [12]
+        self.payoutratio: float = 1.0
+        self.prepoint: bool = True
+        self.can_be_placed_point_on = False
+
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        status: str | None = None
+        win_amount: float = 0.0
 
         if dice_object.total in self.winning_numbers:
             status = "win"
@@ -265,11 +298,13 @@ def _update_bet(self, table_object, dice_object):
 
 
 class DontCome(DontPass):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "DontCome"
+        self.name: str = "DontCome"
+        self.can_be_placed_point_off = False
+        self.can_be_placed_point_on = True
 
-    def _update_bet(self, table_object, dice_object):
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
         status, win_amount = super()._update_bet(table_object, dice_object)
         if not self.prepoint and self.subname == "":
             self.subname = "".join(str(e) for e in self.losing_numbers)
@@ -282,12 +317,12 @@ def _update_bet(self, table_object, dice_object):
 
 
 class LayOdds(Bet):
-    def __init__(self, bet_amount, bet_object):
+    def __init__(self, bet_amount: float, bet_object: Bet):
         super().__init__(bet_amount)
-        self.name = "LayOdds"
-        self.subname = "".join(str(e) for e in bet_object.losing_numbers)
-        self.winning_numbers = bet_object.winning_numbers
-        self.losing_numbers = bet_object.losing_numbers
+        self.name: str = "LayOdds"
+        self.subname: str = "".join(str(e) for e in bet_object.losing_numbers)
+        self.winning_numbers: list[int] = bet_object.winning_numbers
+        self.losing_numbers: list[int] = bet_object.losing_numbers
 
         if self.losing_numbers == [4] or self.losing_numbers == [10]:
             self.payoutratio = 1 / 2
@@ -303,63 +338,69 @@ def __init__(self, bet_amount, bet_object):
 
 
 class Any7(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Any7"
-        self.winning_numbers = [7]
-        self.losing_numbers = [2, 3, 4, 5, 6, 8, 9, 10, 11, 12]
-        self.payoutratio = 4
+        self.name: str = "Any7"
+        self.winning_numbers: list[int] = [7]
+        self.losing_numbers: list[int] = [2, 3, 4, 5, 6, 8, 9, 10, 11, 12]
+        self.payoutratio: int = 4
+
 
 class Two(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Two"
-        self.winning_numbers = [2]
-        self.losing_numbers = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
-        self.payoutratio = 30
+        self.name: str = "Two"
+        self.winning_numbers: list[int] = [2]
+        self.losing_numbers: list[int] = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+        self.payoutratio: int = 30
+
 
 class Three(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Three"
-        self.winning_numbers = [3]
-        self.losing_numbers = [2, 4, 5, 6, 7, 8, 9, 10, 11, 12]
-        self.payoutratio = 15
+        self.name: str = "Three"
+        self.winning_numbers: list[int] = [3]
+        self.losing_numbers: list[int] = [2, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+        self.payoutratio: int = 15
+
 
 class Yo(Bet):
-    def __init__(self, bet_amount):
-        self.name = "Yo"
-        self.winning_numbers = [11]
-        self.losing_numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 12]
-        self.payoutratio = 15
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
+        self.name: str = "Yo"
+        self.winning_numbers: list[int] = [11]
+        self.losing_numbers: list[int] = [2, 3, 4, 5, 6, 7, 8, 9, 10, 12]
+        self.payoutratio: int = 15
+
 
 class Boxcars(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Boxcars"
-        self.winning_numbers = [12]
-        self.losing_numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
-        self.payoutratio = 30
+        self.name: str = "Boxcars"
+        self.winning_numbers: list[int] = [12]
+        self.losing_numbers: list[int] = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+        self.payoutratio: int = 30
+
 
 class AnyCraps(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "AnyCraps"
-        self.winning_numbers = [2, 3, 12]
-        self.losing_numbers = [4, 5, 6, 7, 8, 9, 10, 11]
-        self.payoutratio = 7
+        self.name: str = "AnyCraps"
+        self.winning_numbers: list[int] = [2, 3, 12]
+        self.losing_numbers: list[int] = [4, 5, 6, 7, 8, 9, 10, 11]
+        self.payoutratio: int = 7
+
 
 class CAndE(Bet):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "CAndE"
-        self.winning_numbers = [2, 3, 11, 12]
-        self.losing_numbers = [4, 5, 6, 7, 8, 9, 10]
+        self.name: str = "CAndE"
+        self.winning_numbers: list[int] = [2, 3, 11, 12]
+        self.losing_numbers: list[int] = [4, 5, 6, 7, 8, 9, 10]
 
-    def _update_bet(self, table_object, dice_object):
-        status = None
-        win_amount = 0
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        status: str | None = None
+        win_amount: float = 0
 
         if dice_object.total in self.winning_numbers:
             status = "win"
@@ -373,60 +414,66 @@ def _update_bet(self, table_object, dice_object):
 
         return status, win_amount
 
+
 class Hardway(Bet):
     """
     Attributes
     ----------
     number : int
         The relevant number that the bet wins and loses on (for hardways)
-    winnings_combination : list(int)
+    winning_result : list(int)
         The combination of dice the bet wins on
     """
-    def __init__(self, bet_amount):
+
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.number = None 
-        self.winning_result = [None, None]
+        self.number: int | None = None
+        self.winning_result: list[int | None] = [None, None]
 
-    def _update_bet(self, table_object, dice_object: Dice):
-        status = None 
-        win_amount = 0
+    def _update_bet(self, table_object: "Table", dice_object: Dice) -> tuple[str | None, float]:
+        status: str | None = None
+        win_amount: float = 0.0
 
         if dice_object.result == self.winning_result:
             status = "win"
             win_amount = self.payoutratio * self.bet_amount
-        elif dice_object.total in [self.number, 7]: 
+        elif dice_object.total in [self.number, 7]:
             status = "lose"
 
-        return status, win_amount 
+        return status, win_amount
+
 
 class Hard4(Hardway):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Hard4"
-        self.winning_result = [2, 2]
-        self.number = 4
-        self.payoutratio = 7
+        self.name: str = "Hard4"
+        self.winning_result: list[int | None] = [2, 2]
+        self.number: int = 4
+        self.payoutratio: int = 7
+
 
 class Hard6(Hardway):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Hard6"
-        self.winning_result = [3, 3]
-        self.number = 6
-        self.payoutratio = 9
+        self.name: str ="Hard6"
+        self.winning_result: list[int | None] = [3, 3]
+        self.number: int = 6
+        self.payoutratio: int = 9
+
 
 class Hard8(Hardway):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Hard8"
-        self.winning_result = [4, 4]
-        self.number = 8
-        self.payoutratio = 9
+        self.name: str ="Hard8"
+        self.winning_result: list[int | None] = [4, 4]
+        self.number: int = 8
+        self.payoutratio: int = 9
+
 
 class Hard10(Hardway):
-    def __init__(self, bet_amount):
+    def __init__(self, bet_amount: float):
         super().__init__(bet_amount)
-        self.name = "Hard10"
-        self.winning_result = [5, 5]
-        self.number = 10
-        self.payoutratio= 7
+        self.name: str ="Hard10"
+        self.winning_result: list[int | None] = [5, 5]
+        self.number: int = 10
+        self.payoutratio: int = 7
diff --git a/crapssim/dice.py b/crapssim/dice.py
index ebdbcd73..496f41c1 100644
--- a/crapssim/dice.py
+++ b/crapssim/dice.py
@@ -1,34 +1,34 @@
+import typing
+
 from numpy import random as r
 
 
-class Dice(object):
+class Dice:
     """
     Simulate the rolling of a dice
 
-    Parameters
-    ----------
-    NONE
-
     Attributes
     ----------
-    n_rolls_ : int
+    n_rolls : int
         Number of rolls for the dice
-    result_ : array, shape = [2]
+    result : array, shape = [2]
         Most recent outcome of the roll of two dice
-    total_ : int
+    total : int
         Sum of dice outcome
 
     """
 
-    def __init__(self):
-        self.n_rolls = 0
+    def __init__(self) -> None:
+        self.total: int = 0
+        self.result: typing.Iterable[int] | None = None
+        self.n_rolls: int = 0
 
-    def roll(self):
+    def roll(self) -> None:
         self.n_rolls += 1
         self.result = r.randint(1, 7, size=2)
         self.total = sum(self.result)
 
-    def fixed_roll(self, outcome):
+    def fixed_roll(self, outcome: typing.Iterable[int]) -> None:
         self.n_rolls += 1
         self.result = outcome
         self.total = sum(self.result)
diff --git a/crapssim/player.py b/crapssim/player.py
index 7380c066..97784c61 100644
--- a/crapssim/player.py
+++ b/crapssim/player.py
@@ -1,80 +1,123 @@
-class Player(object):
+import typing
+
+from crapssim.bet import Bet
+from crapssim.strategy import STRATEGY_TYPE, passline
+
+if typing.TYPE_CHECKING:
+    from crapssim.table import Table
+
+
+class Player:
     """
     Player standing at the craps table
 
     Parameters
     ----------
-    bankroll : float
-        Starting amount of cash for the player, will be updated during play
+    bankroll : typing.SupportsFloat
+        Starting amount of cash for the player
     bet_strategy : function(table, player, unit=5)
         A function that implements a particular betting strategy.  See betting_strategies.py
-    name : string, optional (default = "Player")
+    name : string, default = "Player"
         Name of the player
+    unit : typing.SupportsFloat, default=5
+        Standard amount of bet to be used by bet_strategy
 
     Attributes
     ----------
+    bankroll : typing.SupportsFloat
+        Current amount of cash for the player
+    name : str
+        Name of the player
+    bet_strategy :
+        A function that implements a particular betting strategy. See betting_strategies.py.
+    strat_info : dict[str, typing.Any]
+        Variables to be used by the players bet_strategy
+    unit : typing.SupportsFloat
+        Standard amount of bet to be used by bet_strategy
     bets_on_table : list
         List of betting objects for the player
     total_bet_amount : int
         Sum of bet value for the player
     """
 
-    def __init__(self, bankroll, bet_strategy=None, name="Player"):
-        self.bankroll = bankroll
-        self.bet_strategy = bet_strategy
-        self.name = name
-        self.bets_on_table = []
-        self.total_bet_amount = 0
-        # TODO: initial betting strategy
+    def __init__(self, bankroll: typing.SupportsFloat,
+                 bet_strategy: STRATEGY_TYPE = passline,
+                 name: str = "Player",
+                 unit: typing.SupportsFloat = 5,
+                 table: 'Table' = None):
+        self.bankroll: float = bankroll
+        self.bet_strategy: STRATEGY_TYPE = bet_strategy
+        self.strat_info: dict[str, typing.Any] = {}
+        self.name: str = name
+        self.unit: typing.SupportsFloat = unit
+        self.table = None
+        if table is not None:
+            self.sit_at_table(table)
+
+        self.bets_on_table: list[Bet] = []
+        self.total_bet_amount: float = 0.0
+
+    def sit_at_table(self, table: "Table"):
+        table.add_player(self)
+
+    def bet(self, bet_object: Bet) -> None:
+        if self.table.point.status == 'Off' and not bet_object.can_be_placed_point_off:
+            return
+        if self.table.point.status == 'On' and not bet_object.can_be_placed_point_on:
+            return
 
-    def bet(self, bet_object):
         if self.bankroll >= bet_object.bet_amount:
             self.bankroll -= bet_object.bet_amount
-            self.bets_on_table.append(
-                bet_object
-            )  # TODO: make sure this only happens if that bet isn't on the table, otherwise wager amount gets updated
+
+            if (bet_object.name, bet_object.subname) in [(b.name, b.subname) for b in self.bets_on_table]:
+                existing_bet: Bet = self.get_bet(bet_object.name, bet_object.subname)
+                existing_bet.bet_amount += bet_object.bet_amount
+            else:
+                self.bets_on_table.append(bet_object)
+
             self.total_bet_amount += bet_object.bet_amount
 
-    def remove(self, bet_object):
-        # TODO: add bet attribute for whether a bet can be removed and put condition in here
-        if bet_object in self.bets_on_table:
+    def remove(self, bet_object: Bet) -> None:
+        if bet_object in self.bets_on_table and bet_object.removable:
             self.bankroll += bet_object.bet_amount
             self.bets_on_table.remove(bet_object)
             self.total_bet_amount -= bet_object.bet_amount
 
-    def has_bet(self, *bets_to_check):
+    def has_bet(self, *bets_to_check: str) -> bool:
         """ returns True if bets_to_check and self.bets_on_table has at least one thing in common """
         bet_names = {b.name for b in self.bets_on_table}
         return bool(bet_names.intersection(bets_to_check))
 
-    def get_bet(self, bet_name, bet_subname=""):
+    def get_bet(self, bet_name: str, bet_subname: str = "") -> Bet:
         """returns first betting object matching bet_name and bet_subname.
         If bet_subname="Any", returns first betting object matching bet_name"""
         if bet_subname == "Any":
-            bet_name_list = [b.name for b in self.bets_on_table]
-            ind = bet_name_list.index(bet_name)
+            bet_name_list: list[str] = [b.name for b in self.bets_on_table]
+            ind: int = bet_name_list.index(bet_name)
         else:
-            bet_name_list = [[b.name, b.subname] for b in self.bets_on_table]
-            ind = bet_name_list.index([bet_name, bet_subname])
+            bet_name_subname_list: list[list[str]] = [[b.name, b.subname] for b in self.bets_on_table]
+            ind = bet_name_subname_list.index([bet_name, bet_subname])
         return self.bets_on_table[ind]
 
-    def num_bet(self, *bets_to_check):
+    def num_bet(self, *bets_to_check: str) -> int:
         """ returns the total number of bets in self.bets_on_table that match bets_to_check """
         bet_names = [b.name for b in self.bets_on_table]
         return sum([i in bets_to_check for i in bet_names])
 
-    def remove_if_present(self, bet_name, bet_subname=""):
+    def remove_if_present(self, bet_name: str, bet_subname: str = "") -> None:
         if self.has_bet(bet_name):
             self.remove(self.get_bet(bet_name, bet_subname))
 
-    def _add_strategy_bets(self, table, *args, **kwargs):
+    def add_strategy_bets(self) -> None:
         """ Implement the given betting strategy """
-        return self.bet_strategy(self, table, *args, **kwargs)
+        if self.bet_strategy:
+            self.bet_strategy(self, self.table, **self.strat_info)
 
-    def _update_bet(self, table_object, dice_object, verbose=False):
+    def update_bet(self, verbose: bool = False) -> \
+            dict[str, dict[str, str | None | float]]:
         info = {}
         for b in self.bets_on_table[:]:
-            status, win_amount = b._update_bet(table_object, dice_object)
+            status, win_amount = b._update_bet(self.table, self.table.dice)
 
             if status == "win":
                 self.bankroll += win_amount + b.bet_amount
diff --git a/crapssim/strategy.py b/crapssim/strategy.py
index ad5bcff1..829754ab 100644
--- a/crapssim/strategy.py
+++ b/crapssim/strategy.py
@@ -1,7 +1,13 @@
-from crapssim.bet import PassLine, Odds, Come
+import typing
+
 from crapssim.bet import DontPass, LayOdds, DontCome
-from crapssim.bet import Place, Place4, Place5, Place6, Place8, Place9, Place10
 from crapssim.bet import Field
+from crapssim.bet import PassLine, Odds, Come, Bet
+from crapssim.bet import Place, Place4, Place5, Place6, Place8, Place9, Place10
+
+if typing.TYPE_CHECKING:
+    from crapssim.player import Player
+    from crapssim.table import Table
 
 """
 Various betting strategies that are based on conditions of the CrapsTable.
@@ -13,15 +19,49 @@
 Fundamental Strategies
 """
 
+STRATEGY_TYPE = typing.Union[typing.Callable[['Player', 'Table', int, dict[str, int] | None], dict[str, int] | None],
+                             typing.Callable[['Player', 'Table', int, None], None]]
 
-def passline(player, table, unit=5, strat_info=None):
+
+def passline(player: 'Player', table: 'Table'):
+    """ If the point is off place a bet on the Pass Line.
+
+    Parameters
+    ----------
+    player
+        Player object that is using the strategy.
+    table
+        Table object that the strategy is being used on.
+
+    Returns
+    -------
+    None
+        Dictionary of strategy info.
+    """
     # Pass line bet
     if table.point == "Off" and not player.has_bet("PassLine"):
-        player.bet(PassLine(unit))
+        player.bet(PassLine(player.unit))
 
 
-def passline_odds(player, table, unit=5, strat_info=None, mult=1):
-    passline(player, table, unit)
+def passline_odds(player: 'Player', table: 'Table', mult: int | str = 1) -> None:
+    """ If the point is off place a bet on the Pass Line. If the point is on, bet the Pass Line Odds.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        mult
+            Multiplier integers for the odds or "345" for 345x odds.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
+
     # Pass line odds
     if mult == "345":
         if table.point == "On":
@@ -35,48 +75,114 @@ def passline_odds(player, table, unit=5, strat_info=None, mult=1):
         mult = float(mult)
 
     if (
-        table.point == "On"
-        and player.has_bet("PassLine")
-        and not player.has_bet("Odds")
+            table.point == "On"
+            and player.has_bet("PassLine")
+            and not player.has_bet("Odds")
     ):
-        player.bet(Odds(mult * unit, player.get_bet("PassLine")))
-
+        player.bet(Odds(float(mult * player.unit), player.get_bet("PassLine")))
+
+
+def passline_odds2(player: 'Player', table: 'Table') -> None:
+    """ Pass Line bet when point is off, 2x odds bet when point is on.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline_odds(player, table, mult=2)
+
+
+def passline_odds345(player: 'Player', table: 'Table') -> None:
+    """ Pass Line bet when point is off, 345x odds bet when point is on.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline_odds(player, table, mult='345')
+
+
+def pass2come(player: 'Player', table: 'Table') -> None:
+    """ Pass Line bet followed by 2 come bets when point is on.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
 
-def passline_odds2(player, table, unit=5, strat_info=None):
-    passline_odds(player, table, unit, strat_info=None, mult=2)
+    # Come bet (2)
+    if table.point == "On" and player.num_bet("Come") < 2:
+        player.bet(Come(player.unit))
 
 
-def passline_odds345(player, table, unit=5, strat_info=None):
-    passline_odds(player, table, unit, strat_info=None, mult="345")
+def place(player: 'Player', table: 'Table', skip_point: bool = True, numbers: set[int] | None = None) -> \
+        None:
+    """ Place bets, ie 3, 4, 5, 6, 8, 9, 10
 
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+                skip_point: bool
+            If True, don't bet when the point is the given number.
+        numbers: set
+            Point numbers to place bets on.
 
-def pass2come(player, table, unit=5, strat_info=None):
-    passline(player, table, unit)
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
 
-    # Come bet (2)
-    if table.point == "On" and player.num_bet("Come") < 2:
-        player.bet(Come(unit))
+    if numbers is None:
+        numbers: set[int] = {6, 8}
 
+    numbers = set(numbers).intersection({4, 5, 6, 8, 9, 10})
 
-def place(player, table, unit=5, strat_info={"numbers": {6, 8}}, skip_point=True):
-    strat_info["numbers"] = set(strat_info["numbers"]).intersection({4, 5, 6, 8, 9, 10})
     if skip_point:
-        strat_info["numbers"] -= {table.point.number}
+        numbers -= {table.point.number}
 
     # Place the provided numbers when point is ON
     if table.point == "On":
-        if not player.has_bet("Place4") and 4 in strat_info["numbers"]:
-            player.bet(Place4(unit))
-        if not player.has_bet("Place5") and 5 in strat_info["numbers"]:
-            player.bet(Place5(unit))
-        if not player.has_bet("Place6") and 6 in strat_info["numbers"]:
-            player.bet(Place6(6 / 5 * unit))
-        if not player.has_bet("Place8") and 8 in strat_info["numbers"]:
-            player.bet(Place8(6 / 5 * unit))
-        if not player.has_bet("Place9") and 9 in strat_info["numbers"]:
-            player.bet(Place9(unit))
-        if not player.has_bet("Place10") and 10 in strat_info["numbers"]:
-            player.bet(Place10(unit))
+        if not player.has_bet("Place4") and 4 in numbers:
+            player.bet(Place4(player.unit))
+        if not player.has_bet("Place5") and 5 in numbers:
+            player.bet(Place5(player.unit))
+        if not player.has_bet("Place6") and 6 in numbers:
+            player.bet(Place6(6 / 5 * player.unit))
+        if not player.has_bet("Place8") and 8 in numbers:
+            player.bet(Place8(6 / 5 * player.unit))
+        if not player.has_bet("Place9") and 9 in numbers:
+            player.bet(Place9(player.unit))
+        if not player.has_bet("Place10") and 10 in numbers:
+            player.bet(Place10(player.unit))
 
     # Move the bets off the point number if it shows up later
     if skip_point and table.point == "On":
@@ -94,34 +200,78 @@ def place(player, table, unit=5, strat_info={"numbers": {6, 8}}, skip_point=True
             player.remove(player.get_bet("Place10"))
 
 
-def place68(player, table, unit=5, strat_info=None):
-    passline(player, table, unit, strat_info=None)
+def place68(player: 'Player', table: 'Table') -> None:
+    """ Place the 6 and the 8.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
     # Place 6 and 8 when point is ON
     p_has_place_bets = player.has_bet(
         "Place4", "Place5", "Place6", "Place8", "Place9", "Place10"
     )
     if table.point == "On" and not p_has_place_bets:
         if table.point.number == 6:
-            player.bet(Place8(6 / 5 * unit))
+            player.bet(Place8(6 / 5 * player.unit))
         elif table.point.number == 8:
-            player.bet(Place6(6 / 5 * unit))
+            player.bet(Place6(6 / 5 * player.unit))
         else:
-            player.bet(Place8(6 / 5 * unit))
-            player.bet(Place6(6 / 5 * unit))
+            player.bet(Place8(6 / 5 * player.unit))
+            player.bet(Place6(6 / 5 * player.unit))
 
 
-def dontpass(player, table, unit=5, strat_info=None):
-    # Don't pass bet
-    if table.point == "Off" and not player.has_bet("DontPass"):
-        player.bet(DontPass(unit))
+def dontpass(player: 'Player', table: 'Table') -> None:
+    """ Place a bet on the Don't Pass line.
 
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
 
-def layodds(player, table, unit=5, strat_info=None, win_mult=1):
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    # Don't pass bet
+    if table.point == "Off" and not player.has_bet("DontPass"):
+        player.bet(DontPass(player.unit))
+
+
+def layodds(player: 'Player', table: 'Table', win_mult: int | str = 1) -> None:
+    """ Place a lay bet.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        win_mult: str | int
+            Either '345' or an integer that will be multiplied against the odds to calculate bet.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
     # Assume that someone tries to win the `win_mult` times the unit on each bet, which corresponds
     # well to the max_odds on a table.
     # For `win_mult` = "345", this assumes max of 3-4-5x odds
-    dontpass(player, table, unit)
-
+    dontpass(player, table)
+    mult = 1
     # Lay odds for don't pass
     if win_mult == "345":
         mult = 6.0
@@ -136,11 +286,11 @@ def layodds(player, table, unit=5, strat_info=None, win_mult=1):
                 mult = 6 / 5 * win_mult
 
     if (
-        table.point == "On"
-        and player.has_bet("DontPass")
-        and not player.has_bet("LayOdds")
+            table.point == "On"
+            and player.has_bet("DontPass")
+            and not player.has_bet("LayOdds")
     ):
-        player.bet(LayOdds(mult * unit, player.get_bet("DontPass")))
+        player.bet(LayOdds(mult * player.unit, player.get_bet("DontPass")))
 
 
 """
@@ -148,10 +298,24 @@ def layodds(player, table, unit=5, strat_info=None, win_mult=1):
 """
 
 
-def place68_2come(player, table, unit=5, strat_info=None):
+def place68_2come(player: 'Player', table: 'Table') -> None:
+    """ Once point is established, place 6 and 8, with 2 additional come bets.
+        The goal is to be on four distinct numbers, moving place bets if necessary
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
     """
-    Once point is established, place 6 and 8, with 2 additional come bets.
-    The goal is to be on four distinct numbers, moving place bets if necessary
+
     """
     current_numbers = []
     for bet in player.bets_on_table:
@@ -161,18 +325,18 @@ def place68_2come(player, table, unit=5, strat_info=None):
     if table.point == "On" and len(player.bets_on_table) < 4:
         # always place 6 and 8 when they aren't come bets or place bets already
         if 6 not in current_numbers:
-            player.bet(Place6(6 / 5 * unit))
+            player.bet(Place6(6 / 5 * player.unit))
         if 8 not in current_numbers:
-            player.bet(Place8(6 / 5 * unit))
+            player.bet(Place8(6 / 5 * player.unit))
 
     # add come of passline bets to get on 4 numbers
     if player.num_bet("Come", "PassLine") < 2 and len(player.bets_on_table) < 4:
         if table.point == "On":
-            player.bet(Come(unit))
+            player.bet(Come(player.unit))
         if table.point == "Off" and (
-            player.has_bet("Place6") or player.has_bet("Place8")
+                player.has_bet("Place6") or player.has_bet("Place8")
         ):
-            player.bet(PassLine(unit))
+            player.bet(PassLine(player.unit))
 
     # if come bet or passline goes to 6 or 8, move place bets to 5 or 9
     pass_come_winning_numbers = []
@@ -185,235 +349,305 @@ def place68_2come(player, table, unit=5, strat_info=None):
         if player.has_bet("Place6"):
             player.remove(player.get_bet("Place6"))
         if 5 not in current_numbers:
-            player.bet(Place5(unit))
+            player.bet(Place5(player.unit))
         elif 9 not in current_numbers:
-            player.bet(Place9(unit))
+            player.bet(Place9(player.unit))
     elif 8 in pass_come_winning_numbers:
         if player.has_bet("Place8"):
             player.remove(player.get_bet("Place8"))
         if 5 not in current_numbers:
-            player.bet(Place5(unit))
+            player.bet(Place5(player.unit))
         elif 9 not in current_numbers:
-            player.bet(Place9(unit))
-
-
-def ironcross(player, table, unit=5, strat_info=None):
-    passline(player, table, unit)
-    passline_odds(player, table, unit, strat_info=None, mult=2)
-    place(player, table, 2 * unit, strat_info={"numbers": {5, 6, 8}})
+            player.bet(Place9(player.unit))
+
+
+def ironcross(player: 'Player', table: 'Table', mult: int | str = 1) -> None:
+    """ Bet the Pass Line, the Pass Line Odds, and Place on 5, 6, and 8. If point is on and no bet on the field, place
+        a bet on the field.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        mult
+            Either '345' or an integer that will be multiplied against the odds to calculate bet.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
+    passline_odds(player, table, mult)
+    place(player, table, numbers={5, 6, 8})
 
     if table.point == "On":
         if not player.has_bet("Field"):
-            player.bet(
-                Field(
-                    unit,
-                    double=table.payouts["fielddouble"],
-                    triple=table.payouts["fieldtriple"],
-                )
-            )
-
-
-def hammerlock(player, table, unit=5, strat_info=None):
-    passline(player, table, unit)
-    layodds(player, table, unit, win_mult="345")
+            player.bet(Field(
+                player.unit,
+                double=table.payouts["fielddouble"],
+                triple=table.payouts["fieldtriple"],
+            ))
+
+
+def hammerlock(player: 'Player', table: 'Table', mode: str | None = None) -> dict[str, str]:
+    """ Pass Line Bet, Don't Pass bet with a lay of odds. A phased place bet approach, starting inside and then
+        shifting outside eventually taking bet down if two place bets win.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        mode: str
+            Current phase of the Hammerlock, either "place68", "place_inside" or "takedown"
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
+    layodds(player, table, win_mult="345")
 
     place_nums = set()
     for bet in player.bets_on_table:
         if isinstance(bet, Place):
             place_nums.add(bet.winning_numbers[0])
     place_point_nums = place_nums.copy()
-    place_point_nums.add(table.point.number)
+    if table.point.number:
+        place_point_nums.add(table.point.number)
 
     has_place68 = (6 in place_nums) or (8 in place_nums)
     has_place5689 = (
-        (5 in place_nums) or (6 in place_nums) or (8 in place_nums) or (9 in place_nums)
+            (5 in place_nums) or (6 in place_nums) or (8 in place_nums) or (9 in place_nums)
     )
-
     # 3 phases, place68, place_inside, takedown
-    if strat_info is None or table.point == "Off":
-        strat_info = {"mode": "place68"}
+    if mode is None or table.point == "Off":
+        mode = 'place68'
         for bet_nm in ["Place5", "Place6", "Place8", "Place9"]:
             player.remove_if_present(bet_nm)
 
-    if strat_info["mode"] == "place68":
+    if mode == "place68":
         if table.point == "On" and has_place68 and place_nums != {6, 8}:
             # assume that a place 6/8 has won
             if player.has_bet("Place6"):
                 player.remove(player.get_bet("Place6"))
             if player.has_bet("Place8"):
                 player.remove(player.get_bet("Place8"))
-            strat_info["mode"] = "place_inside"
-            place(
-                player,
-                table,
-                unit,
-                strat_info={"numbers": {5, 6, 8, 9}},
-                skip_point=False,
-            )
+            mode = "place_inside"
+            place(player, table, numbers={5, 6, 8, 9}, skip_point=False)
         else:
-            place(
-                player,
-                table,
-                2 * unit,
-                strat_info={"numbers": {6, 8}},
-                skip_point=False,
-            )
-    elif strat_info["mode"] == "place_inside":
+            place(player, table, numbers={6, 8}, skip_point=False)
+    elif mode == "place_inside":
         if table.point == "On" and has_place5689 and place_nums != {5, 6, 8, 9}:
             # assume that a place 5/6/8/9 has won
             for bet_nm in ["Place5", "Place6", "Place8", "Place9"]:
                 player.remove_if_present(bet_nm)
-            strat_info["mode"] = "takedown"
+            mode = "takedown"
         else:
-            place(
-                player,
-                table,
-                unit,
-                strat_info={"numbers": {5, 6, 8, 9}},
-                skip_point=False,
-            )
-    elif strat_info["mode"] == "takedown" and table.point == "Off":
-        strat_info = None
+            place(player, table, numbers={5, 6, 8, 9}, skip_point=False)
+    elif mode == "takedown" and table.point == "Off":
+        mode = None
+
+    player.strat_info['mode'] = mode
+
 
-    return strat_info
+def risk12(player: 'Player', table: 'Table', winnings: typing.SupportsFloat = 0) -> dict[str, int]:
+    """ Pass line and field bet before the point is established. Once the point is established place the 6 and 8.
 
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        winnings: float
+            Amount that has been won thus far.
 
-def risk12(player, table, unit=5, strat_info=None):
-    passline(player, table, unit)
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline(player, table)
 
     if table.pass_rolls == 0:
-        strat_info = {"winnings": 0}
-    elif table.point == "Off":
+        winnings = 0
+
+    if table.point == "Off":
         if table.last_roll in table.payouts["fielddouble"]:
             # win double from the field, lose pass line, for a net of 1 unit win
-            strat_info["winnings"] += unit
+            winnings += player.unit
         elif table.last_roll in table.payouts["fieldtriple"]:
             # win triple from the field, lose pass line, for a net of 2 unit win
-            strat_info["winnings"] += 2 * unit
+            winnings += 2 * player.unit
         elif table.last_roll == 11:
             # win the field and pass line, for a net of 2 units win
-            strat_info["winnings"] += 2 * unit
+            winnings += 2 * player.unit
 
     if table.point == "Off":
-        player.bet(
-            Field(
-                unit,
-                double=table.payouts["fielddouble"],
-                triple=table.payouts["fieldtriple"],
-            )
-        )
+        player.bet(Field(
+            player.unit,
+            double=table.payouts["fielddouble"],
+            triple=table.payouts["fieldtriple"],
+        ))
         if table.last_roll == 7:
             for bet_nm in ["Place6", "Place8"]:
                 player.remove_if_present(bet_nm)
     elif table.point.number in [4, 9, 10]:
-        place(player, table, unit, strat_info={"numbers": {6, 8}})
+        place(player, table, numbers={6, 8})
     elif table.point.number in [5, 6, 8]:
         # lost field bet, so can't automatically cover the 6/8 bets.  Need to rely on potential early winnings
-        if strat_info["winnings"] >= 2 * unit:
-            place(player, table, unit, strat_info={"numbers": {6, 8}})
-        elif strat_info["winnings"] >= 1 * unit:
+        if winnings >= 2 * player.unit:
+            place(player, table, numbers={6, 8})
+        elif winnings >= 1 * player.unit:
             if table.point.number != 6:
-                place(player, table, unit, strat_info={"numbers": {6}})
+                place(player, table, numbers={6})
             else:
-                place(player, table, unit, strat_info={"numbers": {8}})
-
-    return strat_info
-
-
-def knockout(player, table, unit=5, strat_info=None):
-    passline_odds345(player, table, unit)
-    dontpass(player, table, unit)
-
-
-def dicedoctor(player, table, unit=5, strat_info=None):
-    if strat_info is None or table.last_roll in Field(0).losing_numbers:
-        strat_info = {"progression": 0}
-    else:
-        strat_info["progression"] += 1
+                place(player, table, numbers={8})
+
+    player.strat_info['winnings'] = winnings
+
+
+def knockout(player: 'Player', table: 'Table') -> None:
+    """ Pass line bet, don't pass bet, 345x odds behind the pass line bet.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    passline_odds345(player, table)
+    dontpass(player, table)
+
+
+def dicedoctor(player: 'Player', table: 'Table', progression: int = 0) -> dict[str, int]:
+    """ Field bet with a progression if you win.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+        progression
+            How far into the progression list the strategy is
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    if table.last_roll in Field(0).losing_numbers:
+        progression = 0
 
     bet_progression = [10, 20, 15, 30, 25, 50, 35, 70, 50, 100, 75, 150]
-    prog = strat_info["progression"]
-    if prog < len(bet_progression):
-        amount = bet_progression[prog] * unit / 5
-    elif prog % 2 == 0:
+    if progression < len(bet_progression):
+        amount = bet_progression[progression] * player.unit / 5
+    elif progression % 2 == 0:
         # alternate between second to last and last
-        amount = bet_progression[len(bet_progression) - 2] * unit / 5
+        amount = bet_progression[len(bet_progression) - 2] * player.unit / 5
     else:
-        amount = bet_progression[len(bet_progression) - 1] * unit / 5
-
-    player.bet(
-        Field(
-            amount,
-            double=table.payouts["fielddouble"],
-            triple=table.payouts["fieldtriple"],
-        )
-    )
-
-    return strat_info
-
-
-def place68_cpr(player, table, unit=5, strat_info=None):
-    """ place 6 & 8 after point is establish.  Then collect, press, and regress (in that order) on each win """
-    ## NOTE: NOT WORKING
-    if strat_info is None:
-        strat_info = {"mode6": "collect", "mode8": "collect"}
-
-    if table.point == "On":
-        # always place 6 and 8 when they aren't place bets already
-        if not player.has_bet("Place6"):
-            player.bet(Place6(6 / 5 * unit))
-        if not player.has_bet("Place8"):
-            player.bet(Place8(6 / 5 * unit))
-
-    if table.bet_update_info is not None:
-        # place6
-        if player.has_bet("Place6"):
-            bet = player.get_bet("Place6")
-            if (
-                table.bet_update_info[player].get(bet.name) is not None
-            ):  # bet has not yet been updated; skip
-                # print("level3")
-                # print(table.bet_update_info[player][bet.name])
-                if table.bet_update_info[player][bet.name]["status"] == "win":
-                    # print("place6 mode: {}".format(strat_info["mode6"]))
-                    if strat_info["mode6"] == "press":
-                        player.remove(bet)
-                        player.bet(Place6(2 * bet.bet_amount))
-                        strat_info["mode6"] = "regress"
-                    elif strat_info["mode6"] == "regress":
-                        player.remove(bet)
-                        player.bet(Place6(6 / 5 * unit))
-                        strat_info["mode6"] = "collect"
-                    elif strat_info["mode6"] == "collect":
-                        strat_info["mode6"] = "press"
-                    # print("updated place6 mode: {}".format(strat_info["mode6"]))
-        # place8
-        if player.has_bet("Place8"):
-            bet = player.get_bet("Place8")
-            if (
-                table.bet_update_info[player].get(bet.name) is not None
-            ):  # bet has not yet been updated; skip
-                # print("level3")
-                # print(table.bet_update_info[player][bet.name])
-                if table.bet_update_info[player][bet.name]["status"] == "win":
-                    # print("place8 mode: {}".format(strat_info["mode8"]))
-                    if strat_info["mode8"] == "press":
-                        player.remove(bet)
-                        player.bet(Place8(2 * bet.bet_amount))
-                        strat_info["mode8"] = "regress"
-                    elif strat_info["mode8"] == "regress":
-                        player.remove(bet)
-                        player.bet(Place8(6 / 5 * unit))
-                        strat_info["mode8"] = "collect"
-                    elif strat_info["mode8"] == "collect":
-                        strat_info["mode8"] = "press"
-
-    print(strat_info)
-    return strat_info
-
-def place68_dontcome2odds(player, table, unit=5, strat_info=None): 
-    
-    place(player, table, unit, strat_info={"numbers": {6, 8}}, skip_point=False)
+        amount = bet_progression[len(bet_progression) - 1] * player.unit / 5
+
+    player.bet(Field(
+        amount,
+        double=table.payouts["fielddouble"],
+        triple=table.payouts["fieldtriple"],
+    ))
+
+    progression += 1
+
+    player.strat_info['progression'] = progression
+
+
+# def place68_cpr(player: 'Player', table: 'Table', unit: int = 5, strat_info: dict[]=None) -> dict[str, str]:
+#     """ place 6 & 8 after point is establish.  Then collect, press, and regress (in that order) on each win """
+#     ## NOTE: NOT WORKING
+#     if strat_info is None:
+#         strat_info = {"mode6": "collect", "mode8": "collect"}
+#
+#     if table.point == "On":
+#         # always place 6 and 8 when they aren't place bets already
+#         if not player.has_bet("Place6"):
+#             player.bet(Place6(6 / 5 * unit))
+#         if not player.has_bet("Place8"):
+#             player.bet(Place8(6 / 5 * unit))
+#
+#     if table.bet_update_info is not None:
+#         # place6
+#         if player.has_bet("Place6"):
+#             bet = player.get_bet("Place6")
+#             if (
+#                 table.bet_update_info[player].get(bet.name) is not None
+#             ):  # bet has not yet been updated; skip
+#                 # print("level3")
+#                 # print(table.bet_update_info[player][bet.name])
+#                 if table.bet_update_info[player][bet.name]["status"] == "win":
+#                     # print("place6 mode: {}".format(strat_info["mode6"]))
+#                     if strat_info["mode6"] == "press":
+#                         player.remove(bet)
+#                         player.bet(Place6(2 * bet.bet_amount))
+#                         strat_info["mode6"] = "regress"
+#                     elif strat_info["mode6"] == "regress":
+#                         player.remove(bet)
+#                         player.bet(Place6(6 / 5 * unit))
+#                         strat_info["mode6"] = "collect"
+#                     elif strat_info["mode6"] == "collect":
+#                         strat_info["mode6"] = "press"
+#                     # print("updated place6 mode: {}".format(strat_info["mode6"]))
+#         # place8
+#         if player.has_bet("Place8"):
+#             bet = player.get_bet("Place8")
+#             if (
+#                 table.bet_update_info[player].get(bet.name) is not None
+#             ):  # bet has not yet been updated; skip
+#                 # print("level3")
+#                 # print(table.bet_update_info[player][bet.name])
+#                 if table.bet_update_info[player][bet.name]["status"] == "win":
+#                     # print("place8 mode: {}".format(strat_info["mode8"]))
+#                     if strat_info["mode8"] == "press":
+#                         player.remove(bet)
+#                         player.bet(Place8(2 * bet.bet_amount))
+#                         strat_info["mode8"] = "regress"
+#                     elif strat_info["mode8"] == "regress":
+#                         player.remove(bet)
+#                         player.bet(Place8(6 / 5 * unit))
+#                         strat_info["mode8"] = "collect"
+#                     elif strat_info["mode8"] == "collect":
+#                         strat_info["mode8"] = "press"
+#
+#     print(strat_info)
+#     return strat_info
+
+
+def place68_dontcome2odds(player: 'Player', table: 'Table') -> None:
+    """ Place the 6 and 8, bet don't come with 2x odds.
+
+        Parameters
+        ----------
+        player
+            Player object that is using the strategy.
+        table
+            Table object that the strategy is being used on.
+
+        Returns
+        -------
+        None
+            Dictionary of strategy info.
+        """
+    place(player, table, numbers={6, 8}, skip_point=False)
 
     current_numbers = []
     for bet in player.bets_on_table:
@@ -423,26 +657,28 @@ def place68_dontcome2odds(player, table, unit=5, strat_info=None):
     dont_come_losing_numbers = []
     if player.has_bet("DontCome"):
         dont_come_losing_numbers += player.get_bet("DontCome", "Any").losing_numbers
-    
+
     if 6 in dont_come_losing_numbers:
         if player.has_bet("Place6"):
             player.remove(player.get_bet("Place6"))
         if 5 not in current_numbers:
-            player.bet(Place5(unit))
+            player.bet(Place5(player.unit))
     elif 8 in dont_come_losing_numbers:
         if player.has_bet("Place8"):
             player.remove(player.get_bet("Place8"))
         if 9 not in current_numbers:
-            player.bet(Place9(unit))
-    
-    
+            player.bet(Place9(player.unit))
+
     if table.point == "On" and player.num_bet("DontCome") < 1:
-        player.bet(DontCome(unit))
+        player.bet(DontCome(player.unit))
 
     if player.has_bet("DontCome"):
-        dc = player.get_bet("DontCome", "Any")
+        dc: Bet = player.get_bet("DontCome", "Any")
 
-        win_mult = 2
+        if not isinstance(dc, DontCome):
+            raise TypeError
+        mult = 1
+        win_mult: int | float = 2
         # Lay odds for don't come
         if win_mult == "345":
             mult = 6.0
@@ -459,20 +695,22 @@ def place68_dontcome2odds(player, table, unit=5, strat_info=None):
                     mult = 6 / 5 * win_mult
 
         if not player.has_bet("LayOdds") and not dc.prepoint:
-            player.bet(LayOdds(mult * unit, dc))
+            player.bet(LayOdds(mult * player.unit, dc))
+
 
 if __name__ == "__main__":
     # Test a betting strategy
 
-    from player import Player
-    from dice import Dice
-    from table import Table
+    from crapssim.player import Player
+    from crapssim.dice import Dice
+    from crapssim.table import Table
 
     # table = CrapsTable()
     # table._add_player(Player(500, place68_2come))
 
     d = Dice()
     p = Player(500, place68_2come)
+    t = Table()
     p.bet(PassLine(5))
     p.bet(Place6(6))
     print(p.bets_on_table)
diff --git a/crapssim/table.py b/crapssim/table.py
index eefe7436..e3e79e72 100644
--- a/crapssim/table.py
+++ b/crapssim/table.py
@@ -1,3 +1,5 @@
+import typing
+
 from crapssim.dice import Dice
 from crapssim.player import Player
 
@@ -9,66 +11,108 @@ class Table(object):
     table until a specified number of rolls plays out or all players run out
     of money.
 
-    Parameters
-    ----------
-    NONE
-
     Attributes
     ----------
     players : list
         List of player objects at the table
-    total_player_cash : float
-        Sum of all players bankroll and bets on table
     point : string
         The point for the table.  It is either "Off" when point is off or "On"
         when point is on.
-    point_number : int
-        The point number when point is "On" and None when point is "Off"
-    player_has_bets : bool
-        Boolean value for whether any player has a bet on the table.
-    strat_info : dictionary
-        Contains information stored from the strategy, usually mean for
-        strategies that alter based on past information
     bet_update_info : dictionary
         Contains information from updating bets, for given player and a bet
         name, this is status of last bet (win/loss), and win amount.
+    dice : Dice
+        Dice for the table
+    payouts : dice[str, list[int]]
+        Field payouts for the table
+    pass_rolls : int
+        Number of rolls for the current pass
+    last_roll : int
+        Total of the last roll for the table
+    n_shooters : int
+        How many shooters the table has had.
     """
 
-    def __init__(self):
-        self.players = []
-        self.player_has_bets = False
-        # TODO: I think strat_info should be attached to each player object
-        self.strat_info = {}
-        self.point = _Point()
-        self.dice = Dice()
-        self.bet_update_info = None
-        self.payouts = {"fielddouble": [2, 12], "fieldtriple": []}
-        self.pass_rolls = 0
-        self.last_roll = None
-        self.n_shooters = 1
+    def __init__(self) -> None:
+        self.players: list[Player] = []
+        self.point: Point = Point()
+        self.dice: Dice = Dice()
+        self.bet_update_info: dict | None = None
+        self.payouts: dict[str, list[int]] = {"fielddouble": [2, 12], "fieldtriple": []}
+        self.pass_rolls: int = 0
+        self.last_roll: int | None = None
+        self.n_shooters: int = 1
 
     @classmethod
-    def with_payouts(cls, **kwagrs):
+    def with_payouts(cls, **kwargs: list[int]) -> 'Table':
+        """ Return a table with the payouts specified in **kwargs.
+
+        Parameters
+        ----------
+        **kwargs : list[int]
+            The tables payouts.
+
+        Returns
+        -------
+        Table
+            The table with the specified payouts.
+
+        """
         table = cls()
-        for name, value in kwagrs.items():
+        for name, value in kwargs.items():
             table.payouts[name] = value
         return table
 
-    def set_payouts(self, name, value):
+    def set_payouts(self, name: str, value: list[int]) -> None:
+        """ Set a payout.
+
+        Parameters
+        ----------
+        name : str
+            Name of the payout.
+        value : list[int]
+            Rolls for the payout.
+        """
         self.payouts[name] = value
 
-    def add_player(self, player_object):
-        """ Add player object to the table """
+    def add_player(self, player_object: Player) -> None:
+        """ Add player object to the table
+
+        Parameters
+        ----------
+        player_object : Player
+            Player object to add to the table.
+        """
         if player_object not in self.players:
             self.players.append(player_object)
-            self.strat_info[player_object] = None
+        player_object.table = self
+
+    def _setup_run(self, verbose: bool) -> None:
+        """
+        Setup the table to run and ensure that there is at least one player.
+
+        Parameters
+        ----------
+        verbose
+            If True prints a welcome message and the initial players.
+        """
+        if verbose:
+            print("Welcome to the Craps Table!")
+        self.ensure_one_player()
+        if verbose:
+            print(f"Initial players: {[p.name for p in self.players]}")
 
-    def run(self, max_rolls, max_shooter=float("inf"), verbose=True, runout=False):
+    def run(self, max_rolls: int,
+            max_shooter: float | int = float("inf"),
+            verbose: bool = True,
+            runout: bool = False) -> None:
         """
         Runs the craps table until a stopping condition is met.
 
         Parameters
         ----------
+        max_shooter : float | int
+            Maximum number of shooters to run for
         max_rolls : int
             Maximum number of rolls to run for
         verbose : bool
@@ -76,99 +120,226 @@ def run(self, max_rolls, max_shooter=float("inf"), verbose=True, runout=False):
         runout : bool
             If true, continue past max_rolls until player has no more bets on the table
         """
-        # self.dice = Dice()
-        if verbose:
-            print("Welcome to the Craps Table!")
-
-        # make sure at least one player is at table
-        if not self.players:
-            self.add_player(Player(500, "Player1"))
-        if verbose:
-            print(f"Initial players: {[p.name for p in self.players]}")
 
-        # maybe wrap this into update table or something
-        self.total_player_cash = sum(
-            [p.total_bet_amount + p.bankroll for p in self.players]
-        )
+        self._setup_run(verbose)
 
         continue_rolling = True
         while continue_rolling:
 
-            # players make their bets
-            self._add_player_bets()
-            for p in self.players:
-                bets = [
-                    f"{b.name}{b.subname}, ${b.bet_amount}" for b in p.bets_on_table
-                ]
-                if verbose:
-                    print(f"{p.name}'s current bets: {bets}")
+            self.add_player_bets(verbose=verbose)
+            self.roll_and_update(verbose)
 
-            self.dice.roll()
-            if verbose:
-                print("")
-                print("Dice out!")
-                print(f"Shooter rolled {self.dice.total} {self.dice.result}")
-            self._update_player_bets(self.dice, verbose)
-            self._update_table(self.dice)
-            if verbose:
-                print(f"Point is {self.point.status} ({self.point.number})")
-                print(f"Total Player Cash is ${self.total_player_cash}")
+            continue_rolling = self.should_keep_rolling(max_rolls, max_shooter, runout)
 
-            # evaluate the stopping condition
-            if runout:
-                continue_rolling = (
-                    self.dice.n_rolls < max_rolls
+    def fixed_run(self, dice_outcomes: typing.Iterable[typing.Iterable], verbose: bool = False) -> None:
+        """
+        Give a series of fixed dice outcome and run as if that is what was rolled.
+
+        Parameters
+        ----------
+        dice_outcomes
+            Iterable with two integers representing the dice faces.
+        verbose
+            If true, print results from table during each roll
+        """
+
+        for dice_outcome in dice_outcomes:
+            self.add_player_bets(verbose=verbose)
+            self.fixed_roll_and_update(dice_outcome)
+
+    def roll_and_update(self, verbose: bool = False) -> None:
+        """
+        Roll dice, update player bets, and update table.
+
+        Parameters
+        ----------
+        verbose
+            If true, prints out information about the roll and the bets
+        """
+        self.roll(verbose=verbose)
+        self.update_player_bets(verbose=verbose)
+        self.update_table(verbose=verbose)
+
+    def fixed_roll_and_update(self, dice_outcome: typing.Iterable[int], verbose: bool = False) -> None:
+        """
+        Roll dice with fixed dice_outcome, update player bets, and update table.
+
+        Parameters
+        ----------
+        dice_outcome
+            Iterable of the two integers representing the chosen dice faces.
+        verbose
+            If true, prints out information about the roll and the bets
+        """
+        self.fixed_roll(dice_outcome=dice_outcome, verbose=verbose)
+        self.update_player_bets(verbose=verbose)
+        self.update_table(verbose=verbose)
+
+    def roll(self, verbose: bool = False) -> None:
+        """
+        Convenience method to roll the dice with two random numbers.
+
+        Parameters
+        ----------
+        verbose
+            If true, prints out that the Dice are out and what number the shooter rolled.
+
+        """
+        self.dice.roll()
+
+        if verbose:
+            print("")
+            print("Dice out!")
+            print(f"Shooter rolled {self.dice.total} {self.dice.result}")
+
+    def fixed_roll(self, dice_outcome: typing.Iterable[int], verbose=False) -> None:
+        """
+        Convenience method to roll the dice with two fixed numbers.
+
+        Parameters
+        ----------
+        verbose
+            If true, prints out that the Dice are out and what number the shooter rolled.
+        dice_outcome
+            Iterable of two integers representing the chosen dice faces.
+        """
+        self.dice.fixed_roll(dice_outcome)
+
+        if verbose:
+            print("")
+            print("Dice out!")
+            print(f"Shooter rolled {self.dice.total} {self.dice.result}")
+
+    def should_keep_rolling(self, max_rolls: int, max_shooter: int, runout: bool) -> bool:
+        """
+        Determines whether the program should keep running or not.
+
+        Parameters
+        ----------
+        max_rolls
+            Maximum number of rolls to run for
+        max_shooter
+            Maximum number of shooters to run for
+        runout
+            If true, continue past max_rolls until player has no more bets on the table
+
+        Returns
+        -------
+        If True, the program should continue running. If False the program should stop running.
+        """
+        if runout:
+            return (self.dice.n_rolls < max_rolls
                     and self.n_shooters <= max_shooter
-                    and self.total_player_cash > 0
-                ) or self.player_has_bets
-            else:
-                continue_rolling = (
+                    and all(x.bankroll > x.unit for x in self.players)
+                    ) or self.player_has_bets
+        else:
+            return (
                     self.dice.n_rolls < max_rolls
                     and self.n_shooters <= max_shooter
-                    and self.total_player_cash > 0
-                )
+                    and all(x.bankroll > x.unit for x in self.players)
+            )
 
-    def _add_player_bets(self):
-        """ Implement each player's betting strategy """
-        """ TODO: restrict bets that shouldn't be possible based on table"""
-        """ TODO: Make the unit parameter specific to each player, and make it more general """
+    def ensure_one_player(self) -> None:
+        """ Make sure there is at least one player at the table
+        """
+        if len(self.players) == 0:
+            self.add_player(Player(500.0, name="Player1"))
+
+    def add_player_bets(self, verbose: bool = False) -> None:
+        """ Implement each player's betting strategy.
+
+        Parameters
+        ----------
+        verbose
+            If True, print the players current bets.
+        """
         for p in self.players:
-            self.strat_info[p] = p._add_strategy_bets(
-                self, unit=5, strat_info=self.strat_info[p]
-            )  # unit = 10 to change unit
-            # TODO: add player.strat_kwargs as optional parameter (currently manually changed in CrapsTable)
+            p.add_strategy_bets()
+
+            if verbose:
+                bets = [f"{b.name}{b.subname}: ${b.bet_amount}" for b in p.bets_on_table]
+                if verbose:
+                    print(f"{p.name}'s current bets: {bets}")
 
-    def _update_player_bets(self, dice, verbose=False):
-        """ check bets for wins/losses, payout wins to their bankroll, remove bets that have resolved """
+    def update_player_bets(self, verbose: bool = False) -> None:
+        """ Check bets for wins/losses, payout wins to their bankroll, remove bets that have resolved
+
+        Parameters
+        ----------
+        verbose : bool
+            If True, prints whether the player won, lost, etc and the amount
+        """
         self.bet_update_info = {}
         for p in self.players:
-            info = p._update_bet(self, dice, verbose)
+            info = p.update_bet(verbose)
             self.bet_update_info[p] = info
 
-    def _update_table(self, dice):
-        """ update table attributes based on previous dice roll """
+    def update_table(self, verbose: bool = False) -> None:
+        """ update table attributes based on previous dice roll
+
+        Parameters
+        ----------
+        verbose
+            If true, prints out the point and the players total cash
+        """
         self.pass_rolls += 1
-        if self.point == "On" and dice.total == 7:
+        if self.point == "On" and self.dice.total == 7:
             self.n_shooters += 1
-        if self.point == "On" and (dice.total == 7 or dice.total == self.point.number):
+        if self.point == "On" and (self.dice.total == 7 or self.dice.total == self.point.number):
             self.pass_rolls = 0
 
         self.point.update(self.dice)
-        self.total_player_cash = sum(
-            [p.total_bet_amount + p.bankroll for p in self.players]
-        )
-        self.player_has_bets = sum([len(p.bets_on_table) for p in self.players]) >= 1
-        self.last_roll = dice.total
-
-    def _get_player(self, player_name):
-        [p for p in self.players if p.name == player_name]
+        self.last_roll = self.dice.total
+
+        if verbose:
+            print(f"Point is {self.point.status} ({self.point.number})")
+            print(f"Total Player Cash is ${self.total_player_cash}")
+
+    def get_player(self, player_name: str) -> typing.Union['Player', bool]:
+        """
+        Given the name of a player return the player object.
+
+        Parameters
+        ----------
+        player_name : str
+            Name of the player
+
+        Returns
+        -------
+        Player, bool
+            If player is found return player, otherwise return False
+
+        """
         for p in self.players:
             if p.name == player_name:
                 return p
         return False
 
+    @property
+    def player_has_bets(self) -> bool:
+        """
+        Returns whether any of the players on the table have any active bets.
+
+        Returns
+        -------
+        True if any of the players have bets on the table, otherwise False.
+
+        """
+        return sum([len(p.bets_on_table) for p in self.players]) > 0
+
+    @property
+    def total_player_cash(self) -> float:
+        """
+        Returns the total sum of all players total_bet_amounts and bankroll.
 
-class _Point(object):
+        Returns
+        -------
+        The total sum of all players total_bet_amounts and bankroll.
+        """
+        return sum([p.total_bet_amount + p.bankroll for p in self.players])
+
+
+class Point:
     """
     The point on a craps table.
 
@@ -184,14 +355,55 @@ class _Point(object):
         The point number (in [4, 5, 6, 8, 9, 10]) is status == 'On'
     """
 
-    def __init__(self):
-        self.status = "Off"
-        self.number = None
-
-    def __eq__(self, other):
-        return self.status == other
+    def __init__(self) -> None:
+        self.status: str = "Off"
+        self.number: int | None = None
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, str):
+            return self.status.lower() == other.lower() or str(self.number) == other
+        elif isinstance(other, int) and other in (4, 5, 6, 8, 9, 10):
+            return other == self.number
+        elif isinstance(other, Point):
+            return other.status == self.status and other.number == self.number
+        else:
+            raise NotImplementedError
+
+    def __gt__(self, other: object) -> bool:
+        if isinstance(other, str):
+            return self.number > int(other)
+        elif isinstance(other, int):
+            return self.number > other
+        elif isinstance(other, Point):
+            return self.number > other.number
+        else:
+            raise NotImplementedError
+
+    def __lt__(self, other: object) -> bool:
+        if isinstance(other, str):
+            return self.number < int(other)
+        elif isinstance(other, int):
+            return self.number < other
+        elif isinstance(other, Point):
+            return self.number < other.number
+        else:
+            raise NotImplementedError
+
+    def __ge__(self, other: object) -> bool:
+        return self.__eq__(other) or self.__gt__(other)
+
+    def __le__(self, other: object) -> bool:
+        return self.__eq__(other) or self.__lt__(other)
+
+    def update(self, dice_object: Dice) -> None:
+        """
+        Given a Dice object update the points status and number.
 
-    def update(self, dice_object: Dice):
+        Parameters
+        ----------
+        dice_object : Dice
+            The Dice you want to update the point with
+        """
         if self.status == "Off" and dice_object.total in [4, 5, 6, 8, 9, 10]:
             self.status = "On"
             self.number = dice_object.total
@@ -204,7 +416,7 @@ def update(self, dice_object: Dice):
     import sys
 
     # import strategy
-    from crapssim import strategy
+    from crapssim.strategy import dicedoctor
 
     sim = False
     printout = True
@@ -213,7 +425,7 @@ def update(self, dice_object: Dice):
     n_roll = 144
     n_shooter = 2
     bankroll = 1000
-    strategy = strategy.dicedoctor
+    strategy = dicedoctor
     strategy_name = "dicedoctor"  # don't include any "_" in this
     runout = True
     runout_str = "-runout" if runout else ""


Test patch:
diff --git a/Python/craps-test.py b/Python/craps-test.py
index 68e2ea96..21929f75 100644
--- a/Python/craps-test.py
+++ b/Python/craps-test.py
@@ -38,7 +38,7 @@
     Sean.bet(craps.bet.Come(5))
 
     d.fixed_roll([4, 4])
-    Sean._update_bet(None, d)
+    Sean.update_bet()
     Sean.bet(craps.bet.Come(10))
     print("Sean's current bets: {}".format(
         {b.name: b.bet_amount for b in Sean.bets_on_table}
diff --git a/tests/test_bet.py b/tests/test_bet.py
index f99323a5..07d2b8d4 100644
--- a/tests/test_bet.py
+++ b/tests/test_bet.py
@@ -2,7 +2,8 @@
 import crapssim
 import numpy as np
 from crapssim.dice import Dice
-from crapssim.table import Table, _Point
+from crapssim.table import Table, Point
+
 
 # Check EV of bets on a "per-roll" basis
 
@@ -26,12 +27,11 @@
     (crapssim.bet.Hard8(1), -0.0278),
     (crapssim.bet.Hard10(1), -0.0278),
 ])
-
 def test_ev_oneroll(bet, ev):
     d = Dice()
     t = Table()
-    t.point.status = "On" # for place bets to pay properly
-    
+    t.point.status = "On"  # for place bets to pay properly
+
     outcomes = []
     for d1 in range(1, 7):
         for d2 in range(1, 7):
@@ -39,6 +39,5 @@ def test_ev_oneroll(bet, ev):
             status, win_amt = bet._update_bet(t, d)
 
             outcomes.append(win_amt if status == "win" else -1 if status == "lose" else 0)
-    
-    assert round(np.mean(outcomes), 4) == ev
 
+    assert round(np.mean(outcomes), 4) == ev
diff --git a/tests/test_dice.py b/tests/test_dice.py
index 90d185c2..bd9f628e 100644
--- a/tests/test_dice.py
+++ b/tests/test_dice.py
@@ -1,17 +1,21 @@
 import pytest
 from crapssim.dice import Dice
 
+
 @pytest.fixture
 def d1():
     return Dice()
 
+
 def test_no_rolls(d1):
     assert d1.n_rolls == 0
 
+
 def test_one_roll(d1):
     d1.roll()
     assert d1.n_rolls == 1
 
+
 def test_many_roll(d1):
     d1.roll()
     d1.roll()
@@ -19,14 +23,14 @@ def test_many_roll(d1):
     d1.roll()
     assert d1.n_rolls == 4
 
+
 @pytest.mark.parametrize("roll, total", [
-    ([3,4], 7),
-    ([1,1], 2),
-    ([4,3], 7),
-    ([5,6], 11),
+    ([3, 4], 7),
+    ([1, 1], 2),
+    ([4, 3], 7),
+    ([5, 6], 11),
 ])
-
 def test_fixed_roll(d1, roll, total):
     d1.fixed_roll(roll)
     assert d1.result == roll
-    assert d1.total == total
\ No newline at end of file
+    assert d1.total == total
diff --git a/tests/test_player.py b/tests/test_player.py
new file mode 100644
index 00000000..19f21743
--- /dev/null
+++ b/tests/test_player.py
@@ -0,0 +1,40 @@
+from crapssim import Table, Dice
+from crapssim.bet import PassLine
+from crapssim.player import Player
+from crapssim.strategy import passline
+
+
+def test_default_strategy():
+    player = Player(100)
+    assert player.bet_strategy == passline
+
+
+def test_irremovable_bet():
+    player = Player(100)
+    bet = PassLine(50)
+    table = Table()
+    table.add_player(player)
+    player.bet(bet)
+    table = Table()
+    dice = Dice()
+    dice.fixed_roll([2, 2])
+    player.get_bet('PassLine')._update_bet(table, dice)
+    player.remove_if_present('PassLine')
+    assert len(player.bets_on_table) == 1
+
+
+def test_existing_bet():
+    player = Player(100)
+    table = Table()
+    table.add_player(player)
+    bet_one = PassLine(50)
+    player.bet(bet_one)
+    bet_two = PassLine(50)
+    player.bet(bet_two)
+
+    bet_count = len(player.bets_on_table)
+    bet_amount = player.bets_on_table[0].bet_amount
+    bankroll = player.bankroll
+    total_bet_amount = player.total_bet_amount
+
+    assert (bet_count, bet_amount, bankroll, total_bet_amount) == (1, 100, 0, 100)
diff --git a/tests/test_readme.py b/tests/test_readme.py
index bdc388f7..ca6b1a8c 100644
--- a/tests/test_readme.py
+++ b/tests/test_readme.py
@@ -1,28 +1,29 @@
 import pytest
-import crapssim as craps 
+import crapssim as craps
+
 
 def test_first_chunk():
     table = craps.Table()
     your_strat = craps.strategy.passline_odds2
-    you = craps.Player(bankroll=200, bet_strategy=your_strat)
+    you = craps.Player(bankroll=200, bet_strategy=your_strat, table=table)
 
     table.add_player(you)
-    table.run(max_rolls=20)
+    table.run(max_rolls=20, verbose=False)
 
 
 def test_second_chunk():
     n_sim = 10
     bankroll = 300
     strategies = {
-        "place68": craps.strategy.place68, 
-        "ironcross": craps.strategy.ironcross 
+        "place68": craps.strategy.place68,
+        "ironcross": craps.strategy.ironcross
     }
 
     for i in range(n_sim):
-        table = craps.Table() 
+        table = craps.Table()
         for s in strategies:
             table.add_player(craps.Player(bankroll, strategies[s], s))
 
         table.run(max_rolls=float("inf"), max_shooter=10, verbose=False)
         for s in strategies:
-            print(f"{i}, {s}, {table._get_player(s).bankroll}, {bankroll}, {table.dice.n_rolls}")
\ No newline at end of file
+            print(f"{i}, {s}, {table.get_player(s).bankroll}, {bankroll}, {table.dice.n_rolls}")
diff --git a/tests/test_strategy.py b/tests/test_strategy.py
new file mode 100644
index 00000000..1f80c00f
--- /dev/null
+++ b/tests/test_strategy.py
@@ -0,0 +1,116 @@
+from collections import namedtuple
+
+import pytest
+
+from crapssim import Player, Table
+from crapssim.strategy import passline, passline_odds, passline_odds2, passline_odds345, pass2come, place, place68, \
+    dontpass, layodds, place68_2come, ironcross, hammerlock, risk12, knockout, dicedoctor, place68_dontcome2odds
+
+
+@pytest.mark.parametrize(['strategy', 'strategy_info', 'rolls', 'correct_bets'], [
+    (passline, {}, [], {('PassLine', '', 5)}),
+    (passline, {}, [(4, 4)], {('PassLine', '', 5)}),
+    (passline_odds, {}, [], {('PassLine', '', 5)}),
+    (passline_odds, {}, [(4, 4)], {('PassLine', '', 5),
+                                   ('Odds', '8', 5)}),
+    (passline_odds, {}, [(4, 4), (3, 3)], {('PassLine', '', 5),
+                                           ('Odds', '8', 5)}),
+    (passline_odds, {'mult': '345'}, [], {('PassLine', '', 5)}),
+    (passline_odds, {'mult': '345'}, [(6, 4)], {('PassLine', '', 5),
+                                                ('Odds', '10', 15)}),
+    (passline_odds2, {}, [(2, 2)], {('PassLine', '', 5),
+                                    ('Odds', '4', 10)}),
+    (passline_odds345, {}, [(3, 4), (3, 3)], {('PassLine', '', 5),
+                                              ('Odds', '6', 25)}),
+    (pass2come, {}, [], {('PassLine', '', 5)}),
+    (pass2come, {}, [(4, 5)], {('PassLine', '', 5),
+                               ('Come', '', 5)}),
+    (pass2come, {}, [(4, 5), (5, 5)], {('PassLine', '', 5),
+                                       ('Come', '10', 5),
+                                       ('Come', '', 5)}),
+    (pass2come, {}, [(4, 5), (5, 5), (3, 3)], {('PassLine', '', 5),
+                                               ('Come', '10', 5),
+                                               ('Come', '6', 5)}),
+    (place, {'numbers': {4}, 'skip_point': True}, [], set()),
+    (place, {'numbers': {5}, 'skip_point': True}, [(3, 3)], {('Place5', '', 5)}),
+    (place, {'numbers': {5}, 'skip_point': True}, [(3, 2)], set()),
+    (place, {'numbers': {5}, 'skip_point': False}, [(3, 2)], {('Place5', '', 5)}),
+    (place68, {}, [(4, 5)], {('PassLine', '', 5), ('Place6', '', 6),
+                             ('Place8', '', 6)}),
+    (place68, {}, [(2, 4)], {('PassLine', '', 5),
+                             ('Place8', '', 6)}),
+    (dontpass, {}, [], {('DontPass', '', 5)}),
+    (layodds, {'win_mult': 1}, [], {('DontPass', '', 5)}),
+    (layodds, {'win_mult': '345'}, [(3, 3)], {('DontPass', '', 5),
+                                              ('LayOdds', '6', 30)}),
+    (place68_2come, {}, [], set()),
+    (place68_2come, {}, [(3, 3)], {('Place6', '', 6),
+                                   ('Place8', '', 6),
+                                   ('Come', '', 5)}),
+    (place68_2come, {}, [(3, 3), (3, 6)], {('Place6', '', 6),
+                                           ('Place8', '', 6),
+                                           ('Come', '9', 5),
+                                           ('Come', '', 5)}),
+    (place68_2come, {}, [(3, 3), (4, 4)], {('Place6', '', 6),
+                                           ('Place5', '', 5),
+                                           ('Come', '8', 5),
+                                           ('Come', '', 5)}),
+    (ironcross, {}, [], {('PassLine', '', 5)}),
+    (ironcross, {'mult': '2'}, [(4, 4)], {('PassLine', '', 5),
+                                          ('Odds', '8', 10),
+                                          ('Place5', '', 5),
+                                          ('Place6', '', 6),
+                                          ('Field', '', 5)}),
+    (hammerlock, {}, [], {('PassLine', '', 5), ('DontPass', '', 5)}),
+    (hammerlock, {}, [(3, 3)], {('PassLine', '', 5),
+                                ('DontPass', '', 5),
+                                ('Place6', '', 6),
+                                ('Place8', '', 6),
+                                ('LayOdds', '6', 30)}),
+    (hammerlock, {}, [(3, 3), (4, 4)], {('PassLine', '', 5),
+                                        ('DontPass', '', 5),
+                                        ('Place6', '', 6),
+                                        ('Place8', '', 6),
+                                        ('LayOdds', '6', 30),
+                                        ('Place5', '', 5),
+                                        ('Place9', '', 5)}),
+    (risk12, {}, [], {('PassLine', '', 5), ('Field', '', 5)}),
+    (risk12, {}, [(1, 3)], {('PassLine', '', 5),
+                            ('Place6', '', 6),
+                            ('Place8', '', 6)}),
+    (risk12, {}, [(5, 6), (2, 3)], {('PassLine', '', 5),
+                                    ('Place6', '', 6),
+                                    ('Place8', '', 6)}),
+    (knockout, {}, [], {('PassLine', '', 5),
+                        ('DontPass', '', 5)}),
+    (knockout, {}, [(4, 2)], {('PassLine', '', 5),
+                              ('DontPass', '', 5),
+                              ('Odds', '6', 25)}),
+    (dicedoctor, {}, [], {('Field', '', 10)}),
+    (dicedoctor, {}, [(1, 1), (5, 6), (5, 5)], {('Field', '', 30)}),
+    (place68_dontcome2odds, {}, [], set()),
+    (place68_dontcome2odds, {}, [(4, 4)], {('Place6', '', 6),
+                                           ('Place8', '', 6),
+                                           ('DontCome', '', 5)}),
+    (place68_dontcome2odds, {}, [(4, 4), (2, 2)], {('Place6', '', 6),
+                                                   ('Place8', '', 6),
+                                                   ('DontCome', '4', 5),
+                                                   ('LayOdds', '4', 20)})
+])
+def test_strategies_compare_bets(strategy, strategy_info, rolls: list[tuple[int, int]],
+                                 correct_bets: {(str, str, float)}):
+    def strat(player, table, **strat_info):
+        if strat_info != {}:
+            return strategy(player, table, **strat_info)
+        return strategy(player, table, **strategy_info)
+
+    table = Table()
+    player = Player(100, bet_strategy=strat)
+    table.add_player(player)
+
+    table.fixed_run(rolls)
+    table.add_player_bets(verbose=False)
+
+    bets = table.players[0].bets_on_table
+
+    assert {(b.name, b.subname, b.bet_amount) for b in bets} == correct_bets
diff --git a/tests/test_table.py b/tests/test_table.py
new file mode 100644
index 00000000..b2454bb8
--- /dev/null
+++ b/tests/test_table.py
@@ -0,0 +1,71 @@
+import pytest
+
+from crapssim import Table, Player
+from crapssim.bet import Come, PassLine
+from crapssim.strategy import passline
+from crapssim.table import Point
+
+
+def test_ensure_one_player():
+    table = Table()
+    count_zero = len(table.players)
+    table.ensure_one_player()
+    count_one = len(table.players)
+    bankroll = table.players[0].bankroll
+    strategy = table.players[0].bet_strategy
+    assert (count_zero, count_one, bankroll, strategy) == (0, 1, 500, passline)
+
+
+def test_wrong_point_off():
+    table = Table()
+    table.point.status = 'Off'
+    player = Player(500, table=table)
+    player.bet(Come(100))
+    assert (len(player.bets_on_table), player.bankroll) == (0, 500)
+
+
+def test_wrong_point_on():
+    table = Table()
+    table.point.status = 'On'
+    table.point.number = 4
+    player = Player(500, table=table)
+    player.bet(PassLine(100))
+    assert (len(player.bets_on_table), player.bankroll) == (0, 500)
+
+
+@pytest.mark.parametrize(['status', 'number', 'comparison'], [
+    ('Off', None, 'Off'),
+    ('Off', None, 'off'),
+    ('On', 6, 6),
+    ('On', 6, '6'),
+    ('On', 8, 'on')
+])
+def test_point_equality(status, number, comparison):
+    point = Point()
+    point.status = status
+    point.number = number
+    assert point == comparison
+
+
+@pytest.mark.parametrize(['number', 'comparison'], [
+    (8, 6),
+    (8, '6')
+])
+def test_point_greater_than(number, comparison):
+    point = Point()
+    point.status = 'On'
+    point.number = number
+    assert point > comparison
+
+
+@pytest.mark.parametrize(['number', 'comparison'], [
+    (4, 6),
+    (4, '10')
+])
+def test_point_less_than(number, comparison):
+    point = Point()
+    point.status = 'On'
+    point.number = number
+    assert point < comparison
+
+