diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98e6ef6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.db diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..25f9952 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,35 @@ +AllCops: + TargetRubyVersion: 3.4.1 + +Layout/SpaceInsideArrayLiteralBrackets: + Enabled: false + +Style/StringLiterals: + Enabled: false + +Style/TrailingCommaInHashLiteral: + Enabled: false + +Style/TrailingCommaInArrayLiteral: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Layout/SpaceAroundOperators: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Layout/EmptyLineAfterGuardClause: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..47b322c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/.solargraph.yml b/.solargraph.yml new file mode 100644 index 0000000..5ff109f --- /dev/null +++ b/.solargraph.yml @@ -0,0 +1,25 @@ +--- +include: +- "**/*.rb" +exclude: +- spec/**/* +- test/**/* +- vendor/**/* +- ".bundle/**/*" +require: [] +domains: [] +reporters: +- rubocop +formatter: + rubocop: + cops: safe + except: [] + only: [] + extra_args: [] +require_paths: [] +plugins: [] +max_files: 5000 +useBundler: true +rubyVersion: "3.4.1" +workspace: + root: . diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3397785 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source "https://rubygems.org" + +gem "sinatra" + +gem "xxhash" + +gem "sequel" + +gem "json" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b4f6a62 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,40 @@ +GEM + remote: https://rubygems.org/ + specs: + base64 (0.3.0) + bigdecimal (3.2.2) + logger (1.7.0) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + rack (3.1.16) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + ruby2_keywords (0.0.5) + sequel (5.93.0) + bigdecimal + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + tilt (2.6.0) + xxhash (0.6.0) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + sequel + sinatra + xxhash + +BUNDLED WITH + 2.6.9 diff --git a/API.md b/JS_API.md similarity index 100% rename from API.md rename to JS_API.md diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..72e639b --- /dev/null +++ b/config/puma.rb @@ -0,0 +1 @@ +stdout_redirect "./log/puma.stdout.log", "./log/puma.stderr.log", true diff --git a/db.json b/db.json new file mode 100644 index 0000000..6919142 --- /dev/null +++ b/db.json @@ -0,0 +1,3 @@ +{ + "account_num": 6 +} \ No newline at end of file diff --git a/db.rb b/db.rb new file mode 100644 index 0000000..109e1be --- /dev/null +++ b/db.rb @@ -0,0 +1,78 @@ +require "xxhash" +require "sequel" +require "json" + +ALPHANUM = [*"0".."9", *"A".."Z", *"a".."z", "-", "_"].freeze + +# DataBase handler module +module DataBase + db_file = File.expand_path("infinsweeper.db") + DB = Sequel.connect("sqlite:///#{db_file}", single_threaded: false) + + def self.player_list + DB["select * from Players"].all + end + + def self.rm_player(email) + DB["delete from Players where email = ?", email].delete + end + + def self.mk_player(username, email, pass) + raise ArgumentError, "Email format is wrong!" unless email.match?(/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/) + raise ArgumentError, "Password must be at least 8 characters and valid format." unless + pass.match?(/\A[a-zA-Z0-9_.!?@#$%^&*()+=-]+\z/) && pass.length >= 8 + + digest = XXhash.xxh32(pass, 1234) + + path = File.expand_path("db.json") + json = File.exist?(path) ? JSON.parse(File.read(path)) : {} + json["account_num"] ||= 0 + account_num = json["account_num"] + json["account_num"] += 1 + File.write(path, JSON.pretty_generate(json)) + account_num = XXhash.xxh64(account_num, 1234) + code = "" + while account_num.positive? + code << ALPHANUM[account_num % 64] + account_num /= 64 + end + code = code.reverse.rjust(12, "0") + + DB["insert into Players (email, digest, username, code) values (?, ?, ?, ?)", email, digest, username, code].insert + + send_email(email, username, code) + Thread.new do + sleep 24 * 60 * 60 + rm_player(email) unless verified?(email) + end + + "Successfully registered!" + rescue ArgumentError => e + e.message + rescue Sequel::UniqueConstraintViolation + "Account already exists with this email or username!" + end + + def self.verify(code) + DB["update Players set code = ? where code = ?", "!", code].update != 0 + end + + def self.[](email) + DB["select * from Players where email = ?", email].first + end + + def self.[]=(email, data) + DB["update Players set data = ? where email = ?", data, email].update + end + + def self.authorized?(email, pass) + digest = XXhash.xxh32(pass, 1234) + player = self[email] + player && player[:digest].to_i == digest.to_i ? player : false + end + + def self.verified?(email) + player = self[email] + player && player[:code] == "!" + end +end diff --git a/event_bus.rb b/event_bus.rb new file mode 100644 index 0000000..d9466d1 --- /dev/null +++ b/event_bus.rb @@ -0,0 +1,12 @@ +# Event bus +module EventBus + @events = {} + + def self.on(event, &callback) + @events[event] = callback + end + + def self.get(event, *args) + @events[event]&.call(*args) + end +end diff --git a/game_logic.rb b/game_logic.rb new file mode 100644 index 0000000..76219f3 --- /dev/null +++ b/game_logic.rb @@ -0,0 +1,100 @@ +require "xxhash" + +# Game logic main class +class GameLogic + @seed = rand(111_111..999_999) + @pos = { + board: {}, + lost: {}, + cache: {}, + } + + # Returns a pseudorandom number between 0 and 100 + def self.hash(data) + XXhash.xxh32(data.to_s, @seed) % 100 + end + + def self.reset_pos + @pos.each_value(&:clear) + end + + def self.reveal(g_x, g_y) + l_x = ((g_x % 9) + 9) % 9 + l_y = ((g_y % 9) + 9) % 9 + s_x = (g_x / 9).floor + s_y = (g_y / 9).floor + + return if @pos.board["#{s_x}:#{s_y}"] == true + build_sector(s_x, s_y) if @pos.board["#{s_x}:#{s_y}"].nil? + return if @pos.board["#{s_x}:#{s_y}"][l_x][l_y][1] || @pos.board["#{s_x}:#{s_y}"][l_x][l_y][2] + + if @pos.board["#{s_x}:#{s_y}"][l_x][l_y][0] == -1 + @pos.lost["#{s_x}:#{s_y}"] = 1 + elsif @pos.board["#{s_x}:#{s_y}"][l_x][l_y][0].zero? + (-1..1).each do |x| + (-1..1).each do |y| + next if x.zero? && y.zero? + reveal(g_x + x, g_y + y) + end + end + else + @pos.board["#{s_x}:#{s_y}"][l_x][l_y][1] = true + end + + @pos.board["#{s_x}:#{s_y}"] = true if solved?(s_x, s_y) + end + + def self.solved?(s_x, s_y) + (0..8).each do |l_x| + (0..8).each do |l_y| + return false if !@pos.board["#{s_x}:#{s_y}"][l_x][l_y][1] && @pos.board["#{s_x}:#{s_y}"][l_x][l_y][0] != -1 + end + end + true + end + + def self.count(g_x, g_y) + sum = 0 + (-1..1).each do |x| + (-1..1).each do |y| + next if x.zero? && y.zero? + sum += 1 if mine?(g_x + x, g_y + y) + end + end + sum + end + + def self.build_sector(s_x, s_y) + @pos.board["#{s_x}:#{s_y}"] = [] + (0..8).each do |l_x| + @pos.board["#{s_x}:#{s_y}"][l_x] = [] + (0..8).each do |l_y| + g_x = l_x + s_x * 9 + g_y = l_y + s_y * 9 + @pos.board["#{s_x}:#{s_y}"][l_x][l_y] = [ + mine?(g_x, g_y) ? -1 : count(g_x, g_y), + false, false, + ] + end + end + end + + def self.build_cache(s_x, s_y) + return if @pos.cache["#{s_x}:#{s_y}"] + return unless @pos.board["#{s_x}:#{s_y}"] == 1 + @pos.cache["#{s_x}:#{s_y}"] = [] + (0..8).each do |l_x| + @pos.cache["#{s_x}:#{s_y}"][l_x] = [] + (0..8).each do |l_y| + g_x = l_x + s_x * 9 + g_y = l_y + s_y * 9 + @pos.cache["#{s_x}:#{s_y}"][l_x][l_y] = + mine?(g_x, g_y) ? [-1, false, true] : [count(g_x, g_y), true, false] + end + end + end + + def self.mine?(g_x, g_y) + g_x == 4 || g_y == 4 ? false : hash("#{g_x}:#{g_y}") < 17 + end +end diff --git a/log/puma.stderr.log b/log/puma.stderr.log new file mode 100644 index 0000000..5682dad --- /dev/null +++ b/log/puma.stderr.log @@ -0,0 +1,60 @@ +=== puma startup: 2025-06-19 18:12:41 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:13:06 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:13:21 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:13:35 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:15:41 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:15:52 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:16:47 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:17:35 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:18:08 +0300 === +127.0.0.1 - - [19/Jun/2025:18:18:17 +0300] "GET / HTTP/1.1" 200 460 0.0036 +127.0.0.1 - - [19/Jun/2025:18:18:17 +0300] "GET / HTTP/1.1" 304 - 0.0010 +127.0.0.1 - - [19/Jun/2025:18:18:18 +0300] "GET / HTTP/1.1" 304 - 0.0004 +127.0.0.1 - - [19/Jun/2025:18:18:18 +0300] "GET / HTTP/1.1" 304 - 0.0005 +127.0.0.1 - - [19/Jun/2025:18:18:19 +0300] "GET / HTTP/1.1" 304 - 0.0008 +127.0.0.1 - - [19/Jun/2025:18:18:19 +0300] "GET / HTTP/1.1" 304 - 0.0005 +127.0.0.1 - - [19/Jun/2025:18:18:19 +0300] "GET / HTTP/1.1" 304 - 0.0005 +127.0.0.1 - - [19/Jun/2025:18:18:20 +0300] "GET / HTTP/1.1" 304 - 0.0006 +127.0.0.1 - - [19/Jun/2025:18:18:20 +0300] "GET / HTTP/1.1" 304 - 0.0010 +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:19:57 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:21:11 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:22:09 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:22:45 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:23:15 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:23:34 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-19 18:23:45 +0300 === +127.0.0.1 - - [19/Jun/2025:18:24:05 +0300] "GET / HTTP/1.1" 304 - 0.0032 +127.0.0.1 - - [19/Jun/2025:18:32:41 +0300] "GET / HTTP/1.1" 304 - 0.0006 +127.0.0.1 - - [19/Jun/2025:23:53:35 +0300] "GET / HTTP/1.1" 304 - 0.0005 +=== puma startup: 2025-06-20 23:53:14 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-20 23:53:29 +0300 === +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-20 23:53:35 +0300 === +127.0.0.1 - - [20/Jun/2025:23:53:55 +0300] "GET / HTTP/1.1" 304 - 0.0024 +127.0.0.1 - - [20/Jun/2025:23:53:55 +0300] "GET /src/js/game_logic.js HTTP/1.1" 304 - 0.0005 +127.0.0.1 - - [20/Jun/2025:23:53:55 +0300] "GET /src/js/game_renderer.js HTTP/1.1" 304 - 0.0064 +== Sinatra has ended his set (crowd applauds) +=== puma startup: 2025-06-21 14:36:03 +0300 === +127.0.0.1 - - [21/Jun/2025:14:36:35 +0300] "GET / HTTP/1.1" 304 - 0.0036 +127.0.0.1 - - [21/Jun/2025:14:36:35 +0300] "GET /src/js/event_handler.js HTTP/1.1" 304 - 0.0010 +127.0.0.1 - - [21/Jun/2025:14:36:35 +0300] "GET /src/js/ui_renderer.js HTTP/1.1" 304 - 0.0081 +127.0.0.1 - - [21/Jun/2025:14:36:35 +0300] "GET /src/js/saver.js HTTP/1.1" 304 - 0.0009 +127.0.0.1 - - [21/Jun/2025:14:36:36 +0300] "GET / HTTP/1.1" 304 - 0.0007 +127.0.0.1 - - [21/Jun/2025:14:36:36 +0300] "GET / HTTP/1.1" 304 - 0.0006 +== Sinatra has ended his set (crowd applauds) diff --git a/log/puma.stdout.log b/log/puma.stdout.log new file mode 100644 index 0000000..4956e38 --- /dev/null +++ b/log/puma.stdout.log @@ -0,0 +1,47 @@ +=== puma startup: 2025-06-19 18:12:41 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma shutdown: 2025-06-19 18:13:05 +0300 === +- Goodbye! +=== puma startup: 2025-06-19 18:13:06 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma shutdown: 2025-06-19 18:13:13 +0300 === +- Goodbye! +=== puma startup: 2025-06-19 18:13:21 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:13:35 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:15:41 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:15:52 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:16:47 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:17:35 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma shutdown: 2025-06-19 18:17:44 +0300 === +- Goodbye! +=== puma startup: 2025-06-19 18:18:08 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:19:57 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:21:11 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:22:09 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:22:45 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:23:15 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma shutdown: 2025-06-19 18:23:22 +0300 === +- Goodbye! +=== puma startup: 2025-06-19 18:23:34 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-19 18:23:45 +0300 === +=== puma startup: 2025-06-20 23:53:14 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-20 23:53:29 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-20 23:53:35 +0300 === +- Gracefully stopping, waiting for requests to finish +=== puma startup: 2025-06-21 14:36:03 +0300 === +- Gracefully stopping, waiting for requests to finish diff --git a/main.rb b/main.rb new file mode 100644 index 0000000..5f38633 --- /dev/null +++ b/main.rb @@ -0,0 +1,7 @@ +require "sinatra" + +set :public_folder, "public" + +get "/" do + send_file "index.html" +end diff --git a/src/assets/img/logo_large.png b/public/src/assets/img/logo_large.png similarity index 100% rename from src/assets/img/logo_large.png rename to public/src/assets/img/logo_large.png diff --git a/src/assets/img/logo_sm.png b/public/src/assets/img/logo_sm.png similarity index 100% rename from src/assets/img/logo_sm.png rename to public/src/assets/img/logo_sm.png diff --git a/src/assets/img/minesweeper.png b/public/src/assets/img/minesweeper.png similarity index 100% rename from src/assets/img/minesweeper.png rename to public/src/assets/img/minesweeper.png diff --git a/src/assets/style.css b/public/src/assets/style.css similarity index 100% rename from src/assets/style.css rename to public/src/assets/style.css diff --git a/src/js/constants.js b/public/src/js/constants.js similarity index 100% rename from src/js/constants.js rename to public/src/js/constants.js diff --git a/src/js/event_bus.js b/public/src/js/event_bus.js similarity index 100% rename from src/js/event_bus.js rename to public/src/js/event_bus.js diff --git a/src/js/event_handler.js b/public/src/js/event_handler.js similarity index 100% rename from src/js/event_handler.js rename to public/src/js/event_handler.js diff --git a/src/js/game_controller.js b/public/src/js/game_controller.js similarity index 100% rename from src/js/game_controller.js rename to public/src/js/game_controller.js diff --git a/src/js/game_logic.js b/public/src/js/game_logic.js similarity index 100% rename from src/js/game_logic.js rename to public/src/js/game_logic.js diff --git a/src/js/game_renderer.js b/public/src/js/game_renderer.js similarity index 100% rename from src/js/game_renderer.js rename to public/src/js/game_renderer.js diff --git a/src/js/index.js b/public/src/js/index.js similarity index 100% rename from src/js/index.js rename to public/src/js/index.js diff --git a/src/js/saver.js b/public/src/js/saver.js similarity index 100% rename from src/js/saver.js rename to public/src/js/saver.js diff --git a/src/js/third_party_utils.js b/public/src/js/third_party_utils.js similarity index 100% rename from src/js/third_party_utils.js rename to public/src/js/third_party_utils.js diff --git a/src/js/ui_renderer.js b/public/src/js/ui_renderer.js similarity index 100% rename from src/js/ui_renderer.js rename to public/src/js/ui_renderer.js diff --git a/src/js/utils.js b/public/src/js/utils.js similarity index 100% rename from src/js/utils.js rename to public/src/js/utils.js diff --git a/run.fish b/run.fish new file mode 100644 index 0000000..e334d9d --- /dev/null +++ b/run.fish @@ -0,0 +1,31 @@ +set pipe_path /tmp/infin +set oldsum (cat (find . -name '*.rb' | sort) | md5sum) +if not test -p infinsweeper.db + sqlite3 infinsweeper.db < schema.sql +end +pkill ruby +ruby main.rb -p8080 & +if not test -p $pipe_path + touch $pipe_path +end +while true + sleep 1 + set newsum (cat (find . -name '*.rb' | sort) | md5sum) + if test "$oldsum" != "$newsum" + set oldsum $newsum + pkill ruby + ruby main.rb -p8080 & + end + if set line (cat $pipe_path) + echo $line + if test "$line" = "die" + pkill ruby + echo "" > $pipe_path + exit + end + if test "$line" = "open" + echo "" > $pipe_path + xdg-open http://localhost:8080 + end + end +end diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..9fa6a13 --- /dev/null +++ b/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE Players ( + email TEXT PRIMARY KEY, + username TEXT UNIQUE, + digest BLOB, + data BLOB, + code TEXT +);