Compare commits

...

38 Commits

Author SHA1 Message Date
7d548c390a Merge branch 'daniel-server'
# Conflicts:
#	public/scss/main.scss
2018-06-12 22:51:31 -07:00
clairecahill
ba4f60c0a8 Add link to the about page 2018-06-12 21:21:51 -07:00
clairecahill
2ed23ee32b Merge branch 'daniel-server' of https://github.com/OSU-CS290-Sp18/final-project-group-4 into daniel-server
Add a link to the about page
2018-06-12 21:18:51 -07:00
55a39fe1c7 Make top-bar take up full width. 2018-06-12 21:11:55 -07:00
clairecahill
23b8471301 add link to the about page 2018-06-12 21:11:10 -07:00
d8ae468f39 Add rules.ecr and change background image. 2018-06-12 21:01:28 -07:00
b7bdbbdc12 Fix small mistakes, properly center GO. 2018-06-12 20:50:07 -07:00
clairecahill
1af80d185c Merge branch 'daniel-server' of https://github.com/OSU-CS290-Sp18/final-project-group-4 into daniel-server 2018-06-12 19:56:59 -07:00
733c32748e Format code. 2018-06-12 16:32:58 -07:00
bc395e5e7e Add alphabetic ordering. 2018-06-12 16:32:14 -07:00
b844a83db1 Clean up about page. 2018-06-12 16:28:29 -07:00
clairecahill
7eab8eb71e Claire's changes, add styling, backgrounds, and rules page 2018-06-11 19:56:25 -07:00
fireball1983
7e17bba627
Merge pull request #2 from OSU-CS290-Sp18/aboutpage
Added reference
2018-06-11 01:07:28 -07:00
Michael Huang
913dea2fea last bit 2018-06-11 01:06:02 -07:00
fireball1983
59bc24553d
Merge pull request #1 from OSU-CS290-Sp18/aboutpage
Michael's about page
2018-06-11 01:01:59 -07:00
Michael Huang
3af22f9464 abuotpage 2018-06-11 01:00:50 -07:00
dogcatfee
fc42ac7651 Move table creation out of save_game 2018-06-08 14:20:50 -07:00
dogcatfee
b41035fac7 Open database once update 2018-06-08 14:08:47 -07:00
dogcatfee
2c8bc7aff9 ignore javascript and css 2018-06-06 23:29:32 -07:00
dogcatfee
f04f44e593 ignore javascript and css 2018-06-06 23:28:20 -07:00
dogcatfee
fe61d3dc73 ignore javascript and css 2018-06-06 23:19:48 -07:00
dogcatfee
dac5a64a45 Game cleaner implemented 2018-06-06 23:02:25 -07:00
dogcatfee
c0a1548463 Save on move update 2018-06-06 21:41:07 -07:00
dogcatfee
b4a31e709c Merge branch 'matt-server' of https://github.com/OSU-CS290-Sp18/final-project-group-4 into matt-server 2018-06-06 19:26:44 -07:00
dogcatfee
48d5309f63 update gitignore again 2018-06-06 19:26:23 -07:00
dogcatfee
24b262901f update gitignore again 2018-06-06 19:23:05 -07:00
dogcatfee
4b226210e8 remove cache 2018-06-06 19:22:04 -07:00
dogcatfee
6bae399d59 update gitignore 2018-06-06 19:19:51 -07:00
dogcatfee
0a103b7b73 Run save every 10 minutes 2018-06-06 19:18:15 -07:00
dogcatfee
164cf6a73a Merge branch 'matt-server' 2018-06-06 18:57:41 -07:00
dogcatfee
12c137ef4f Game loading completed 2018-06-06 18:33:01 -07:00
sessionm21
9c3665ea5c Add game creation on DB query 2018-06-05 10:27:07 -07:00
sessionm21
2ec51a45ea sqlite additions 2018-06-04 11:28:39 -07:00
sessionm21
65be4f3c4c Revert "Revert "Create README.md""
This reverts commit 05974b845c.
2018-05-26 11:10:14 -07:00
sessionm21
5ee0839451 Revert "reinit"
This reverts commit 0939a6590e.
2018-05-26 11:09:42 -07:00
sessionm21
05974b845c Revert "Create README.md"
This reverts commit de0fbfed94.
2018-05-26 11:07:40 -07:00
sessionm21
0939a6590e reinit 2018-05-26 11:06:17 -07:00
sessionm21
de0fbfed94
Create README.md 2018-05-04 23:45:42 -07:00
19 changed files with 527 additions and 43 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.js linguist-vendored
*.css linguist-vendored
*.scss linguist-vendored=false

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/lib/ /lib/
/bin/ /bin/
/.shards/ /.shards/
game_saves.db
.sass-cache*

BIN
Go Normal file

Binary file not shown.

65
public/css/main.css vendored
View File

@ -1,20 +1,71 @@
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: "Indie Flower", serif; font-family: "Comfortaa", serif;
text-align: center; } text-align: center; }
h1 { h1 {
font-size: 5em; font-family: "Bangers", serif;
margin: 0px; } font-size: 7em;
color: darkred;
display: inline;
margin: 0px;
margin-top: 10px;
margin-bottom: 10px;
background: white;
padding: 2px 17px 2px 8px;
border-radius: 15px; }
body { body {
font-family: "Raleway", sans-serif; font-family: "Comfortaa", sans-serif;
margin: 0px; margin: 0px;
background-color: #f4f4f4; } background-color: #f4f4f4; }
.rules-body {
background-image: url(/images/grid.jpg); }
.rules-title {
font-family: "Bangers", serif;
font-size: 66px;
color: darkred;
display: inline;
border-radius: 20px;
line-height: 115px;
padding: 10px;
background-color: white;
margin-left: 35%; }
.content-wrapper { .content-wrapper {
max-width: 750px; max-width: 750px;
margin: auto; } margin: auto; }
ul.rules-list {
background-color: #f4f4f4;
padding: 26px;
border-radius: 15px;
margin: 0px 25px; }
li.rule {
padding: 3px;
font-size: 18px; }
ul.game-instructions {
display: flex;
padding: 0px; }
li#welcome {
padding: 0px 10px 0px 5px;
border-left: solid tomato;
text-align: center;
list-style: none; }
li#welcome:last-child {
border-right: solid tomato; }
div.top-bar {
background-image: url(/images/grid.jpg);
line-height: 135px;
display: flex;
justify-content: center;
margin-bottom: 40px; }
.board { .board {
background-color: tomato; background-color: tomato;
padding: 20px; padding: 20px;
@ -63,7 +114,11 @@ body {
.split-item { .split-item {
flex-grow: 1; flex-grow: 1;
box-sizing: border-box; } box-sizing: border-box;
border: solid tomato 1px;
border-radius: 22px;
background-color: white;
margin: 0px 10px; }
.split-wrapper form { .split-wrapper form {
display: flex; display: flex;

95
public/css/style.css vendored Normal file
View File

@ -0,0 +1,95 @@
body {
font-family: Roboto;
background-image: url(/images/clouds.jpg);
background-color: #F5F7FA;
background-size: cover;
color: #2d2d2d;
}
a {
text-decoration:none;
}
.card {
background-color:white;
box-shadow: 2px 2px 3px rgba(0,0,0,.13) ,1px 2px 2px rgba(0,0,0,.1) , -1px -2px 2px rgba(0,0,0,.05);
margin: auto;
margin-top: 20px;
max-width: 750px;
padding: 20px;
}
.member-container{
display:flex;
flex-direction: column;
}
.person-photo img{
padding-top:10px;
height:30%;
width:30%;
display: block;
margin-left: auto;
margin-right: auto;
border-radius:50%;
}
.member {
padding: 20px;
box-sizing: border-box;
flex-grow:1;
flex-basis:50%;
background-color:white;
}
.toplevel{
position:relative;
display: flex;
flex-wrap: wrap;
}
h2 {
font-size:30px;
color:#546076;
font-weight:100;
text-align:center;
width: 100%;
}
.Mission{
font-size:40px;
font-weight:100;
padding-top:16px;
margin-left: auto;
margin-right: auto;
text-align:center;
}
.Person-Name{
font-size:20px;
font-weight:600;
padding-top:16px;
margin-left: auto;
margin-right: auto;
text-align:center;
}
.member-text {
position:center;
display: block;
margin: auto;
font:Roboto;
font-weight:400;
}
.mission-text {
position:center;
display: block;
margin: auto;
width:50%;
width:800px;
font:Roboto;
font-weight:400;
text-align:center;
padding-top:1%;
}

BIN
public/images/claire.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
public/images/clouds.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
public/images/danila.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/images/grid.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/images/matthew.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/images/michael.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -5,26 +5,87 @@ $background-grey: #f4f4f4;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: "Indie Flower", serif; font-family: "Comfortaa", serif;
text-align: center; text-align: center;
} }
h1 { h1 {
font-size: 5em; font-family: "Bangers", serif;
font-size: 7em;
color:darkred;
display:inline;
margin: 0px; margin: 0px;
margin-top: 10px;
margin-bottom: 10px;
background: white;
padding: 2px 17px 2px 8px;
border-radius: 15px;
} }
body { body {
font-family: "Raleway", sans-serif; font-family: "Comfortaa", sans-serif;
margin: 0px; margin: 0px;
background-color: $background-grey; background-color: $background-grey;
} }
.rules-body {
background-image: url(/images/grid.jpg);
}
.rules-title {
font-family: "Bangers", serif;
font-size: 66px;
color: darkred;
display: inline;
border-radius: 20px;
line-height: 115px;
padding: 10px;
background-color: white;
margin-left: 35%;
}
.content-wrapper { .content-wrapper {
max-width: 750px; max-width: 750px;
margin: auto; margin: auto;
} }
ul.rules-list {
background-color: $background-grey;
padding: 26px;
border-radius: 15px;
margin: 0px 25px;
}
li.rule {
padding: 3px;
font-size: 18px;
}
ul.game-instructions {
display: flex;
padding: 0px;
}
li#welcome {
padding: 0px 10px 0px 5px;
border-left: solid tomato;
text-align: center;
list-style: none;
&:last-child {
border-right: solid tomato;
}
}
div.top-bar {
background-image: url(/images/grid.jpg);
line-height: 135px;
display: flex;
justify-content: center;
margin-bottom: 40px;
}
.board { .board {
background-color: tomato; background-color: tomato;
padding: 20px; padding: 20px;
@ -97,6 +158,10 @@ body {
.split-item { .split-item {
flex-grow: 1; flex-grow: 1;
box-sizing: border-box; box-sizing: border-box;
border: solid tomato 1px;
border-radius: 22px;
background-color: white;
margin: 0px 10px;
} }
.split-wrapper form { .split-wrapper form {

161
src/Go.cr
View File

@ -2,29 +2,152 @@ require "./Go/*"
require "kemal" require "kemal"
require "json" require "json"
require "db"
require "sqlite3"
URL = "localhost" URL = "localhost"
PORT = "3000" PORT = "3000"
GAME_CACHE = {} of String => Go::Game GAME_CACHE = {} of String => Go::Game
GAME_SAVE = "./game_saves.db"
GAME_DB = DB.open "sqlite3:./#{GAME_SAVE}"
def query_game(db, id) : Go::Game? def make_table(db)
# Function: make_table
# Parameters: db(Sqlite)
# Returns: None
db.exec "create table if not exists game_saves (
gameid string, turn integer, size integer, white_pass string,
black_pass string, time string, board string, UNIQUE(gameid) )"
end
def save_game(db, gameid, game)
# Function: save_game
# Parameters: db(Sqlite) gameid(String) game(Go::Game)
# Returns: None
turn, size, white_pass, black_pass, board = game.encode
# If duplicate => replace values, else => make new row for gameid
db.exec "insert or replace into game_saves values (?, ?, ?, ?, ?, ?, ?)",
gameid, turn.value, size, white_pass, black_pass, Time.now.to_json, board
end
def save_all(cache)
# Function: save_all
# Parameters: cache({(String),(Go::Game)})
# Returns: None
cache.each do |game_hash|
gameid, game = game_hash
save_game(GAME_DB, gameid, game)
end
end
def query_game(db, gameid) : Go::Game?
# Function: query_game
# Parameters: db(Sqlite) gameid(String)
# Returns: (Go::Game) for a given gameid
turn = 0
size = 9
white_pass = ""
black_pass = ""
board = ""
begin
# Query whole row where the gameid is found
db.query "SELECT turn,size,white_pass,black_pass,board FROM game_saves WHERE gameid = ?", gameid do |rs|
rs.each do
turn = rs.read(Int32)
size = rs.read(Int32)
white_pass = rs.read(String)
black_pass = rs.read(String)
board = rs.read(String)
end
end
if ( board == "" )
return nil return nil
end
# New Go::Game object
game = Go::Game.new()
game.size = Go::Size.from_value(size)
game.white_pass = white_pass
game.black_pass = black_pass
game.turn = Go::Color.from_value(turn)
# Parses game board string
counter = 0
# For each character in the board String
board.each_char do |char|
x = counter / 9
y = counter % 9
coord = {x.to_i8, y.to_i8}
if(char == 'B')
game.board[coord] = Go::Color::Black
elsif(char == 'W')
game.board[coord] = Go::Color::White
end
counter += 1
end
rescue
# Catch bad query
return nil
end
# Finished Go::Game object to return
return game
end
def game_cleaner(db, cache)
# Function: game_cleaner
# Parameters: db(Sqlite) cache({(String),(Go::Game)})
# Returns: None
# Description: Cleans the database and memory of games older than 24 hours, every 2 hours
spawn do
loop do
gameid = ""
ntime = Time.now()
# Time span, for the subtraction of two time objects
tspan = Time::Span.new(0,0,0)
db.query "SELECT time, gameid FROM game_saves" do |rs|
rs.each do
stime = Time.from_json(rs.read(String))
gameid = rs.read(String)
tspan = ntime - stime
end
end
if( tspan.hours > 24 || tspan.days > 0 )
# Delete game from database
db.exec("DELETE FROM game_saves WHERE gameid = ?", gameid)
# Delete game from memory
cache.delete(gameid)
puts "Game: #{gameid} deleted due to inactivity"
end
sleep 2.hour
end
end
end end
def lookup_game(db, cache, id) : Go::Game? def lookup_game(db, cache, id) : Go::Game?
# Function: lookup_game
# Parameters: db(Sqlite) cache({(String), (Go::Game)}) id(String)
# Returns: None
# Description: Loads game data from memory, then attempts load from database
if game = cache[id]? if game = cache[id]?
return game return game
else else
loaded_game = query_game(db, id) loaded_game = query_game(db, id)
cache[id] = loaded_game if loaded_game # Need to convert id to string for some reason
cache[id.to_s] = loaded_game if loaded_game
return loaded_game return loaded_game
end end
end end
def create_game(db, cache, game, id) def create_game(db, cache, game, id)
# Function: create_game
# Parameters: db(Sqlite) cache({(String), (Go::Game)}) game(Go::Game) id(String)
# Returns: None
cache[id] = game cache[id] = game
end end
def handle_message(id, game, socket, message) def handle_message(id, game, socket, message)
# Function: handle_message
# Parameters: id(String) game(Go::Game) socket(WebSocket) message(String)
# Returns: None
# Description: Handle placement messages from the WebSocket
split_command = message.split(" ") split_command = message.split(" ")
command = split_command[0] command = split_command[0]
if command == "place" if command == "place"
@ -34,9 +157,19 @@ def handle_message(id, game, socket, message)
game.update(x, y, color) game.update(x, y, color)
game.sockets.each { |socket| socket.send game.to_json } game.sockets.each { |socket| socket.send game.to_json }
# If saving game on move
save_game(GAME_DB, id, game)
end end
end end
get "/about" do |env|
render "src/Go/views/about.ecr"
end
get "/rules" do |env|
render "src/Go/views/rules.ecr"
end
get "/" do |env| get "/" do |env|
render "src/Go/views/index.ecr", "src/Go/views/base.ecr" render "src/Go/views/index.ecr", "src/Go/views/base.ecr"
end end
@ -46,7 +179,7 @@ post "/game" do |env|
game_password = env.params.body["password"]? game_password = env.params.body["password"]?
if game_id == nil || game_password == nil if game_id == nil || game_password == nil
render_404 render_404
elsif game = lookup_game(nil, GAME_CACHE, game_id) elsif game = lookup_game(GAME_DB, GAME_CACHE, game_id)
id = game_id id = game_id
size = game.size.value size = game.size.value
black = nil black = nil
@ -78,7 +211,7 @@ post "/create" do |env|
if game_id == nil || user_password == nil || other_password == nil || color == nil || color_e == nil if game_id == nil || user_password == nil || other_password == nil || color == nil || color_e == nil
render_404 render_404
elsif game = lookup_game(nil, GAME_CACHE, game_id) elsif game = lookup_game(GAME_DB, GAME_CACHE, game_id)
render_404 render_404
else else
color_e = color_e.as(Go::Color) color_e = color_e.as(Go::Color)
@ -101,7 +234,7 @@ end
ws "/game/:id" do |socket, env| ws "/game/:id" do |socket, env|
game_id = env.params.url["id"] game_id = env.params.url["id"]
if game = lookup_game(nil, GAME_CACHE, game_id) if game = lookup_game(GAME_DB, GAME_CACHE, game_id)
socket.send game.to_json socket.send game.to_json
game.sockets << socket game.sockets << socket
@ -117,4 +250,22 @@ ws "/game/:id" do |socket, env|
end end
end end
# For timed-autosave
# spawn do
# loop do
# sleep 10.minute
# save_all(GAME_CACHE)
# end
# end
# If table does not exist, build it
make_table(GAME_DB)
# Remove games that are older than 24hrs
game_cleaner(GAME_DB, GAME_CACHE)
Kemal.run Kemal.run
# If exit is disabled in kemal.stop
# For save on close
# at_exit do
# save_all(GAME_CACHE)
# end

View File

@ -20,6 +20,14 @@ module Go
property turn : Color property turn : Color
property sockets : Array(HTTP::WebSocket) property sockets : Array(HTTP::WebSocket)
def initialize()
@size = Size::Small
@white_pass = ""
@black_pass = ""
@board = Board.new
@turn = Color::Black
@sockets = [] of HTTP::WebSocket
end
def initialize(size : Size, @black_pass, @white_pass) def initialize(size : Size, @black_pass, @white_pass)
@size = size @size = size
@board = Board.new @board = Board.new
@ -133,7 +141,7 @@ module Go
end end
def encode def encode
{ @turn.to_s, @size.value, board_string(@board) } { @turn, @size.value, @white_pass.to_s, @black_pass.to_s, board_string(@board) }
end end
end end
end end

72
src/Go/views/about.ecr Normal file
View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>About Us</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,400,700" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css" media="screen">
<script src="https://use.fontawesome.com/releases/v5.0.9/js/all.js" defer></script>
</head>
<body>
<div class="card">
<h2>Hello!</h2>
<p>
Welcome to our Go website. We are Group 4 from CS 290. Our mission is very ambitious - to get an A in CS 290, Web Development. Despite the immense difficulty of this task, we did our best to bring you this website, so that you may enjoy playing the game of Go with your friends. After Google's AlphaGo's victory against the best human Go player, there's not really a point to playing against a computer - you will lose.
</p>
</div>
<div class="member-container card">
<h2>Group Members</h2>
<div class="toplevel">
<article class="member">
<div class="member-content">
<div class="person-photo" alt"">
<img src="/images/claire.jpg" alt="daa">
</div>
<h2 class="Person-Name" >Claire Cahill</h2>
<p class="member-text">
Claire created a page explaining the rules of Go, so that new players can learn how to play the game instead of clicking around randomly.
</p>
</div>
</article>
<article class="member">
<div class="member-content">
<div class="person-photo" alt"">
<img src="/images/danila.jpg" alt="daa">
</div>
<h2 class="Person-Name" >Danila Fedorin</h2>
<p class="member-text">
Danila created the front-end and a basic back-end for the Go game. He used unconventional languages, namely Elm, a functional programming language designed for web applications, and Crystal, a compiled sibiling language of Ruby.
</p>
</div>
</article>
<article class="member">
<div class="member-content">
<div class="person-photo" alt"">
<img src="/images/michael.jpg" alt="daa">
</div>
<h2 class="Person-Name" >Michael Huang</h2>
<p class="member-text">
Michael created this about page!
</p>
</div>
</article>
<article class="member">
<div class="member-content">
<div class="person-photo" alt"">
<img src="/images/matthew.jpg" alt="daa">
</div>
<h2 class="Person-Name" >Matthew Sessions</h2>
<p class="member-text">
Matt adapted the Crystal back-end to work with SQLite. This meant loading and saving Go games into the database instead of an in-memory cache, thereby allowing games to persist after server restarts.
</p>
</div>
</article>
</div>
</div>
</body>
</html>

View File

@ -3,10 +3,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link href="https://fonts.googleapis.com/css?family=Indie+Flower|Raleway" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Indie+Flower|Raleway" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Comfortaa|Bangers" rel="stylesheet">
</head> </head>
<body> <body>
<div class="top-bar"><h1 class="title">GO</h1></div>
<div class="content-wrapper"> <div class="content-wrapper">
<h1>Go</h1>
<%= content %> <%= content %>
</div> </div>
</body> </body>

View File

@ -1,3 +1,11 @@
<ul class="game-instructions">
<li id="welcome">Welcome to our simulation of Go! <a href="/about">Meet the creators.</a></li>
<li id="welcome">Defeat your opponent by earning more territory.</li>
<li id="welcome">"Capture" dots of the opposite color by surrounding them.</li>
<li id="welcome">Create a game by filling out the fields below, giving the next player a password in "Their password."</li>
<li id="welcome">Join a game by entering the corresponding session name and password.</li>
<li id="welcome">Additional rules are found <a href="/rules">here.</a></li>
</ul>
<div class="split-wrapper"> <div class="split-wrapper">
<div class="split-item"> <div class="split-item">
<h2>Create Game</h2> <h2>Create Game</h2>

24
src/Go/views/rules.ecr Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/main.css"> </link>
</head>
<body class="rules-body">
<h2 class="rules-title">Rules of Go</h2>
<ul class="rules-list">
<li class="rule">The main object of the game is to cover more surface area of the board with your color (either black or white). Dots (sometimes referred to as “stones”) are placed at the intersections of lines.</li>
<li class="rule">Once a dot is placed, it cannot be removed. However, it can be “captured” by the other player if he/she completely surrounds a group of dots.</li>
<li class="rule">Every dot captured by a player earns him/her a point.</li>
<li class="rule">Points are also earned for the dots of that players color remaining on the board at the end of the game.</li>
<li class="rule">The player with the most points at the end of the game is the winner.</li>
<li class="rule">Dots are placed one at a time. Black plays first.</li>
<img src="/images/go-example.jpg">
<li class="rule">This diagram, (taken from <a href=https://www.britgo.org/intro/intro2.html>britgo</a>) shows the black player capturing a white stone at a (location a shows an example of a black “eye”). The black player has 15 points of territory - 10 in the bottom right corner and 5 at the top of the board. They also earn a point for capturing a white dot at location a, so their total is 16 points. The white player has 17 points - 11 along the left side and 6 in the top right corner. Therefore, the white player would win by 1 point.</li>
<li class="rule">Each player has an endless amount of dots to place.</li>
<li class="rule">A player can choose to pass on a turn. This usually occurs when the player cannot place any more dots, if they are able to place it in their own “eye” or the “eye” of the other player, causing it to be captured. The game ends if both players to pass their turn.</li>
<li class="rule">Strings of dots can only be formed when dots are horizontally or vertically adjacent (diagonals dont count).</li>
<li class="rule">A player may not self-capture. A self-capture would occur if a dot was placed inside a group of the opposite color.</li>
<li class="rule">For additional rules, check out <a href=https://www.britgo.org/intro/intro2.html>this page.</a></li>
</ul>
</body>
</html>