Merge branch 'matt-server'

This commit is contained in:
dogcatfee 2018-06-06 18:57:41 -07:00
commit 164cf6a73a
28 changed files with 10805 additions and 1 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/docs/
/lib/
/bin/
/.shards/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "external/GoUI"]
path = external/GoUI
url = https://dev.danilafe.com/CS-290/GoUI.git

1
.travis.yml Normal file
View File

@ -0,0 +1 @@
language: crystal

BIN
Go Normal file

Binary file not shown.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Danila Fedorin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
generate-elm :
cd external/GoUI && elm make Go.elm --output ../../public/js/Go.js
generate-css :
scss public/scss/main.scss > public/css/main.css

View File

@ -1 +1,40 @@
# Final Project for Group 4
# Go
This is an implementation of a game of GO in web application form.
## Installation
TODO: Write installation instructions for Windows
___
TODO: Write installation instructions for Mac
___
Arch Linux:
1. Install dependencies: `pacman -S crystal shards ruby-sass`
2. Install Shards: `shards install`
3. TODO: Add elm compilation
4. Compile SASS Style: `make generate-css`
5. Compile Crystal app: `crystal build --release src/Go.cr`
6. Run with `./Go`
___
## Usage
While crystal app is running, go to localhost with port of 3000 in the browser.
## Development
TODO: Write development instructions here
## Contributing
1. Fork it ( https://github.com/[your-github-name]/Go/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
## Contributors
- [[your-github-name]](https://github.com/[your-github-name]) Danila Fedorin - creator, maintainer
- [sessionm21](https://github.com/sessionm21) Matthew Sessions - hax0r, maintainer

1
external/GoUI vendored Submodule

@ -0,0 +1 @@
Subproject commit b68222dab7a766ede897ba9bdc68f262e2194e3d

BIN
game_saves.db Normal file

Binary file not shown.

115
public/css/main.css Normal file
View File

@ -0,0 +1,115 @@
h1, h2, h3, h4, h5, h6 {
font-family: "Indie Flower", serif;
text-align: center; }
h1 {
font-size: 5em;
margin: 0px; }
body {
font-family: "Raleway", sans-serif;
margin: 0px;
background-color: #f4f4f4; }
.content-wrapper {
max-width: 750px;
margin: auto; }
.board {
background-color: tomato;
padding: 20px;
max-width: 500px;
margin: auto;
border-radius: 10px; }
.black-player .board-cell:hover .overlay {
background-color: black; }
.white-player .board-cell:hover .overlay {
background-color: white; }
.board-cell {
display: inline-block;
position: relative;
padding: 5.5555555556%; }
.board-cell .overlay {
position: absolute;
box-sizing: border-box;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
border-radius: 50%;
transition: background-color .25s; }
.board-cell.small {
padding: 5.5555555556%; }
.board-cell.medium {
padding: 3.8461538462%; }
.board-cell.large {
padding: 2.6315789474%; }
.black-cell .overlay {
background-color: black; }
.white-cell .overlay {
background-color: white; }
.split-wrapper {
display: flex;
width: 100%; }
@media screen and (max-width: 640px) {
.split-wrapper {
flex-direction: column; } }
.split-item {
flex-grow: 1;
box-sizing: border-box; }
.split-wrapper form {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px; }
.split-wrapper form input {
margin-top: 20px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
border: none;
outline: none;
width: 100%;
max-width: 300px; }
.split-wrapper form input[type="radio"] {
opacity: 0;
width: 0px;
height: 0px; }
.split-wrapper form input[type="radio"]:checked ~ label {
color: tomato;
transition: color .25s; }
.split-wrapper form input[type="submit"] {
padding: 10px;
background-color: tomato;
color: white; }
.split-wrapper form input[type="submit"]:focus, .split-wrapper form input[type="submit"]:hover {
background-color: inherit;
color: tomato;
transition: background-color .25s, color .25s; }
.split-wrapper form input[type="text"] {
background-color: inherit;
border-bottom: solid tomato;
border-width: 2px;
height: 3em;
box-sizing: border-box;
display: block; }
.split-wrapper form input[type="text"]:focus {
border-width: 3px;
transition: background-color .25s, border-width .25s; }
.split-wrapper form .radio-parent {
display: flex;
margin-top: 20px;
width: 100%;
max-width: 300px; }
.split-wrapper form .radio-wrapper {
flex-grow: 1;
display: inline-block;
text-align: center; }

9744
public/js/Go.js Normal file

File diff suppressed because it is too large Load Diff

169
public/scss/main.scss Normal file
View File

@ -0,0 +1,169 @@
$background-grey: #f4f4f4;
@mixin board-cell($size) {
padding: 100%/$size/2;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Indie Flower", serif;
text-align: center;
}
h1 {
font-size: 5em;
margin: 0px;
}
body {
font-family: "Raleway", sans-serif;
margin: 0px;
background-color: $background-grey;
}
.content-wrapper {
max-width: 750px;
margin: auto;
}
.board {
background-color: tomato;
padding: 20px;
max-width: 500px;
margin: auto;
border-radius: 10px;
}
.black-player {
.board-cell:hover .overlay {
background-color: black;
}
}
.white-player {
.board-cell:hover .overlay {
background-color: white;
}
}
.board-cell {
display: inline-block;
position: relative;
@include board-cell(9);
.overlay {
position: absolute;
box-sizing: border-box;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
border-radius: 50%;
transition: background-color .25s;
}
&.small {
@include board-cell(9);
}
&.medium {
@include board-cell(13);
}
&.large {
@include board-cell(19);
}
}
.black-cell {
.overlay {
background-color: black;
}
}
.white-cell {
.overlay {
background-color: white;
}
}
.split-wrapper {
display: flex;
@media screen and (max-width: 640px) {
flex-direction: column;
}
width: 100%;
}
.split-item {
flex-grow: 1;
box-sizing: border-box;
}
.split-wrapper form {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
input {
margin-top: 20px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
border: none;
outline: none;
width: 100%;
max-width: 300px;
}
input[type="radio"] {
opacity: 0;
width: 0px;
height: 0px;
&:checked ~ label {
color: tomato;
transition: color .25s;
}
}
input[type="submit"] {
padding: 10px;
background-color: tomato;
color: white;
&:focus, &:hover {
background-color: inherit;
color: tomato;
transition: background-color .25s, color .25s;
}
}
input[type="text"] {
background-color: inherit;
border-bottom: solid tomato;
border-width: 2px;
&:focus {
border-width: 3px;
transition: background-color .25s, border-width .25s;
}
height: 3em;
box-sizing: border-box;
display: block;
}
.radio-parent {
display: flex;
margin-top: 20px;
width: 100%;
max-width: 300px;
}
.radio-wrapper {
flex-grow: 1;
display: inline-block;
text-align: center;
}
}

59
public/svg/black.svg Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="100"
width="100"
version="1.1"
id="svg4"
sodipodi:docname="black.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1159"
inkscape:window-height="719"
id="namedview6"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="307"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<circle
cx="50"
cy="50"
r="40"
stroke="black"
stroke-width="3"
fill="red"
id="circle2"
style="fill:#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

58
public/svg/circle.svg Normal file
View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="100"
width="100"
version="1.1"
id="svg4"
sodipodi:docname="circle.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1159"
inkscape:window-height="719"
id="namedview6"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="307"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<circle
cx="50"
cy="50"
r="40"
stroke="black"
stroke-width="3"
fill="red"
id="circle2"
style="fill:#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

64
public/svg/cross.svg Normal file
View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="100"
width="100"
version="1.1"
id="svg14"
sodipodi:docname="crossB.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1028"
id="namedview16"
showgrid="false"
inkscape:zoom="8.27"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg14" />
<line
x1="50"
y1="0"
x2="50"
y2="100"
style="stroke:rgb(0,0,0);stroke-width:2"
id="line10" />
<line
x1="0"
y1="50"
x2="100"
y2="50"
style="stroke:rgb(0,0,0);stroke-width:2"
id="line12" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

59
public/svg/white.svg Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="100"
width="100"
version="1.1"
id="svg4"
sodipodi:docname="white.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1159"
inkscape:window-height="719"
id="namedview6"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="307"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<circle
cx="50"
cy="50"
r="40"
stroke="black"
stroke-width="3"
fill="red"
id="circle2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

22
shard.lock Normal file
View File

@ -0,0 +1,22 @@
version: 1.0
shards:
db:
github: crystal-lang/crystal-db
version: 0.5.0
kemal:
github: kemalcr/kemal
version: 0.22.0
kilt:
github: jeromegn/kilt
version: 0.4.0
radix:
github: luislavena/radix
version: 0.3.8
sqlite3:
github: crystal-lang/crystal-sqlite3
version: 0.9.0

21
shard.yml Normal file
View File

@ -0,0 +1,21 @@
name: Go
version: 0.1.0
authors:
- Danila Fedorin <danila.fedorin@gmail.com>
targets:
Go:
main: src/Go.cr
dependencies:
kemal:
github: kemalcr/kemal
db:
github: crystal-lang/crystal-db
sqlite3:
github: crystal-lang/crystal-sqlite3
crystal: 0.24.2
license: MIT

9
spec/Go_spec.cr Normal file
View File

@ -0,0 +1,9 @@
require "./spec_helper"
describe Go do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end

2
spec/spec_helper.cr Normal file
View File

@ -0,0 +1,2 @@
require "spec"
require "../src/Go"

199
src/Go.cr Normal file
View File

@ -0,0 +1,199 @@
require "./Go/*"
require "kemal"
require "json"
require "db"
require "sqlite3"
URL = "localhost"
PORT = "3000"
GAME_CACHE = {} of String => Go::Game
GAME_SAVE = "./game_saves.db"
def save_game(db, gameid, game)
# Function: save_game
# Parameters: db(String)[Unused] gameid(String) game(Go::Game)
turn, size, white_pass, black_pass, board = game.encode
DB.open "sqlite3:./#{GAME_SAVE}" do |db|
# Create table if one does not exist, gameid is UNIQUE => No duplicates
db.exec "create table if not exists game_saves (gameid string, turn integer, size integer, white_pass string, black_pass string, board string, UNIQUE(gameid) )"
# 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, board
end
end
def query_game(db, gameid) : Go::Game?
# Function: query_game
# Parameters: db(String)[Unused] gameid(String)
turn = 0
size = Go::Size::Small
white_pass = ""
black_pass = ""
board = ""
begin
DB.open "sqlite3:./#{GAME_SAVE}" do |db|
# Query whole row where the gameid is found
db.query "SELECT * FROM game_saves WHERE gameid = ?", gameid do |rs|
rs.each do
id = rs.read(String) # Reduntant
turn = rs.read(Int32)
size = rs.read(Int32)
white_pass = rs.read(String)
black_pass = rs.read(String)
board = rs.read(String)
end
end
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
puts "DB query Failed"
return nil
end
# Finished Go::Game object to return
return game
end
def lookup_game(db, cache, id) : Go::Game?
# Function: lookup_game
# Parameters: db(String)[Unused] cache({(String), (Go::Game)}) id(String)
if game = cache[id]?
return game
else
loaded_game = query_game(db, id)
# Need to convert id to string for some reason
cache[id.to_s] = loaded_game if loaded_game
return loaded_game
end
end
def create_game(db, cache, game, id)
# Function: create_game
# Parameters: db(String)[Unused] cache({(String), (Go::Game)}) game(Go::Game) id(String)
cache[id] = game
end
def handle_message(id, game, socket, message)
# Function: handle_message
# Parameters: id(String) game(Go::Game) socket() message()
split_command = message.split(" ")
command = split_command[0]
if command == "place"
x = split_command[1].to_i8
y = split_command[2].to_i8
color = split_command[3] == "Black" ? Go::Color::Black : Go::Color::White
game.update(x, y, color)
game.sockets.each { |socket| socket.send game.to_json }
end
end
get "/" do |env|
render "src/Go/views/index.ecr", "src/Go/views/base.ecr"
end
get "/save" do |env|
GAME_CACHE.each do |game_hash|
gameid, game = game_hash
save_game("none", gameid, game)
end
end
post "/game" do |env|
game_id = env.params.body["id"]?
game_password = env.params.body["password"]?
if game_id == nil || game_password == nil
render_404
elsif game = lookup_game(nil, GAME_CACHE, game_id)
id = game_id
size = game.size.value
black = nil
if game_password == game.black_pass
black = true
elsif game_password == game.white_pass
black = false
end
black.try { |black| render "src/Go/views/game.ecr", "src/Go/views/base.ecr"} || render_404
else
render_404
end
end
post "/create" do |env|
game_id = env.params.body["id"]?
user_password = env.params.body["your-password"]?
other_password = env.params.body["their-password"]?
color = env.params.body["color"]?
color_e = nil
if color == "black"
color_e = Go::Color::Black
elsif color == "white"
color_e = Go::Color::White
end
if game_id == nil || user_password == nil || other_password == nil || color == nil || color_e == nil
render_404
elsif game = lookup_game(nil, GAME_CACHE, game_id)
render_404
else
color_e = color_e.as(Go::Color)
user_password = user_password.as(String)
other_password = other_password.as(String)
if color_e == Go::Color::Black
white_pass, black_pass = other_password, user_password
else
white_pass, black_pass = user_password, other_password
end
game = Go::Game.new(Go::Size::Small, black_pass, white_pass)
create_game(nil, GAME_CACHE, game, game_id.as(String))
id = game_id
size = game.size.value
black = color_e == Go::Color::Black
render "src/Go/views/game.ecr", "src/Go/views/base.ecr"
end
end
ws "/game/:id" do |socket, env|
game_id = env.params.url["id"]
if game = lookup_game(nil, GAME_CACHE, game_id)
socket.send game.to_json
game.sockets << socket
socket.on_message do |message|
game.try { |game| handle_message(game_id, game, socket, message) }
end
socket.on_close do
game.try { |game| game.sockets.delete socket }
end
else
render_404
end
end
Kemal.run

147
src/Go/Game.cr Normal file
View File

@ -0,0 +1,147 @@
module Go
enum Color
Black
White
end
enum Size
Small = 9,
Medium = 13,
Large = 19
end
alias Board = Hash(Tuple(Int8, Int8), Color)
class Game
property size : Size
property black_pass : String
property white_pass : String
property board : Board
property turn : Color
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)
@size = size
@board = Board.new
@turn = Color::Black
@sockets = [] of HTTP::WebSocket
end
private def cell_json(index, color, json)
json.object do
json.field "index" do
json.object do
json.field "x", index[0]
json.field "y", index[1]
end
end
json.field "color", color.to_s
end
end
private def board_json(board, json)
json.array do
board.each do |key, value|
cell_json(key, value, json)
end
end
end
def to_json
JSON.build do |json|
json.object do
json.field "turn", @turn.to_s
json.field "board" { board_json(@board, json) }
end
end
end
private def count_neighbors(x, y, color, visited)
coord = {x, y}
if visited.includes?(coord) || (x < 0 || x >= @size.value || y < 0 || y >= @size.value)
return 0
else
visited.push(coord)
case @board[coord]?
when color
return count_neighbors(x - 1, y, color, visited) +
count_neighbors(x + 1, y, color, visited) +
count_neighbors(x, y - 1, color, visited) +
count_neighbors(x, y + 1, color, visited)
when nil
return 1
else
return 0
end
end
return 0
end
private def remove_color(x, y, color)
coord = {x, y}
if !(x < 0 || x >= @size.value || y < 0 || y >= @size.value) && @board[coord]? == color
@board.delete(coord)
remove_color(x - 1, y, color)
remove_color(x + 1, y, color)
remove_color(x, y - 1, color)
remove_color(x, y + 1, color)
end
end
private def try_remove_branch(x, y, color)
coord = {x, y}
if @board[coord]? == color
neighbor_count = count_neighbors(x, y, color, [] of Tuple(Int8, Int8))
if neighbor_count == 0
remove_color(x, y, color)
end
end
end
def invert(color)
color == Color::Black ? Color::White : Color::Black
end
def update(x, y, color)
coord = {x, y}
if @turn == color
@board[coord] = color
new_color = invert(color)
try_remove_branch(x - 1, y, new_color)
try_remove_branch(x + 1, y, new_color)
try_remove_branch(x, y - 1, new_color)
try_remove_branch(x, y + 1, new_color)
try_remove_branch(x, y, color)
@turn = new_color
end
end
private def color_char(color)
color == Color::Black ? 'B' : 'W'
end
private def board_string(board)
String.build do |str|
(0...@size.value).each do |x|
(0...@size.value).each do |y|
color = @board[{x.to_i8, y.to_i8}]?
char = color ? color_char(color) : 'E'
str << char
end
end
end
end
def encode
{ @turn, @size.value, @white_pass.to_s, @black_pass.to_s, board_string(@board) }
end
end
end

3
src/Go/version.cr Normal file
View File

@ -0,0 +1,3 @@
module Go
VERSION = "0.1.0"
end

13
src/Go/views/base.ecr Normal file
View File

@ -0,0 +1,13 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/main.css">
<link href="https://fonts.googleapis.com/css?family=Indie+Flower|Raleway" rel="stylesheet">
</head>
<body>
<div class="content-wrapper">
<h1>Go</h1>
<%= content %>
</div>
</body>
</html>

11
src/Go/views/game.ecr Normal file
View File

@ -0,0 +1,11 @@
<div id="elm-root" class="<%= black ? "black" : "white" %>-player"></div>
<script src="/js/Go.js"></script>
<script>
var node = document.getElementById('elm-root')
var app = Elm.Main.embed(node, {
'black' : <%= black %>,
'url' : "<%= "ws://" + URL + ":" + PORT %>",
'id' : "<%= id %>",
'size' : <%= size %>
})
</script>

29
src/Go/views/index.ecr Normal file
View File

@ -0,0 +1,29 @@
<div class="split-wrapper">
<div class="split-item">
<h2>Create Game</h2>
<form autocomplete="off" id="create-form" action="/create" method="post">
<input required placeholder="Session Name" type="text" name="id"></input>
<input required placeholder="Your Password" type="text" name="your-password"></input>
<input required placeholder="Their Password" type="text" name="their-password"></input>
<div class="radio-parent">
<div class="radio-wrapper">
<input type="radio" name="color" id="radio-black" value="black" checked></input>
<label for="radio-black">Black</label>
</div>
<div class="radio-wrapper">
<input type="radio" name="color" id="radio-white" value="white"></input>
<label for="radio-white">White</label>
</div>
</div>
<input type="submit" value="Create Game"></input>
</form>
</div>
<div class="split-item">
<h2>Join Game</h2>
<form autocomplete="off" id="join-form" action="/game" method="post">
<input required placeholder="Session Name" type="text" name="id"></input>
<input required placeholder="Your Password" type="text" name="password"></input>
<input type="submit" value="Join Game"></input>
</form>
</div>
</div>