diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9dc66cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.js linguist-vendored +*.css linguist-vendored +*.scss linguist-vendored=false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78d79f2..88d3436 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /lib/ /bin/ /.shards/ +game_saves.db +.sass-cache* \ No newline at end of file diff --git a/Go b/Go new file mode 100644 index 0000000..b68ba69 Binary files /dev/null and b/Go differ diff --git a/public/css/main.css b/public/css/main.css index 4dfa801..e805303 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -88,6 +88,7 @@ div.top-bar { .board-cell { display: inline-block; position: relative; +<<<<<<< HEAD padding: 5.5555555556%; } .board-cell .overlay { @@ -109,6 +110,24 @@ div.top-bar { .board-cell.large { padding: 2.6315789474%; } +======= + 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%; } +>>>>>>> 733c32748eb734f126d45e84e3609b04d4f1f023 .black-cell .overlay { background-color: black; diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..8b6346c --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,94 @@ +body { + font-family: Roboto; + background-image: url(/images/clouds.jpg); + background-color: #F5F7FA; + 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%; +} diff --git a/public/images/claire.jpg b/public/images/claire.jpg new file mode 100644 index 0000000..b6b3f8a Binary files /dev/null and b/public/images/claire.jpg differ diff --git a/public/images/clouds.jpg b/public/images/clouds.jpg new file mode 100644 index 0000000..17d7526 Binary files /dev/null and b/public/images/clouds.jpg differ diff --git a/public/images/danila.jpg b/public/images/danila.jpg new file mode 100644 index 0000000..d336567 Binary files /dev/null and b/public/images/danila.jpg differ diff --git a/public/images/matthew.jpg b/public/images/matthew.jpg new file mode 100644 index 0000000..d336567 Binary files /dev/null and b/public/images/matthew.jpg differ diff --git a/public/images/michael.jpg b/public/images/michael.jpg new file mode 100644 index 0000000..d336567 Binary files /dev/null and b/public/images/michael.jpg differ diff --git a/public/scss/css/main.css b/public/scss/css/main.css new file mode 100644 index 0000000..7bfce3c --- /dev/null +++ b/public/scss/css/main.css @@ -0,0 +1,158 @@ +h1, h2, h3, h4, h5, h6 { + font-family: "Comfortaa", serif; + text-align: center; +} + +h1 { + font-size: 5em; + margin: 0px; +} + +body { + font-family: "Comfortaa", sans-serif; + margin: 0px; + background-color: #f4f4f4; +} + +.content-wrapper { + max-width: 750px; + margin: auto; +} + +li { + list-style: none; +} + +ul.game-instructions { + display: flex; +} + +div.top-bar { + background-image: url(../../src/Go/views/grid.jpg); + line-height: 135px; +} + +.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 0.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 0.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 0.25s, color 0.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 0.25s, border-width 0.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; +} + +/*# sourceMappingURL=css/main.css.map */ diff --git a/src/Go.cr b/src/Go.cr index 12cfbd6..e9b3e89 100644 --- a/src/Go.cr +++ b/src/Go.cr @@ -2,29 +2,152 @@ 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" +GAME_DB = DB.open "sqlite3:./#{GAME_SAVE}" -def query_game(db, id) : Go::Game? - return nil +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 + 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 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]? return game else 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 end end 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 end 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(" ") command = split_command[0] if command == "place" @@ -34,9 +157,15 @@ def handle_message(id, game, socket, message) game.update(x, y, color) game.sockets.each { |socket| socket.send game.to_json } + # If saving game on move + save_game(GAME_DB, id, game) end end +get "/about" do |env| + render "src/Go/views/about.ecr" +end + get "/" do |env| render "src/Go/views/index.ecr", "src/Go/views/base.ecr" end @@ -46,7 +175,7 @@ post "/game" do |env| game_password = env.params.body["password"]? if game_id == nil || game_password == nil render_404 - elsif game = lookup_game(nil, GAME_CACHE, game_id) + elsif game = lookup_game(GAME_DB, GAME_CACHE, game_id) id = game_id size = game.size.value black = nil @@ -78,7 +207,7 @@ post "/create" do |env| 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) + elsif game = lookup_game(GAME_DB, GAME_CACHE, game_id) render_404 else color_e = color_e.as(Go::Color) @@ -101,7 +230,7 @@ end ws "/game/:id" do |socket, env| 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 game.sockets << socket @@ -117,4 +246,22 @@ ws "/game/:id" do |socket, env| 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 + +# If exit is disabled in kemal.stop +# For save on close +# at_exit do +# save_all(GAME_CACHE) +# end diff --git a/src/Go/Game.cr b/src/Go/Game.cr index 546bb6f..0f46560 100644 --- a/src/Go/Game.cr +++ b/src/Go/Game.cr @@ -20,6 +20,14 @@ module Go 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 @@ -133,7 +141,7 @@ module Go end 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 diff --git a/src/Go/views/about.ecr b/src/Go/views/about.ecr new file mode 100644 index 0000000..4a5ec5d --- /dev/null +++ b/src/Go/views/about.ecr @@ -0,0 +1,72 @@ + + + + + About Us + + + + + + + +
+

Hello!

+

+ 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. +

+
+
+

Group Members

+
+
+
+
+ daa +
+

Claire Cahill

+

+ 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. +

+
+
+ +
+
+
+ daa +
+

Danila Fedorin

+

+ 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. +

+
+
+ +
+
+
+ daa +
+

Michael Huang

+

+ Michael created this about page! +

+
+
+ +
+
+
+ daa +
+

Matthew Sessions

+

+ 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. +

+
+
+
+
+ +