Skip to content

Commit 335351d

Browse files
committed
Adds README and pydocs.
1 parent f6bac21 commit 335351d

21 files changed

+309
-13
lines changed

README.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
Introduction
2+
============
3+
4+
Dots is an implementation of the classic game where you draw a bunch of dots on
5+
a piece of paper in a grid pattern and then players take turns connecting the
6+
dots. When a square, or cell, is made the player "captures" the cell. The player
7+
with the greatest number of captured cells at the end of the game wins.
8+
9+
Dots has always been a favorite game of mine and when I decided I wanted to
10+
learn a little bit about game programming, I figured Dots would be a good place
11+
to start. Dots is simple enough that I figured I wouldn't get bogged down in the
12+
details of the game during implementation, but complex enough that making the
13+
game would give me a nice little taste of game programming. Since Python is my
14+
go-to language I figured Pygame would be a good place to start.
15+
16+
Requirements
17+
============
18+
There are just two requirements for running Dots:
19+
20+
1. Python 3.7.7 or greater (https://www.python.org/downloads/)
21+
2. Pygame 2.0.0 or greater (https://www.pygame.org/)
22+
23+
During development I use Python virtual environments to isolate the Python
24+
version and packages I need. Feel free to use whatever you are comfortable with.
25+
26+
Running the Game
27+
================
28+
Dots is executed from the command line. Below is the usage documentation when
29+
starting with the -h or --help option.
30+
31+
```
32+
$ python dots.py -h
33+
usage: dots.py [-h] [-r ROWS] [-c COLUMNS] [-w CELL_WIDTH] [-t CELL_HEIGHT] [-p [PLAYER ...]]
34+
35+
A pygame implementation of the classic game of trying to capture as many cells as you can by connecting the dots.
36+
37+
optional arguments:
38+
-h, --help show this help message and exit
39+
-r ROWS number of rows of cells (default: 4)
40+
-c COLUMNS number of columns of cells (default: 4)
41+
-w CELL_WIDTH width of cells (default: 100 pixels)
42+
-t CELL_HEIGHT height of cells (default: 100 pixels)
43+
-p [PLAYER ...] names of the two to four players (default: Alice Bob)
44+
```
45+
46+
As you can see, there are default values for all options so simply executing
47+
dots.py will run the game for you.
48+
49+
Playing the Game
50+
================
51+
When you first start the game, the game board will look something similar to
52+
this:
53+
54+
![Dots 1](docs/dots1.png)
55+
56+
The left pointing arrow indicates whose turn it is to connect two dots. To
57+
connect two dots, simply move your mouse over an edge between two dots. As you
58+
hover over an edge, the edge will be outlined to highlight the edge as shown
59+
below:
60+
61+
![Dots 2](docs/dots2.png)
62+
63+
Click on the edge to join the two dots. Once you click on the edge it will be
64+
permanently colored in indicating the dots are now joined. Clicking on an edge
65+
that already connects two dots has no effect. After connecting two dots, it
66+
becomes the next player's turn. You will see the left pointing arrow now points
67+
at the next player.
68+
69+
![Dots 3](docs/dots3.png)
70+
71+
Players continue alternating connecting dots until a cell is captured by
72+
connecting all of the dots that make up the cell. If connecting two dots
73+
captures a cell, the cell's background color changes to the color of the player
74+
that captured the cell. The player's score also updates when a cell is captured.
75+
Additionally, the player that captured the cell gets to connect another two
76+
dots. As long as the player continues capturing cells, it remains their turn.
77+
78+
![Dots 4](docs/dots4.png)
79+
80+
Continue with game play until all of the cells have been captured and a winner
81+
is determined. At the end of the game, you will have the option to start a new
82+
game. Click the "Play Again?" button to start a new game.
83+
84+
![Dots 5](docs/dots5.png)
85+
86+
I hope you enjoy playing the game!

banner.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""A banner graphic for the game of Dots."""
2+
13
# Copyright 2021 Curt Bathras
24
#
35
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -25,6 +27,9 @@
2527

2628

2729
class Banner(pg.Rect):
30+
"""Banner is used to create the graphic at the end of the game to display
31+
the winner.
32+
"""
2833
def __init__(self, rect: pg.Rect, bg_color: pg.Color):
2934
super().__init__(rect)
3035
self.bg_color = bg_color
@@ -34,6 +39,7 @@ def __init__(self, rect: pg.Rect, bg_color: pg.Color):
3439
self.screen.get_height() // 2)
3540

3641
def draw(self, winner: list[Player]) -> None:
42+
"""Draw the banner to the screen."""
3743
# Create the message text
3844
msg = f'{winner[0].name} Wins!' if len(winner) == 1 else "It's a TIE!"
3945
text = FONT_20.render(msg, True, BLACK)
@@ -50,6 +56,7 @@ def draw(self, winner: list[Player]) -> None:
5056
pg.display.update(self)
5157

5258
def clear(self) -> None:
59+
"""Clear the banner from the screen."""
5360
pg.draw.rect(self.screen, BACKGROUND_COLOR, self)
5461

5562
def __str__(self) -> str:

board.py

+52-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""The main board for game play in the game of Dots."""
2+
13
# Copyright 2021 Curt Bathras
24
#
35
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -29,6 +31,37 @@
2931

3032

3133
class Board:
34+
"""A Board is the collection of all of the entities that make up the game.
35+
Namely, dots, edges, and cells. Entities are grouped into what I call
36+
superrows and supercolumns. A single element, or supercell, of a superrow
37+
and supercolumn is the collection of one dot, two edges and one cell. The
38+
number of superrows is one greater than the number of cells in the board's
39+
height and the number of supercolumns is one greater than the number of
40+
cells in the board's width. Superrows and supercolumns allow for a very fast
41+
lookup of which two edges could possibly contain a point in space. Once the
42+
supercell is known, it is quick to check if either edge contains the point.
43+
44+
The diagram below illustrates the concept of a supercell. D = a dot, E = an
45+
edge, C = a cell.
46+
47+
DDD | EEEEEEEEEEEEEEE
48+
DDD | EEEEEEEEEEEEEEE
49+
---------------------
50+
EEE | CCCCCCCCCCCCCCC
51+
EEE | CCCCCCCCCCCCCCC
52+
EEE | CCCCCCCCCCCCCCC
53+
EEE | CCCCCCCCCCCCCCC
54+
EEE | CCCCCCCCCCCCCCC
55+
EEE | CCCCCCCCCCCCCCC
56+
57+
This supercell consists of one dot, two edges (one vertical and one
58+
horizontal) and one cell. For the last supercolumn, the supercell only
59+
contains the dot and vertical edge. For the last superrow, the supercell
60+
only contains the dot and horizontal edge. The upper left corner of a
61+
supercell and its height and width are well-defined. So the lookup of which
62+
supercell contains a point is very fast. From there, it is at most two
63+
comparisons to see if either edge contains the point.
64+
"""
3265
def __init__(self, x_shift: int=0, y_shift: int=0):
3366
super().__init__()
3467
self._cfg: Config = Config()
@@ -37,7 +70,7 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
3770
self._screen: pg.Surface = pg.display.get_surface()
3871
self._highlighted_edge = None
3972

40-
# Create the dots
73+
# Create the dots (a 2D list of Dot objects)
4174
self._dots: list[list[Dot]] = []
4275
for r in range(0, self._cfg.CELL_ROWS + 1):
4376
row = []
@@ -56,7 +89,7 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
5689
dot.draw()
5790
self._dots.append(row)
5891

59-
# Create the cells
92+
# Create the cells (a 2D list of Cell objects)
6093
self._cells: list[list[Cell]] = []
6194
for r in range(0, self._cfg.CELL_ROWS):
6295
row = []
@@ -75,7 +108,7 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
75108
cell.draw()
76109
self._cells.append(row)
77110

78-
# Create the edges
111+
# Create the edges (a 2D list of Edge objects)
79112
# Superrows and supercolumns are rows and columns of a collection
80113
# of one dot, two edges, and one cell. This allows for easy grouping
81114
# and lookup of edges based on mouse location.
@@ -98,7 +131,11 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
98131
)
99132
h_edge.draw()
100133

101-
# Establish cell to edge relationships
134+
# Establish cell to edge relationships - for checking whether a
135+
# cell is captured by a user, we need to know which cells the
136+
# edge touches. An edge always touches one or two cells. Edges
137+
# that touch one cell are the edges that make up the exterior
138+
# of the board.
102139
# top superrow
103140
if h_edge and r == 0:
104141
h_edge.cell2 = self._cells[r][c]
@@ -130,7 +167,10 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
130167
)
131168
v_edge.draw()
132169

133-
# Establish cell to edge relationships
170+
# Establish cell to edge relationships - for checking whether a
171+
# cell is captured by a user, we need to know which edges the
172+
# cell touches. A cell always touches four edges: top, bottom,
173+
# left and right.
134174
# left supercolumn
135175
if v_edge and c == 0:
136176
v_edge.cell2 = self._cells[r][c]
@@ -170,6 +210,7 @@ def __init__(self, x_shift: int=0, y_shift: int=0):
170210
self.draw()
171211

172212
def get_edge(self, pos: tuple) -> Edge:
213+
"""Retrieve the edge containing pos."""
173214
# To look up the edge to see if it contains the x,y you can quickly
174215
# retrieve the tuple of edges that possibly contains x,y by:
175216
# row = y // (dd + ch)
@@ -178,12 +219,14 @@ def get_edge(self, pos: tuple) -> Edge:
178219
# x,y
179220
try:
180221
x, y = pos
222+
# Determine the superrow and supercolumn containing the point
181223
row = (y - self._y_shift - self._cfg.GUTTER_WIDTH) // \
182224
(self._cfg.DOT_DIA + self._cfg.CELL_HEIGHT)
183225
col = (x - self._x_shift - self._cfg.GUTTER_WIDTH) // \
184226
(self._cfg.DOT_DIA + self._cfg.CELL_WIDTH)
185227
edges = self._edges[row][col]
186228

229+
# Check to see if either edge contains the point
187230
for edge in edges:
188231
if edge.collidepoint(pos):
189232
return edge
@@ -192,6 +235,7 @@ def get_edge(self, pos: tuple) -> Edge:
192235
return None
193236

194237
def highlight_edge(self, edge: Edge) -> None:
238+
"""Highlight the specified edge."""
195239
if not edge.captured and self._highlighted_edge != edge:
196240
if self._highlighted_edge:
197241
pg.draw.rect(self._screen,
@@ -203,19 +247,22 @@ def highlight_edge(self, edge: Edge) -> None:
203247
self._highlighted_edge = edge
204248

205249
def unhighlight_edge(self) -> None:
250+
"""Clear the highlighted edge so it is no longer highlighted."""
206251
if self._highlighted_edge:
207252
pg.draw.rect(self._screen, EDGE_COLOR_DEFAULT,
208253
self._highlighted_edge)
209254
pg.display.update(self._highlighted_edge)
210255
self._highlighted_edge = None
211256

212257
def capture_edge(self, edge: Edge) -> None:
258+
"""Capture the specified edge."""
213259
self._highlighted_edge = None
214260
edge.captured = True
215261
pg.draw.rect(self._screen, EDGE_COLOR_CAPTURED, edge)
216262
pg.display.update(edge)
217263

218264
def draw(self) -> None:
265+
"""Draw the entire board."""
219266
pg.display.flip()
220267

221268
def __str__(self) -> str:

button.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""A general button implementation."""
2+
13
# Copyright 2021 Curt Bathras
24
#
35
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -28,6 +30,7 @@
2830

2931

3032
class Button:
33+
"""A button that a user can click that performs a function."""
3134
def __init__(self, pos: tuple, size: tuple, callback: callable=None,
3235
font: pg.font.Font=FONT_16, text: str='', visible: bool=True,
3336
text_color: pg.Color=WHITE, radius: int=3):
@@ -48,13 +51,16 @@ def __init__(self, pos: tuple, size: tuple, callback: callable=None,
4851

4952
@property
5053
def visible(self) -> bool:
54+
"""Get the button visibility."""
5155
return self._visible
5256

5357
@visible.setter
5458
def visible(self, val: bool) -> None:
59+
"""Set the button visibility."""
5560
self._visible = val
5661

5762
def draw(self) -> None:
63+
"""Draw the button on the screen."""
5864
if self._visible:
5965
# Get the button's surface and draw a rectangle on it
6066
self._rect = self._surf.get_rect()
@@ -88,6 +94,7 @@ def draw(self) -> None:
8894
pg.display.update(self._rect)
8995

9096
def handle_event(self, event: pg.event) -> None:
97+
"""Handle the mouse events."""
9198
if self._visible:
9299
if event.type == pg.MOUSEBUTTONDOWN:
93100
if self._rect.collidepoint(event.pos):

cell.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""A cell is bounded by four dots and four edges in the game of Dots."""
2+
13
# Copyright 2021 Curt Bathras
24
#
35
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -26,6 +28,11 @@
2628

2729

2830
class Cell(Entity):
31+
"""A Cell represents the thing a player is trying to capture. It is bounded
32+
by four dots and four edges. The Cell class derives from the Entity abstract
33+
base class which in turns derives from the Pygame Rect class so every Cell
34+
is also a Rect.
35+
"""
2936
def __init__(self, pos: tuple, size: tuple, bg_color: pg.Color):
3037

3138
super().__init__(pos, size, bg_color)
@@ -37,47 +44,60 @@ def __init__(self, pos: tuple, size: tuple, bg_color: pg.Color):
3744

3845
@property
3946
def captured(self) -> bool:
47+
"""Get the captured status of the cell."""
4048
return self._captured
4149

4250
@property
4351
def edge_top(self) -> Edge:
52+
"""Get the top edge associated with the cell."""
4453
return self._edge_top
4554

4655
@edge_top.setter
4756
def edge_top(self, val) -> None:
57+
"""Set the top edge associated with the cell."""
4858
self._edge_top = val
4959

5060
@property
5161
def edge_bottom(self) -> Edge:
62+
"""Get the bottom edge associated with the cell."""
5263
return self._edge_bottom
5364

5465
@edge_bottom.setter
5566
def edge_bottom(self, val) -> None:
67+
"""Set the bottom edge associated with the cell."""
5668
self._edge_bottom = val
5769

5870
@property
5971
def edge_right(self) -> Edge:
72+
"""Get the right edge associated with the cell."""
6073
return self._edge_right
6174

6275
@edge_right.setter
6376
def edge_right(self, val) -> None:
77+
"""Set the right edge associated with the cell."""
6478
self._edge_right = val
6579

6680
@property
6781
def edge_left(self) -> Edge:
82+
"""Get the left edge associated with the cell."""
6883
return self._edge_left
6984

7085
@edge_left.setter
7186
def edge_left(self, val) -> None:
87+
"""Set the left edge associated with the cell."""
7288
self._edge_left = val
7389

7490
def handle_event(self, event: pg.event) -> None:
91+
"""No-op implemenation because a cell handles no events."""
7592
return
7693

7794
def draw(self) -> None:
95+
"""Draw the cell to the screen."""
7896
pg.draw.rect(self._screen, self._bg_color, self)
7997

8098
def check_for_capture(self, player: Player) -> bool:
99+
"""Check to see if all edges associated with this cell have been
100+
captured which means this cell has been captured."""
81101
if self._edge_top.captured and self._edge_bottom.captured \
82102
and self._edge_left.captured and self._edge_right.captured:
83103

0 commit comments

Comments
 (0)