├── log └── .keep ├── .nvmrc ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── stylesheets │ │ ├── pages │ │ │ ├── not_found.sass │ │ │ ├── puzzle_attempts.sass │ │ │ ├── puzzle_reports.sass │ │ │ └── about.sass │ │ ├── responsive.sass │ │ └── application.sass │ ├── images │ │ ├── favicon.ico │ │ └── pieces │ │ │ ├── disguised │ │ │ ├── b.svg │ │ │ ├── bB.svg │ │ │ ├── bK.svg │ │ │ ├── bN.svg │ │ │ ├── bP.svg │ │ │ ├── bQ.svg │ │ │ ├── bR.svg │ │ │ ├── w.svg │ │ │ ├── wB.svg │ │ │ ├── wK.svg │ │ │ ├── wN.svg │ │ │ ├── wP.svg │ │ │ ├── wQ.svg │ │ │ └── wR.svg │ │ │ ├── letter │ │ │ ├── bP.svg │ │ │ ├── wP.svg │ │ │ ├── bK.svg │ │ │ └── wK.svg │ │ │ ├── mono │ │ │ ├── R.svg │ │ │ ├── P.svg │ │ │ ├── K.svg │ │ │ ├── N.svg │ │ │ ├── B.svg │ │ │ └── Q.svg │ │ │ ├── alpha │ │ │ ├── bP.svg │ │ │ ├── bR.svg │ │ │ ├── wR.svg │ │ │ ├── bN.svg │ │ │ ├── wP.svg │ │ │ ├── bB.svg │ │ │ ├── bQ.svg │ │ │ └── wK.svg │ │ │ ├── cburnett │ │ │ ├── bP.svg │ │ │ ├── wP.svg │ │ │ ├── wR.svg │ │ │ ├── bR.svg │ │ │ ├── wN.svg │ │ │ ├── wK.svg │ │ │ ├── bB.svg │ │ │ ├── wB.svg │ │ │ ├── wQ.svg │ │ │ ├── bK.svg │ │ │ ├── bN.svg │ │ │ └── bQ.svg │ │ │ ├── kiwen-suwi │ │ │ ├── bQ.svg │ │ │ ├── bR.svg │ │ │ ├── bP.svg │ │ │ ├── wR.svg │ │ │ ├── wQ.svg │ │ │ ├── bK.svg │ │ │ ├── bN.svg │ │ │ ├── bB.svg │ │ │ └── wP.svg │ │ │ ├── kosal │ │ │ ├── bR.svg │ │ │ ├── bN.svg │ │ │ ├── wR.svg │ │ │ ├── bP.svg │ │ │ ├── bB.svg │ │ │ ├── bK.svg │ │ │ ├── wB.svg │ │ │ └── wP.svg │ │ │ ├── pixel │ │ │ ├── wP.svg │ │ │ ├── bP.svg │ │ │ ├── bB.svg │ │ │ ├── wB.svg │ │ │ ├── bQ.svg │ │ │ ├── wQ.svg │ │ │ ├── bR.svg │ │ │ ├── wR.svg │ │ │ ├── bK.svg │ │ │ ├── wK.svg │ │ │ ├── bN.svg │ │ │ └── wN.svg │ │ │ ├── shapes │ │ │ ├── bR.svg │ │ │ ├── wR.svg │ │ │ ├── bP.svg │ │ │ ├── wP.svg │ │ │ ├── bN.svg │ │ │ ├── wN.svg │ │ │ ├── bK.svg │ │ │ ├── wK.svg │ │ │ ├── bB.svg │ │ │ ├── wB.svg │ │ │ ├── bQ.svg │ │ │ └── wQ.svg │ │ │ ├── chessnut │ │ │ ├── wP.svg │ │ │ ├── bP.svg │ │ │ ├── wN.svg │ │ │ └── bN.svg │ │ │ ├── firi │ │ │ ├── bP.svg │ │ │ └── wP.svg │ │ │ ├── chess7 │ │ │ ├── bR.svg │ │ │ └── bP.svg │ │ │ ├── mpchess │ │ │ ├── bP.svg │ │ │ ├── wB.svg │ │ │ ├── wP.svg │ │ │ ├── wN.svg │ │ │ └── bB.svg │ │ │ ├── pirouetti │ │ │ ├── bR.svg │ │ │ ├── wR.svg │ │ │ ├── wB.svg │ │ │ ├── bB.svg │ │ │ ├── bP.svg │ │ │ ├── wP.svg │ │ │ ├── wN.svg │ │ │ ├── bN.svg │ │ │ └── bQ.svg │ │ │ ├── anarcandy │ │ │ ├── bP.svg │ │ │ └── wP.svg │ │ │ ├── fresca │ │ │ ├── bR.svg │ │ │ ├── wR.svg │ │ │ ├── bP.svg │ │ │ └── wP.svg │ │ │ ├── reillycraig │ │ │ ├── bR.svg │ │ │ └── wR.svg │ │ │ ├── leipzig │ │ │ └── bP.svg │ │ │ ├── merida │ │ │ ├── bP.svg │ │ │ ├── wR.svg │ │ │ └── bR.svg │ │ │ └── riohacha │ │ │ ├── bP.svg │ │ │ └── wP.svg │ └── config │ │ └── manifest.js ├── views │ ├── puzzles │ │ ├── not_found.slim │ │ ├── edit.slim │ │ └── index.slim │ ├── shared │ │ └── _main_footer.slim │ ├── game_modes │ │ ├── rated │ │ │ ├── puzzle_attempt.slim │ │ │ ├── puzzle_attempts_list.slim │ │ │ └── puzzles.slim │ │ ├── speedrun │ │ │ └── puzzles.slim │ │ ├── rated.slim │ │ ├── adventure │ │ │ └── play_level.html.slim │ │ ├── quest │ │ │ └── play_quest_level.slim │ │ ├── three.slim │ │ ├── haste.slim │ │ ├── openings.slim │ │ ├── speedrun.slim │ │ ├── countdown.slim │ │ ├── mate_in_one.slim │ │ ├── rook_endgames.slim │ │ └── infinity │ │ │ └── _recent_puzzle_item.slim │ ├── pages │ │ ├── not_found.html.erb │ │ ├── puzzle_player.slim │ │ ├── position.html.erb │ │ ├── endgame_studies.slim │ │ ├── mate_in_two.slim │ │ └── defined_position.html.erb │ ├── puzzle_player │ │ └── _above_board.slim │ ├── static │ │ ├── svgs │ │ │ ├── _resize_bottom_right.html │ │ │ ├── _sign_in.html │ │ │ ├── _chevron_right.html │ │ │ ├── _check.html │ │ │ ├── _x.html │ │ │ └── _volume_off.html │ │ ├── snippets │ │ │ ├── _google_analytics.html.erb │ │ │ ├── _miniboard_link.html.erb │ │ │ ├── _bugsnag.html.erb │ │ │ └── _miniboard.html.erb │ │ └── descriptions │ │ │ ├── _saavedra_position.html.erb │ │ │ ├── _lucena_position.slim │ │ │ ├── _vancura_position.html.erb │ │ │ └── _philidor_position.html.erb │ ├── puzzle_reports │ │ └── index.slim │ ├── users │ │ └── show.html.erb │ ├── puzzle_sets │ │ ├── new.slim │ │ └── index.slim │ └── devise │ │ └── passwords │ │ └── new.html.erb ├── javascript │ ├── game_modes │ │ ├── repetition │ │ │ ├── responsive.sass │ │ │ ├── views │ │ │ │ ├── background.ts │ │ │ │ ├── onboarding.ts │ │ │ │ ├── level_indicator.ts │ │ │ │ └── progress_bar.ts │ │ │ └── models │ │ │ │ └── level_status.ts │ │ ├── three │ │ │ └── responsive.sass │ │ ├── rated │ │ │ └── responsive.sass │ │ ├── haste │ │ │ └── responsive.sass │ │ ├── mate_in_one │ │ │ └── responsive.sass │ │ ├── openings │ │ │ └── responsive.sass │ │ ├── rook-endgames │ │ │ └── responsive.sass │ │ ├── countdown │ │ │ └── responsive.sass │ │ ├── infinity │ │ │ ├── index.ts │ │ │ └── responsive.sass │ │ └── speedrun │ │ │ └── responsive.sass │ ├── globals.d.ts │ ├── components │ │ ├── move_status │ │ │ └── style.sass │ │ ├── new_puzzle_player │ │ │ └── views │ │ │ │ └── instructions.ts │ │ ├── chessground_board │ │ │ └── svgs │ │ │ │ └── board_bg.svg │ │ └── mini_chessboard │ │ │ └── pieces.ts │ ├── pages │ │ ├── puzzle_index.ts │ │ ├── puzzle_list.ts │ │ ├── user_profile.ts │ │ ├── puzzle_set │ │ │ ├── responsive.sass │ │ │ └── index.ts │ │ └── puzzle_list │ │ │ └── style.sass │ ├── packs │ │ └── bugsnag.ts │ ├── local_storage.ts │ ├── types.ts │ └── api │ │ └── client.ts ├── models │ ├── lichess_v2_puzzles_puzzle_set.rb │ ├── user_data │ │ ├── puzzle_report.rb │ │ ├── completed_repetition_level.rb │ │ ├── user_haste_rounds.rb │ │ ├── user_three_rounds.rb │ │ ├── user_openings_rounds.rb │ │ ├── user_mate_in_one_rounds.rb │ │ ├── user_rook_endgames_rounds.rb │ │ ├── completed_countdown_level.rb │ │ ├── user_speedruns.rb │ │ ├── completed_repetition_round.rb │ │ ├── user_countdown_levels.rb │ │ ├── solved_infinity_puzzle.rb │ │ ├── user_repetition_levels.rb │ │ ├── rated_puzzle_attempt.rb │ │ ├── position.rb │ │ ├── completed_haste_round.rb │ │ ├── completed_three_round.rb │ │ ├── completed_openings_round.rb │ │ ├── completed_mate_in_one_round.rb │ │ ├── completed_rook_endgames_round.rb │ │ └── user_rating.rb │ ├── nil_user.rb │ ├── levels │ │ ├── speedrun_puzzle.rb │ │ ├── countdown_puzzle.rb │ │ ├── repetition_puzzle.rb │ │ └── infinity_puzzle.rb │ ├── puzzles │ │ └── puzzle_finder.rb │ ├── completed_quest_world_level.rb │ ├── importers │ │ └── haste_puzzle_loader.rb │ └── puzzle_set.rb └── controllers │ ├── solved_puzzles_controller.rb │ ├── user_settings_controller.rb │ ├── puzzle_reports_controller.rb │ └── users │ └── registrations_controller.rb ├── .browserslistrc ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .rspec ├── ansible ├── inventory.ini └── blitz-puma.service.j2 ├── bin ├── dev ├── rake ├── bundle ├── rails ├── yarn ├── spring └── update ├── public ├── demo.gif ├── icon.png ├── sounds │ └── sfx │ │ ├── Check.mp3 │ │ ├── Move.mp3 │ │ └── Capture.mp3 ├── engines │ └── stockfish.wasm ├── robots.txt └── icon.svg ├── Procfile.dev ├── lib └── tasks │ └── assets.rake ├── config ├── environment.rb ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── bugsnag.rb │ ├── cookies_serializer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── permissions_policy.rb │ ├── filter_parameter_logging.rb │ ├── wrap_parameters.rb │ ├── new_framework_defaults_5_1.rb │ └── inflections.rb ├── boot.rb └── secrets.yml ├── config.ru ├── db └── migrate │ ├── 20160313074604_add_profile_to_users.rb │ ├── 20180624063512_add_tagline_to_users.rb │ ├── 20160313063553_add_username_to_users.rb │ ├── 20160411003705_add_unique_index_to_user_emails.rb │ ├── 20250918195258_add_game_mode_to_solved_puzzles.rb │ ├── 20250821171005_add_order_to_quest_world.rb │ ├── 20250919032532_add_piece_set_to_user_chessboards.rb │ ├── 20181206064113_remove_unique_index_on_speedrun_puzzles.rb │ ├── 20250821171012_add_order_to_quest_world_level.rb │ ├── 20180623114045_create_speedrun_levels.rb │ ├── 20181206064149_add_unique_compound_index_on_speedrun_puzzles.rb │ ├── 20181207065925_create_countdown_levels.rb │ ├── 20250821042208_create_quest_worlds.rb │ ├── 20250118220000_add_solved_puzzles_count_to_users.rb │ ├── 20250821042209_create_quest_world_levels.rb │ ├── 20210417123508_create_join_table_puzzle_sets_lichess_v2_puzzles.rb │ ├── 20180620032206_create_infinity_levels.rb │ ├── 20160313213221_create_completed_rounds.rb │ ├── 20180626024019_create_completed_repetition_levels.rb │ ├── 20210417123507_create_puzzle_sets.rb │ ├── 20180625160707_create_repetition_levels.rb │ ├── 20160417190025_add_unique_index_on_case_insensitive_username.rb │ ├── 20241220000001_create_feature_flags.rb │ ├── 20180626024026_create_completed_repetition_rounds.rb │ ├── 20160313200604_create_level_attempts.rb │ ├── 20181211073138_create_completed_haste_rounds.rb │ ├── 20201207014332_create_completed_three_rounds.rb │ ├── 20201021035332_create_puzzle_reports.rb │ ├── 20250122000000_create_solved_puzzles.rb │ ├── 20181207070049_create_completed_countdown_levels.rb │ ├── 20161125221817_create_positions.rb │ ├── 20250118220001_populate_solved_puzzles_count.rb │ ├── 20250120000000_create_completed_openings_rounds.rb │ ├── 20250116000004_create_completed_mate_in_one_rounds.rb │ ├── 20160313074949_create_levels.rb │ ├── 20250918032709_create_completed_rook_endgames_rounds.rb │ ├── 20180625160722_create_repetition_puzzles.rb │ ├── 20180621081826_create_infinity_puzzles.rb │ ├── 20180623125613_create_speedrun_puzzles.rb │ ├── 20181211070754_create_haste_puzzles.rb │ ├── 20250821044449_add_puzzle_ids_and_success_criteria_to_quest_world_levels.rb │ ├── 20180623113358_create_completed_speedruns.rb │ ├── 20201002022940_create_puzzles.rb │ ├── 20180620084825_create_solved_infinity_puzzles.rb │ ├── 20181207070038_create_countdown_puzzles.rb │ ├── 20190220052623_create_user_chessboard.rb │ ├── 20250821043552_create_completed_quest_worlds.rb │ ├── 20250821043546_create_completed_quest_world_levels.rb │ ├── 20250123000000_add_created_at_index_to_solved_puzzles.rb │ ├── 20190221152109_create_user_ratings.rb │ ├── 20201229001317_create_lichess_v2_puzzles.rb │ ├── 20190221152057_create_rated_puzzles.rb │ └── 20250821173256_rename_order_to_number_in_quest_tables.rb ├── .env.sample ├── Rakefile ├── postcss.config.js ├── spec └── capybara_helper.rb ├── tsconfig.json ├── Gemfile └── .gitignore /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.1 2 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.4.5 2 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /ansible/inventory.ini: -------------------------------------------------------------------------------- 1 | [app] 2 | blitztactics.com ansible_user=root 3 | -------------------------------------------------------------------------------- /app/views/puzzles/not_found.slim: -------------------------------------------------------------------------------- 1 | .container 2 | | Puzzle not found! 3 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /app/views/puzzles/edit.slim: -------------------------------------------------------------------------------- 1 | h3 2 | | Editing puzzle #{@puzzle.puzzle_id} 3 | -------------------------------------------------------------------------------- /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/demo.gif -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/icon.png -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/not_found.sass: -------------------------------------------------------------------------------- 1 | .not-found 2 | text-align: center 3 | margin-top: 40px 4 | -------------------------------------------------------------------------------- /public/sounds/sfx/Check.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/sounds/sfx/Check.mp3 -------------------------------------------------------------------------------- /public/sounds/sfx/Move.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/sounds/sfx/Move.mp3 -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /public/engines/stockfish.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/engines/stockfish.wasm -------------------------------------------------------------------------------- /public/sounds/sfx/Capture.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linrock/blitz-tactics/HEAD/public/sounds/sfx/Capture.mp3 -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: env PORT=3000 RUBY_DEBUG_OPEN=true bin/rails server 2 | js: NODE_ENV=development yarn build:watch 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../builds 4 | -------------------------------------------------------------------------------- /app/views/shared/_main_footer.slim: -------------------------------------------------------------------------------- 1 | .main-footer 2 | .container 3 | a(href="/scoreboard") Scoreboard 4 | a(href="/about") About 5 | -------------------------------------------------------------------------------- /app/views/game_modes/rated/puzzle_attempt.slim: -------------------------------------------------------------------------------- 1 | section.puzzle-attempts 2 | .container 3 | | This is puzzle attempt #{@puzzle_attempt.id} 4 | -------------------------------------------------------------------------------- /app/views/pages/not_found.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | That page doesn't exist 4 |
5 |
6 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/tasks/assets.rake: -------------------------------------------------------------------------------- 1 | # Ensure JavaScript assets are built before Rails asset precompilation 2 | Rake::Task["assets:precompile"].enhance(["javascript:build"]) 3 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .repetition-mode 3 | .sidebar 4 | display: none !important 5 | -------------------------------------------------------------------------------- /app/models/lichess_v2_puzzles_puzzle_set.rb: -------------------------------------------------------------------------------- 1 | class LichessV2PuzzlesPuzzleSet < ActiveRecord::Base 2 | belongs_to :lichess_v2_puzzle 3 | belongs_to :puzzle_set 4 | end -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/b.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/puzzle_report.rb: -------------------------------------------------------------------------------- 1 | # For reporting bad puzzles 2 | 3 | class PuzzleReport < ActiveRecord::Base 4 | belongs_to :puzzle 5 | belongs_to :user 6 | end 7 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/puzzle_attempts.sass: -------------------------------------------------------------------------------- 1 | section.puzzle-attempts 2 | h2 3 | margin: 20px 0 40px 4 | 5 | .puzzle-attempt 6 | float: left 7 | margin: 20px 8 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/w.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/disguised/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20160313074604_add_profile_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddProfileToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :profile, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180624063512_add_tagline_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTaglineToUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :users, :tagline, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_blitz-tactics_session' 4 | -------------------------------------------------------------------------------- /app/javascript/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /app/javascript/components/move_status/style.sass: -------------------------------------------------------------------------------- 1 | // Styles have been moved to app/javascript/game_modes/base.sass 2 | // This ensures they are properly loaded for all game modes in the above-board area 3 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20160313063553_add_username_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddUsernameToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :username, :string, :unique => true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/pages/puzzle_index.ts: -------------------------------------------------------------------------------- 1 | import { SolutionPlayer } from '@blitz/pages/infinity' 2 | 3 | // Initialize solution player for puzzle index pages 4 | export default () => { 5 | new SolutionPlayer() 6 | } -------------------------------------------------------------------------------- /app/javascript/pages/puzzle_list.ts: -------------------------------------------------------------------------------- 1 | import { SolutionPlayer } from '@blitz/pages/infinity' 2 | 3 | // Initialize solution player for puzzle index pages 4 | export default () => { 5 | new SolutionPlayer() 6 | } -------------------------------------------------------------------------------- /db/migrate/20160411003705_add_unique_index_to_user_emails.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexToUserEmails < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index :users, :email, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/packs/bugsnag.ts: -------------------------------------------------------------------------------- 1 | import Bugsnag from '@bugsnag/js' 2 | 3 | const bugsnagOptions = JSON.parse( 4 | document.querySelector("#bugsnag-options-json").innerHTML 5 | ) 6 | Bugsnag.start(bugsnagOptions); 7 | -------------------------------------------------------------------------------- /db/migrate/20250918195258_add_game_mode_to_solved_puzzles.rb: -------------------------------------------------------------------------------- 1 | class AddGameModeToSolvedPuzzles < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :solved_puzzles, :game_mode, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | RAILS_ENV=development 2 | 3 | # puma config 4 | WEB_CONCURRENCY=3 5 | 6 | # misc config 7 | SECRET_KEY_BASE= 8 | 9 | # 3rd-party services 10 | GA_TRACKING_ID= 11 | BUGSNAG_KEY= 12 | MAILGUN_API_KEY= 13 | -------------------------------------------------------------------------------- /app/javascript/pages/user_profile.ts: -------------------------------------------------------------------------------- 1 | import { SolutionPlayer } from '../pages/infinity' 2 | 3 | export default function UserProfile() { 4 | // Initialize solution player for recent puzzles 5 | new SolutionPlayer() 6 | } 7 | -------------------------------------------------------------------------------- /app/models/nil_user.rb: -------------------------------------------------------------------------------- 1 | # anonymous users 2 | 3 | class NilUser 4 | include UserDelegates 5 | 6 | def user_rating 7 | UserRating.new 8 | end 9 | 10 | def present? 11 | false 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250821171005_add_order_to_quest_world.rb: -------------------------------------------------------------------------------- 1 | class AddOrderToQuestWorld < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :quest_worlds, :order, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250919032532_add_piece_set_to_user_chessboards.rb: -------------------------------------------------------------------------------- 1 | class AddPieceSetToUserChessboards < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :user_chessboards, :piece_set, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181206064113_remove_unique_index_on_speedrun_puzzles.rb: -------------------------------------------------------------------------------- 1 | class RemoveUniqueIndexOnSpeedrunPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_index :speedrun_puzzles, :puzzle_hash 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /app/assets/images/pieces/letter/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /db/migrate/20250821171012_add_order_to_quest_world_level.rb: -------------------------------------------------------------------------------- 1 | class AddOrderToQuestWorldLevel < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :quest_world_levels, :order, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/bugsnag.rb: -------------------------------------------------------------------------------- 1 | Bugsnag.configure do |config| 2 | config.logger = Logger.new(STDOUT) 3 | config.logger.level = Logger::ERROR 4 | 5 | if ENV["BUGSNAG_KEY"].present? 6 | config.api_key = ENV["BUGSNAG_KEY"] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/assets/images/pieces/letter/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/pages/puzzle_player.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "New puzzle player" } 2 | 3 | .vue-app-mount 4 | 5 | script 6 | | 7 | document.addEventListener("DOMContentLoaded", () => { 8 | blitz.puzzles = #{@puzzles.to_json.html_safe}; 9 | }); 10 | -------------------------------------------------------------------------------- /app/models/user_data/completed_repetition_level.rb: -------------------------------------------------------------------------------- 1 | # tracks the repetition levels that a player has completed 2 | # to determine the next level 3 | 4 | class CompletedRepetitionLevel < ActiveRecord::Base 5 | belongs_to :user 6 | belongs_to :repetition_level 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180623114045_create_speedrun_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateSpeedrunLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :speedrun_levels do |t| 4 | t.string :name, null: false 5 | t.timestamps null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20181206064149_add_unique_compound_index_on_speedrun_puzzles.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueCompoundIndexOnSpeedrunPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | add_index :speedrun_puzzles, [:speedrun_level_id, :puzzle_hash], unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /db/migrate/20181207065925_create_countdown_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateCountdownLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :countdown_levels do |t| 4 | t.string :name, null: false 5 | t.timestamps null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/levels/speedrun_puzzle.rb: -------------------------------------------------------------------------------- 1 | # a puzzle for a speedrun level 2 | 3 | class SpeedrunPuzzle < ActiveRecord::Base 4 | include PuzzleRecord 5 | 6 | belongs_to :speedrun_level, touch: true 7 | 8 | validates :puzzle_hash, uniqueness: { scope: :speedrun_level_id } 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250821042208_create_quest_worlds.rb: -------------------------------------------------------------------------------- 1 | class CreateQuestWorlds < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :quest_worlds do |t| 4 | t.string :description 5 | t.string :background 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/levels/countdown_puzzle.rb: -------------------------------------------------------------------------------- 1 | # a puzzle for a countdown level 2 | 3 | class CountdownPuzzle < ActiveRecord::Base 4 | include PuzzleRecord 5 | 6 | belongs_to :countdown_level, touch: true 7 | 8 | validates :puzzle_hash, uniqueness: { scope: :countdown_level_id } 9 | end 10 | -------------------------------------------------------------------------------- /app/views/puzzle_player/_above_board.slim: -------------------------------------------------------------------------------- 1 | .above-board 2 | .instructions.invisible White to move 3 | .puzzle-hint.invisible 4 | .move 5 | .hint-trigger Show hint 6 | .move-status   7 | .combo-counter.invisible 8 | span.counter   9 | span.combo-text combo 10 | -------------------------------------------------------------------------------- /db/migrate/20250118220000_add_solved_puzzles_count_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSolvedPuzzlesCountToUsers < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :users, :solved_puzzles_count, :integer, default: 0, null: false 4 | add_index :users, :solved_puzzles_count 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/views/static/svgs/_resize_bottom_right.html: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/models/levels/repetition_puzzle.rb: -------------------------------------------------------------------------------- 1 | # a puzzle for a repetition level 2 | 3 | class RepetitionPuzzle < ActiveRecord::Base 4 | include PuzzleRecord 5 | 6 | belongs_to :repetition_level 7 | 8 | default_scope { order('id ASC') } 9 | 10 | validates :puzzle_hash, uniqueness: true 11 | end 12 | -------------------------------------------------------------------------------- /app/models/levels/infinity_puzzle.rb: -------------------------------------------------------------------------------- 1 | # a puzzle for an infinity level 2 | 3 | class InfinityPuzzle < ActiveRecord::Base 4 | include PuzzleRecord 5 | 6 | belongs_to :infinity_level 7 | has_many :solved_infinity_puzzles, dependent: :destroy 8 | 9 | validates :puzzle_hash, uniqueness: true 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/R.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20250821042209_create_quest_world_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateQuestWorldLevels < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :quest_world_levels do |t| 4 | t.references :quest_world, null: false, foreign_key: true 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20210417123508_create_join_table_puzzle_sets_lichess_v2_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateJoinTablePuzzleSetsLichessV2Puzzles < ActiveRecord::Migration[6.1] 2 | def change 3 | create_join_table :puzzle_sets, :lichess_v2_puzzles do |t| 4 | t.index :puzzle_set_id 5 | t.index :lichess_v2_puzzle_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /app/views/game_modes/speedrun/puzzles.slim: -------------------------------------------------------------------------------- 1 | .puzzle-page.container 2 | h2 Today's speedrun puzzles 3 | - if @speedrun_theme 4 | h3 Theme: #{titleize_theme(@speedrun_theme)} 5 | 6 | .puzzles 7 | - @puzzles.each_with_index do |puzzle, i| 8 | div 9 | = linked_puzzle_miniboard(puzzle) 10 | .puzzle-num= i + 1 11 | -------------------------------------------------------------------------------- /db/migrate/20180620032206_create_infinity_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateInfinityLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :infinity_levels do |t| 4 | t.string :difficulty, null: false 5 | t.timestamps null: false 6 | end 7 | add_index :infinity_levels, :difficulty, unique: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/puzzle_reports.sass: -------------------------------------------------------------------------------- 1 | .puzzle-reports-list 2 | padding-top: 30px 3 | 4 | .puzzle-report-summary 5 | margin-bottom: 20px 6 | 7 | .puzzle-link 8 | color: inherit 9 | opacity: 0.8 10 | 11 | .puzzle-report-message 12 | padding: 5px 0 13 | 14 | .puzzle-report-timestamp 15 | font-size: 14px 16 | opacity: 0.6 17 | -------------------------------------------------------------------------------- /db/migrate/20160313213221_create_completed_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedRounds < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :completed_rounds do |t| 4 | t.integer :level_attempt_id, :null => false 5 | t.integer :time_elapsed 6 | t.integer :errors_count 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20180626024019_create_completed_repetition_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedRepetitionLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :completed_repetition_levels do |t| 4 | t.integer :user_id, null: false 5 | t.integer :repetition_level_id, null: false 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20210417123507_create_puzzle_sets.rb: -------------------------------------------------------------------------------- 1 | class CreatePuzzleSets < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :puzzle_sets do |t| 4 | t.integer :user_id, null: false 5 | t.string :name, null: false 6 | t.text :description 7 | t.timestamps 8 | end 9 | add_index :puzzle_sets, :user_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/user_data/user_haste_rounds.rb: -------------------------------------------------------------------------------- 1 | # for presenting haste info for users and nil users 2 | 3 | class UserHasteRounds 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_haste_score(date) 10 | return 'None' if !@user.present? 11 | @user.completed_haste_rounds.personal_best(date) or 'None' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/game_modes/rated/puzzle_attempts_list.slim: -------------------------------------------------------------------------------- 1 | section.puzzle-attempts 2 | .container 3 | h2 Your recent puzzle attempts 4 | 5 | - @puzzle_attempts.each do |attempt| 6 | .puzzle-attempt 7 | ruby: 8 | puzzle = attempt.rated_puzzle 9 | = homepage_miniboard_link "/rated/attempts/#{attempt.id}", puzzle 10 | = attempt.outcome 11 | -------------------------------------------------------------------------------- /app/models/user_data/user_three_rounds.rb: -------------------------------------------------------------------------------- 1 | # for presenting three rounds info for users and nil users 2 | 3 | class UserThreeRounds 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_three_score(date) 10 | return 'None' unless @user.present? 11 | @user.completed_three_rounds.personal_best(date) or 'None' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/views/background.ts: -------------------------------------------------------------------------------- 1 | import { subscribe, GameEvent } from '@blitz/events' 2 | 3 | export default class Background { 4 | 5 | get el(): HTMLElement { 6 | return document.querySelector(`body`) 7 | } 8 | 9 | constructor() { 10 | subscribe({ 11 | 'level:unlocked': () => this.el.classList.add(`unlocked`) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/models/user_data/user_openings_rounds.rb: -------------------------------------------------------------------------------- 1 | # for presenting openings info for users and nil users 2 | 3 | class UserOpeningsRounds 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_openings_score(date) 10 | return 'None' if !@user.present? 11 | @user.completed_openings_rounds.personal_best(date) or 'None' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/game_modes/rated/puzzles.slim: -------------------------------------------------------------------------------- 1 | .puzzle-page.container 2 | - if current_user 3 | h2 Rated puzzles you've solved recently 4 | 5 | .puzzles 6 | - @puzzles.each_with_index do |puzzle, i| 7 | div 8 | = linked_puzzle_miniboard(puzzle) 9 | .puzzle-num= i + 1 10 | - else 11 | div Log in or sign up to view the recent puzzles you've seen! 12 | -------------------------------------------------------------------------------- /db/migrate/20180625160707_create_repetition_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateRepetitionLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :repetition_levels do |t| 4 | t.integer :number, null: false 5 | t.string :name 6 | t.timestamps null: false 7 | end 8 | add_index :repetition_levels, :number, unique: true, order: { number: :asc } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/user_data/user_mate_in_one_rounds.rb: -------------------------------------------------------------------------------- 1 | # for presenting mate-in-one info for users and nil users 2 | 3 | class UserMateInOneRounds 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_mate_in_one_score(date) 10 | return 'None' if !@user.present? 11 | @user.completed_mate_in_one_rounds.personal_best(date) or 'None' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user_data/user_rook_endgames_rounds.rb: -------------------------------------------------------------------------------- 1 | # for presenting rook endgames info for users and nil users 2 | 3 | class UserRookEndgamesRounds 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_rook_endgames_score(date) 10 | return 'None' if !@user.present? 11 | @user.completed_rook_endgames_rounds.personal_best(date) or 'None' 12 | end 13 | end -------------------------------------------------------------------------------- /app/views/pages/position.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:title) { "Position trainer" } %> 2 | 3 |
4 |
5 |
6 | 7 | 15 | -------------------------------------------------------------------------------- /db/migrate/20160417190025_add_unique_index_on_case_insensitive_username.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexOnCaseInsensitiveUsername < ActiveRecord::Migration[4.2] 2 | def up 3 | execute "CREATE UNIQUE INDEX index_users_on_lowercase_username 4 | ON users USING btree (LOWER(username));" 5 | end 6 | 7 | def down 8 | execute "DROP INDEX index_users_on_lowercase_username;" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/user_data/completed_countdown_level.rb: -------------------------------------------------------------------------------- 1 | # record of the countdown levels that a player has completed 2 | 3 | class CompletedCountdownLevel < ActiveRecord::Base 4 | belongs_to :user 5 | belongs_to :countdown_level 6 | 7 | def self.personal_best(countdown_level_id) 8 | where(countdown_level_id: countdown_level_id) 9 | .order(score: :desc) 10 | .first&.score 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20241220000001_create_feature_flags.rb: -------------------------------------------------------------------------------- 1 | class CreateFeatureFlags < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :feature_flags do |t| 4 | t.string :name, null: false 5 | t.boolean :enabled, default: false, null: false 6 | t.text :description 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :feature_flags, :name, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/static/snippets/_google_analytics.html.erb: -------------------------------------------------------------------------------- 1 | <% if ENV["GA_ID"].present? %> 2 | 3 | 10 | <% end %> 11 | -------------------------------------------------------------------------------- /db/migrate/20180626024026_create_completed_repetition_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedRepetitionRounds < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :completed_repetition_rounds do |t| 4 | t.integer :user_id, null: false 5 | t.integer :repetition_level_id, null: false 6 | t.integer :elapsed_time_ms, null: false 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/P.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/pages/endgame_studies.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Endgame studies" } 2 | 3 | .positions-index.container 4 | h2 Endgame studies 5 | .positions 6 | .description 7 | | Here are some endgame studies to help improve your endgame! 8 | 9 | .examples 10 | - open(Rails.root.join("db/endgame-fens.txt"), "r").read.split(/\n/).each_with_index do |fen, i| 11 | = linked_miniboard "Study #{i+1}", fen, "win" 12 | -------------------------------------------------------------------------------- /db/migrate/20160313200604_create_level_attempts.rb: -------------------------------------------------------------------------------- 1 | class CreateLevelAttempts < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :level_attempts do |t| 4 | t.integer :user_id, :null => false 5 | t.integer :level_id, :null => false 6 | t.datetime :last_attempt_at 7 | t.timestamps 8 | end 9 | add_index :level_attempts, :user_id 10 | add_index :level_attempts, :level_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20181211073138_create_completed_haste_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedHasteRounds < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :completed_haste_rounds do |t| 4 | t.integer :user_id, null: false 5 | t.integer :score, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :completed_haste_rounds, :user_id 9 | add_index :completed_haste_rounds, :created_at 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20201207014332_create_completed_three_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedThreeRounds < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :completed_three_rounds do |t| 4 | t.integer :user_id, null: false 5 | t.integer :score, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :completed_three_rounds, :user_id 9 | add_index :completed_three_rounds, :created_at 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/pages/mate_in_two.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Mate-in-2 puzzles" } 2 | 3 | .positions-index.container 4 | h2 Mate-in-2 puzzles 5 | .positions 6 | .description These puzzles were created by chess composer W J Baird over 100 years ago! 7 | 8 | .examples 9 | - open(Rails.root.join("db/wj-baird-checkmates.txt"), "r").read.split(/\n/).each_with_index do |fen, i| 10 | = linked_miniboard "Study #{i+1}", fen, "win" 11 | -------------------------------------------------------------------------------- /db/migrate/20201021035332_create_puzzle_reports.rb: -------------------------------------------------------------------------------- 1 | class CreatePuzzleReports < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :puzzle_reports do |t| 4 | t.integer :puzzle_id, null: false 5 | t.integer :user_id, null: false 6 | t.string :message, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :puzzle_reports, :puzzle_id 10 | add_index :puzzle_reports, :user_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20250122000000_create_solved_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateSolvedPuzzles < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :solved_puzzles do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :puzzle_id, null: false 6 | t.timestamps 7 | end 8 | 9 | add_index :solved_puzzles, [:user_id, :puzzle_id], unique: true 10 | add_index :solved_puzzles, :puzzle_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/K.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static/snippets/_miniboard_link.html.erb: -------------------------------------------------------------------------------- 1 | <% path = local_assigns[:path] %> 2 | <% title = local_assigns[:title] %> 3 | 4 | 15 | -------------------------------------------------------------------------------- /app/models/user_data/user_speedruns.rb: -------------------------------------------------------------------------------- 1 | # for presenting speedrun info for users and nil users 2 | 3 | class UserSpeedruns 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_speedrun_time(speedrun_level) 10 | if @user.present? 11 | @user.completed_speedruns 12 | .where(speedrun_level_id: speedrun_level.id).formatted_fastest_time 13 | else 14 | 'None' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/20181207070049_create_completed_countdown_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedCountdownLevels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :completed_countdown_levels do |t| 4 | t.integer :user_id, null: false 5 | t.integer :countdown_level_id, null: false 6 | t.integer :score, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :completed_countdown_levels, :user_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /db/migrate/20161125221817_create_positions.rb: -------------------------------------------------------------------------------- 1 | class CreatePositions < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :positions do |t| 4 | t.integer :user_id 5 | t.string :fen, :null => false 6 | t.string :goal 7 | t.string :name 8 | t.text :description 9 | t.jsonb :configuration, :null => false, :default => {} 10 | t.timestamps 11 | end 12 | add_index :positions, :user_id 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/N.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/solved_puzzles_controller.rb: -------------------------------------------------------------------------------- 1 | class SolvedPuzzlesController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def create 5 | SolvedPuzzle.track_solve(current_user.id, params[:puzzle_id], params[:game_mode]) 6 | render json: { status: 'success' } 7 | rescue => e 8 | Rails.logger.error "Failed to track solved puzzle: #{e.message}" 9 | render json: { status: 'error', message: e.message }, status: 422 10 | end 11 | end -------------------------------------------------------------------------------- /app/javascript/local_storage.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/marcuswestin/store.js 2 | 3 | import engine from 'store/src/store-engine' 4 | import localStorage from 'store/storages/localStorage' 5 | import cookieStorage from 'store/storages/cookieStorage' 6 | import expire from 'store/plugins/expire' 7 | 8 | const storages = [localStorage, cookieStorage] 9 | const plugins = [expire] 10 | 11 | const store = engine.createStore(storages, plugins) 12 | 13 | export default store 14 | -------------------------------------------------------------------------------- /app/controllers/user_settings_controller.rb: -------------------------------------------------------------------------------- 1 | # sound on/off 2 | 3 | class UserSettingsController < ApplicationController 4 | 5 | def update 6 | if user_signed_in? 7 | current_user.set_sound_enabled settings_params[:sound_enabled] 8 | else 9 | session[:sound_enabled] = settings_params[:sound_enabled] 10 | end 11 | end 12 | 13 | private 14 | 15 | def settings_params 16 | params.require(:settings).permit(:sound_enabled) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/user_data/completed_repetition_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the time a user takes to make it through all repetition puzzles in a round 2 | 3 | class CompletedRepetitionRound < ActiveRecord::Base 4 | belongs_to :user 5 | belongs_to :repetition_level 6 | 7 | validates :elapsed_time_ms, presence: true, numericality: { greater_than: 1_000 } 8 | 9 | def formatted_time_spent 10 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/javascript/game_modes/three/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .three-mode 3 | .three-under-board 4 | .recent-high-scores 5 | display: none !important 6 | 7 | .timers, .three-complete 8 | flex-direction: row !important 9 | justify-content: space-around !important 10 | 11 | @media (max-width: 500px) 12 | .three-mode 13 | .three-under-board 14 | .current-score 15 | .score 16 | font-size: 24px -------------------------------------------------------------------------------- /db/migrate/20250118220001_populate_solved_puzzles_count.rb: -------------------------------------------------------------------------------- 1 | class PopulateSolvedPuzzlesCount < ActiveRecord::Migration[7.1] 2 | def up 3 | # Manually populate the counter cache for all users 4 | User.find_each do |user| 5 | count = user.solved_puzzles.count 6 | user.update_column(:solved_puzzles_count, count) 7 | end 8 | end 9 | 10 | def down 11 | # Set all counts back to 0 12 | User.update_all(solved_puzzles_count: 0) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/B.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20250120000000_create_completed_openings_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedOpeningsRounds < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :completed_openings_rounds do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.integer :score, null: false 6 | t.integer :elapsed_time_ms, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :completed_openings_rounds, [:user_id, :created_at] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user_data/user_countdown_levels.rb: -------------------------------------------------------------------------------- 1 | # for presenting countdown info for users and nil users 2 | 3 | class UserCountdownLevels 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def best_countdown_score(countdown_level) 10 | return 'None' if !@user.present? or !countdown_level 11 | @user.completed_countdown_levels 12 | .where(countdown_level_id: countdown_level.id) 13 | .maximum(:score) or 'None' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/javascript/game_modes/rated/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .rated-mode 3 | .container 4 | display: block !important 5 | 6 | .rated-complete 7 | width: 100% !important 8 | 9 | .rated-under-board 10 | margin-top: 0 !important 11 | margin-left: 0 !important 12 | padding: 20px 13 | 14 | .timers, .rated-complete 15 | flex-direction: row !important 16 | 17 | .make-a-move 18 | width: 100% !important 19 | -------------------------------------------------------------------------------- /db/migrate/20250116000004_create_completed_mate_in_one_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedMateInOneRounds < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :completed_mate_in_one_rounds do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.integer :score, null: false 6 | t.integer :elapsed_time_ms, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :completed_mate_in_one_rounds, [:user_id, :created_at] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user_data/solved_infinity_puzzle.rb: -------------------------------------------------------------------------------- 1 | # tracks the puzzles that a user has solved 2 | 3 | class SolvedInfinityPuzzle < ActiveRecord::Base 4 | belongs_to :infinity_puzzle 5 | belongs_to :user 6 | 7 | validates :difficulty, inclusion: InfinityLevel::DIFFICULTIES 8 | 9 | scope :most_recent_last, -> do # last = latest solved 10 | order(updated_at: :asc) 11 | end 12 | 13 | scope :with_difficulty, -> (difficulty) do 14 | where(difficulty: difficulty) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/views/static/svgs/_sign_in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /db/migrate/20160313074949_create_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateLevels < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :levels do |t| 4 | t.string :slug 5 | t.string :name 6 | t.integer :next_level_id 7 | t.string :secret_key 8 | t.integer :puzzle_ids, :array => true 9 | t.jsonb :options 10 | t.timestamps 11 | end 12 | add_index :levels, :slug, :unique => true 13 | add_index :levels, :secret_key, :unique => true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20250918032709_create_completed_rook_endgames_rounds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedRookEndgamesRounds < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :completed_rook_endgames_rounds do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.integer :score, null: false 6 | t.integer :elapsed_time_ms, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :completed_rook_endgames_rounds, [:user_id, :created_at] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/views/onboarding.ts: -------------------------------------------------------------------------------- 1 | // onboarding message on first level of repetition mode 2 | 3 | import { subscribe, GameEvent } from '@blitz/events' 4 | 5 | export default class Onboarding { 6 | 7 | get el() { 8 | return document.querySelector(`.onboarding`) 9 | } 10 | 11 | constructor() { 12 | if (!this.el) { 13 | return 14 | } 15 | subscribe({ 16 | [GameEvent.PUZZLES_START]: () => this.el.classList.add(`invisible`) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/views/static/descriptions/_saavedra_position.html.erb: -------------------------------------------------------------------------------- 1 |

Saavedra position

2 | 3 |
4 | White to play wins with an underpromotion to a rook, avoiding stalemate traps. 5 |

6 | Try to escort the pawn to promotion while avoiding perpetual checks and stalemate ideas. 7 | 8 |
9 | resources: 10 | 11 | wikipedia 12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /db/migrate/20180625160722_create_repetition_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateRepetitionPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :repetition_puzzles do |t| 4 | t.integer :repetition_level_id, null: false 5 | t.jsonb :data, null: false 6 | t.string :puzzle_hash, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :repetition_puzzles, :repetition_level_id 10 | add_index :repetition_puzzles, :puzzle_hash, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180621081826_create_infinity_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateInfinityPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :infinity_puzzles do |t| 4 | t.integer :infinity_level_id, null: false 5 | t.jsonb :data, null: false 6 | t.string :puzzle_hash, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :infinity_puzzles, :infinity_level_id, order: { id: :asc } 10 | add_index :infinity_puzzles, :puzzle_hash, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180623125613_create_speedrun_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateSpeedrunPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :speedrun_puzzles do |t| 4 | t.integer :speedrun_level_id, null: false 5 | t.jsonb :data, null: false 6 | t.string :puzzle_hash, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :speedrun_puzzles, :speedrun_level_id, order: { id: :asc } 10 | add_index :speedrun_puzzles, :puzzle_hash, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20181211070754_create_haste_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateHastePuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :haste_puzzles do |t| 4 | t.jsonb :data, null: false 5 | t.integer :difficulty, null: false 6 | t.string :color, null: false 7 | t.string :puzzle_hash, null: false 8 | t.timestamps null: false 9 | end 10 | add_index :haste_puzzles, :puzzle_hash, unique: true 11 | add_index :haste_puzzles, [:difficulty, :color] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250821044449_add_puzzle_ids_and_success_criteria_to_quest_world_levels.rb: -------------------------------------------------------------------------------- 1 | class AddPuzzleIdsAndSuccessCriteriaToQuestWorldLevels < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :quest_world_levels, :puzzle_ids, :string, array: true, default: [] 4 | add_column :quest_world_levels, :success_criteria, :jsonb, default: {} 5 | 6 | add_index :quest_world_levels, :puzzle_ids, using: 'gin' 7 | add_index :quest_world_levels, :success_criteria, using: 'gin' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180623113358_create_completed_speedruns.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedSpeedruns < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :completed_speedruns do |t| 4 | t.integer :user_id, null: false 5 | t.integer :speedrun_level_id, null: false 6 | t.integer :elapsed_time_ms, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :completed_speedruns, 10 | [:user_id, :speedrun_level_id], 11 | order: { elapsed_time_ms: :asc } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user_data/user_repetition_levels.rb: -------------------------------------------------------------------------------- 1 | # for presenting repetition level info for users and nil users 2 | 3 | class UserRepetitionLevels 4 | 5 | def initialize(user) 6 | @user = user # User or NilUser 7 | end 8 | 9 | def highest_repetition_level_unlocked 10 | if @user.present? 11 | level_number = @user.highest_repetition_level_number_completed + 1 12 | else 13 | level_number = 1 14 | end 15 | RepetitionLevel.number(level_number) || RepetitionLevel.last 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/static/descriptions/_lucena_position.slim: -------------------------------------------------------------------------------- 1 | h2 Lucena position 2 | 3 | .description 4 | | 5 | With correct play, most rook and pawn endgames end up in 6 | either the Lucena or Philidor positions. 7 | br 8 | br 9 | | Try to build a bridge with your rook and play 10 | for the win. Black will try to reach the Philidor 11 | position to force a draw. 12 | 13 | .source 14 | | resources: 15 | a<(href="https://en.wikipedia.org/wiki/Lucena_position" target="_blank") 16 | | wikipedia 17 | -------------------------------------------------------------------------------- /app/views/static/snippets/_bugsnag.html.erb: -------------------------------------------------------------------------------- 1 | <% if ENV["BUGSNAG_KEY"].present? %> 2 | 13 | <%= javascript_include_tag 'bugsnag', async: true, type: 'module' %> 14 | <% end %> 15 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /db/migrate/20201002022940_create_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreatePuzzles < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :puzzles do |t| 4 | t.string :puzzle_id, null: false 5 | t.jsonb :puzzle_data, default: {}, null: false 6 | t.jsonb :metadata, default: {}, null: false 7 | t.text :notes 8 | t.string :puzzle_data_hash, null: false 9 | t.timestamps 10 | end 11 | add_index :puzzles, :puzzle_id, unique: true 12 | add_index :puzzles, :puzzle_data_hash 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/game_modes/rated.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Rated mode" } 2 | 3 | section.game-mode.rated-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .vue-app-mount 12 | 13 | script#rated-mode-data(type="application/json") 14 | | { "playerRating": #{@user_rating.rating.round}, "numPuzzlesSeen": #{@user_rating.rated_puzzle_attempts_count} } 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/static/svgs/_chevron_right.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/puzzle_reports/index.slim: -------------------------------------------------------------------------------- 1 | .puzzle-reports-list.container 2 | - @puzzle_reports.each do |puzzle_report| 3 | .puzzle-report-summary 4 | a.puzzle-link(href="/p/#{puzzle_report.puzzle_id}") 5 | | Puzzle #{puzzle_report.puzzle_id} 6 | .puzzle-report-message 7 | span= puzzle_report.user.username 8 | span> 9 | | : 10 | span.puzzle-report-message= puzzle_report.message 11 | .puzzle-report-timestamp 12 | = time_ago_in_words(puzzle_report.updated_at) 13 | span< ago 14 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static/svgs/_check.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/migrate/20180620084825_create_solved_infinity_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateSolvedInfinityPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :solved_infinity_puzzles do |t| 4 | t.integer :user_id, null: false 5 | t.integer :infinity_puzzle_id, null: false 6 | t.string :difficulty, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :solved_infinity_puzzles, [:user_id, :infinity_puzzle_id], unique: true 10 | add_index :solved_infinity_puzzles, [:user_id, :updated_at] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20181207070038_create_countdown_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateCountdownPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :countdown_puzzles do |t| 4 | t.integer :countdown_level_id, null: false 5 | t.jsonb :data, null: false 6 | t.string :puzzle_hash, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :countdown_puzzles, :countdown_level_id, order: { id: :asc } 10 | add_index :countdown_puzzles, 11 | [:countdown_level_id, :puzzle_hash], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190220052623_create_user_chessboard.rb: -------------------------------------------------------------------------------- 1 | class CreateUserChessboard < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :user_chessboards do |t| 4 | t.integer :user_id, null: false 5 | t.string :light_square_color 6 | t.string :dark_square_color 7 | t.string :selected_square_color 8 | t.string :opponent_from_square_color 9 | t.string :opponent_to_square_color 10 | t.timestamps null: false 11 | end 12 | add_index :user_chessboards, :user_id, unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/pages/puzzle_set/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .haste-mode 3 | .container 4 | display: block !important 5 | 6 | .haste-complete 7 | width: 100% !important 8 | 9 | .haste-sidebar 10 | margin-top: 0 !important 11 | margin-left: 0 !important 12 | padding: 20px 13 | 14 | .recent-high-scores 15 | display: none !important 16 | 17 | .timers, .haste-complete 18 | flex-direction: row !important 19 | 20 | .make-a-move 21 | width: 100% !important 22 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/game_modes/adventure/play_level.html.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Adventure mode" } 2 | 3 | section.game-mode.adventure-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible 12 | .vue-app-mount 13 | 14 | #puzzle-player( 15 | data-puzzles=@puzzle_data.to_json 16 | data-level-info=@puzzle_data[:level_info].to_json 17 | ) 18 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/javascript/game_modes/haste/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .haste-mode 3 | .haste-under-board 4 | .timers 5 | padding: 15px 0 6 | 7 | .timer 8 | font-size: 36px 9 | 10 | .n-solved 11 | font-size: 16px 12 | 13 | .haste-complete 14 | padding: 15px 0 15 | 16 | .score-section 17 | flex-direction: column 18 | gap: 20px 19 | margin-bottom: 20px 20 | 21 | .recent-high-scores 22 | margin-bottom: 20px 23 | 24 | .list 25 | gap: 6px 26 | -------------------------------------------------------------------------------- /db/migrate/20250821043552_create_completed_quest_worlds.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedQuestWorlds < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :completed_quest_worlds do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.references :quest_world, null: false, foreign_key: true 6 | t.datetime :completed_at 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :completed_quest_worlds, [:user_id, :quest_world_id], 12 | unique: true, name: 'index_completed_quest_worlds_on_user_and_world' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/javascript/game_modes/mate_in_one/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .haste-mode 3 | .haste-under-board 4 | .timers 5 | padding: 15px 0 6 | 7 | .timer 8 | font-size: 36px 9 | 10 | .n-solved 11 | font-size: 16px 12 | 13 | .haste-complete 14 | padding: 15px 0 15 | 16 | .score-section 17 | flex-direction: column 18 | gap: 20px 19 | margin-bottom: 20px 20 | 21 | .recent-high-scores 22 | margin-bottom: 20px 23 | 24 | .list 25 | gap: 6px 26 | -------------------------------------------------------------------------------- /app/views/game_modes/quest/play_quest_level.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Quest mode" } 2 | 3 | section.game-mode.quest-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | #puzzle-player( 15 | data-puzzles=@puzzle_data.to_json 16 | data-level-info=@puzzle_data[:level_info].to_json 17 | ) 18 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/game_modes/openings/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .openings-mode 3 | .openings-under-board 4 | .timers 5 | padding: 15px 0 6 | 7 | .timer 8 | font-size: 36px 9 | 10 | .n-solved 11 | font-size: 16px 12 | 13 | .openings-complete 14 | padding: 15px 0 15 | 16 | .score-section 17 | flex-direction: column 18 | gap: 20px 19 | margin-bottom: 20px 20 | 21 | .recent-high-scores 22 | margin-bottom: 20px 23 | 24 | .list 25 | gap: 6px 26 | -------------------------------------------------------------------------------- /app/views/game_modes/three.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Three mode" } 2 | 3 | section.game-mode.three-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .missed-puzzles-section#missed-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .missed-puzzles-list#missed-puzzles-list -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/game_modes/haste.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Haste mode" } 2 | 3 | section.game-mode.haste-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list 18 | -------------------------------------------------------------------------------- /spec/capybara_helper.rb: -------------------------------------------------------------------------------- 1 | require "selenium/webdriver" 2 | 3 | Capybara.register_driver :chrome do |app| 4 | Capybara::Selenium::Driver.new(app, browser: :chrome) 5 | end 6 | 7 | Capybara.register_driver :headless_chrome do |app| 8 | capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 9 | chromeOptions: { args: %w(headless disable-gpu) } 10 | ) 11 | 12 | Capybara::Selenium::Driver.new app, 13 | browser: :chrome, 14 | desired_capabilities: capabilities 15 | end 16 | 17 | Capybara.javascript_driver = :headless_chrome 18 | 19 | Capybara.raise_javascript_errors = true 20 | -------------------------------------------------------------------------------- /app/javascript/game_modes/rook-endgames/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .rook-endgames-mode 3 | .rook-endgames-under-board 4 | .timers 5 | padding: 15px 0 6 | 7 | .timer 8 | font-size: 36px 9 | 10 | .n-solved 11 | font-size: 16px 12 | 13 | .rook-endgames-complete 14 | padding: 15px 0 15 | 16 | .score-section 17 | flex-direction: column 18 | gap: 20px 19 | margin-bottom: 20px 20 | 21 | .recent-high-scores 22 | margin-bottom: 20px 23 | 24 | .list 25 | gap: 6px -------------------------------------------------------------------------------- /app/views/game_modes/openings.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Openings mode" } 2 | 3 | section.game-mode.openings-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list 18 | -------------------------------------------------------------------------------- /app/views/game_modes/speedrun.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Speedrun mode" } 2 | 3 | section.game-mode.speedrun-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list 18 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/game_modes/countdown.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Countdown mode" } 2 | 3 | section.game-mode.countdown-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list 18 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:title) { @user.username } %> 2 | 3 |
4 |
5 |
6 |

<%= @user.username %>

7 | <% if @user.tagline.present? %> 8 |

<%= @user.tagline %>

9 | <% end %> 10 |

Member since <%= @user.created_at&.strftime("%b %d, %Y") || "Unknown" %>

11 |
12 | 13 | <%= render partial: "users/puzzle_stats" %> 14 |
15 |
16 | 17 | <%= render 'shared/main_footer' %> 18 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/game_modes/mate_in_one.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Mate-in-One mode" } 2 | 3 | section.game-mode.mate-in-one-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list 18 | -------------------------------------------------------------------------------- /app/views/game_modes/rook_endgames.slim: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Rook endgames mode" } 2 | 3 | section.game-mode.rook-endgames-mode 4 | .container 5 | = render partial: "puzzle_player/above_board" 6 | .board-area-container 7 | .board-area 8 | .chessground-board 9 | .piece-promotion-modal-mount 10 | .chessground 11 | .board-modal-container.invisible(style="display: none") 12 | .vue-app-mount 13 | 14 | .played-puzzles-section#played-puzzles-section(style="display: none;") 15 | .container 16 | h3 Puzzles played 17 | .played-puzzles-list#played-puzzles-list -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .container 3 | width: 100vw 4 | padding: 0 5 | 6 | .below-board, .under-board 7 | width: 100vw !important 8 | 9 | .board-area 10 | width: 100vw !important 11 | height: 100vw !important 12 | 13 | .chessground-board 14 | width: 100% !important 15 | height: 100% !important 16 | 17 | // Hide board resizer when board is full-viewport on small screens 18 | .chessboard-drag-handle 19 | display: none !important 20 | 21 | @media (max-width: 500px) 22 | .below-board, .under-board 23 | padding: 20px 0 24 | -------------------------------------------------------------------------------- /db/migrate/20250821043546_create_completed_quest_world_levels.rb: -------------------------------------------------------------------------------- 1 | class CreateCompletedQuestWorldLevels < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :completed_quest_world_levels do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.references :quest_world_level, null: false, foreign_key: true 6 | t.datetime :completed_at 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :completed_quest_world_levels, [:user_id, :quest_world_level_id], 12 | unique: true, name: 'index_completed_quest_world_levels_on_user_and_level' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/static/descriptions/_vancura_position.html.erb: -------------------------------------------------------------------------------- 1 |

Vancura position

2 | 3 |
4 | Keep attacking white's pawn from the side, 5 | while preventing the king from hiding from checks. 6 |

7 | Your king should be behind your rook to not block attacks against white's pawn. 8 | If white's pawn moves to the 7th rank, move the rook behind it. 9 | 10 |
11 | resources: 12 | 13 | wikipedia 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static/descriptions/_philidor_position.html.erb: -------------------------------------------------------------------------------- 1 |

Philidor position

2 | 3 |
4 | Defend the Philidor position and play for a draw. Place the 5 | rook on the third rank to cut off the opposing king and prevent 6 | it from invading and threatening checkmate. 7 |

8 | Keep your rook on the third rank until Black advances the pawn. 9 | Then keep checking the Black kind from behind. 10 | 11 |
12 | resources: 13 | 14 | wikipedia 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chessnut/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/game_modes/countdown/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .countdown-mode 3 | .container 4 | display: block !important 5 | 6 | .countdown-under-board 7 | margin-top: 0 !important 8 | margin-left: 0 !important 9 | 10 | .timers 11 | flex-direction: row !important 12 | 13 | .countdown-complete 14 | padding: 15px 0 15 | 16 | .score-section 17 | flex-direction: column 18 | gap: 20px 19 | margin-bottom: 20px 20 | 21 | @media (max-width: 500px) 22 | .countdown-mode 23 | .countdown-under-board 24 | .description 25 | line-height: 18px -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/puzzle_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class PuzzleReportsController < ApplicationController 2 | 3 | def index 4 | @puzzle_reports = PuzzleReport.order('id DESC').limit(500) 5 | end 6 | 7 | def new 8 | @puzzle_id = params[:puzzle_id] 9 | end 10 | 11 | def create 12 | puzzle_report_params = params.require(:puzzle_report).permit(:puzzle_id, :message) 13 | puzzle_id = puzzle_report_params[:puzzle_id] 14 | PuzzleReport.create!({ 15 | user_id: current_user.id, 16 | puzzle_id: puzzle_id, 17 | message: puzzle_report_params[:message], 18 | }) 19 | redirect_to "/p/#{puzzle_id}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/javascript/game_modes/infinity/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import Infinity from './infinity.vue' 4 | import SolutionViewer from '../../pages/infinity' 5 | 6 | import './style.sass' 7 | import './responsive.sass' 8 | 9 | export type InfinityPuzzleDifficulty = 'easy' | 'medium' | 'hard' | 'insane' 10 | 11 | export interface InfinityPuzzleSolved { 12 | puzzle_id: number, 13 | difficulty: InfinityPuzzleDifficulty 14 | } 15 | 16 | export default function InfinityMode() { 17 | createApp(Infinity).mount('.infinity-mode .vue-app-mount') 18 | 19 | // Initialize solution viewer for recent puzzles 20 | SolutionViewer() 21 | } 22 | -------------------------------------------------------------------------------- /app/assets/images/pieces/firi/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/firi/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/game_modes/infinity/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .infinity-mode .infinity-sidebar 3 | width: 100vw 4 | margin: 0 !important 5 | flex-direction: row 6 | 7 | .sidebar-label 8 | width: 180px 9 | margin-bottom: 0 10 | 11 | .difficulties 12 | display: flex 13 | align-items: center 14 | 15 | [data-difficulty] 16 | padding: 5px 20px 5px 0 17 | 18 | &:last-child 19 | padding-right: 0 20 | 21 | .stats 22 | display: flex 23 | align-items: center 24 | 25 | .infinity-mode .infinity-under-board 26 | @media (max-width: 500px) 27 | padding: 10px 28 | -------------------------------------------------------------------------------- /db/migrate/20250123000000_add_created_at_index_to_solved_puzzles.rb: -------------------------------------------------------------------------------- 1 | class AddCreatedAtIndexToSolvedPuzzles < ActiveRecord::Migration[5.1] 2 | def change 3 | # Add composite index for efficient user-specific and date range queries 4 | # This will significantly improve performance for: 5 | # - User profile queries (user_id first for selectivity) 6 | # - unique_puzzles_solved_on_day queries (date range with grouping) 7 | # - admin analytics queries by date 8 | # - Recent activity feeds and daily stats 9 | add_index :solved_puzzles, [:user_id, :created_at], 10 | name: 'index_solved_puzzles_on_user_id_and_created_at' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chess7/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/static/svgs/_x.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/puzzle_sets/new.slim: -------------------------------------------------------------------------------- 1 | .container.puzzle-set-form-page 2 | h2 New puzzle set 3 | 4 | = form_for @new_puzzle_set do |f| 5 | .left 6 | div 7 | input.ps-input-name(name="puzzle_set[name]" type="text" placeholder="Puzzle set name") 8 | textarea.ps-input-description(name="puzzle_set[description]" placeholder="Description (optional)") 9 | textarea.ps-input-puzzle-ids(name="puzzle_set[puzzle_ids]" placeholder="Puzzle IDs") 10 | input.blue-button(type="submit" value="Create puzzle set" onClick="this.disabled = true; this.form.submit();") 11 | .right 12 | | Input up to 25k Lichess v2 puzzle IDs separated by spaces, newlines, or commas. 13 | 14 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_1.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.1 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 8 | 9 | # Make `form_with` generate non-remote forms. 10 | Rails.application.config.action_view.form_with_generates_remote_forms = false 11 | 12 | # Unknown asset fallback will return the path passed in when the given 13 | # asset is not present in the asset pipeline. 14 | # Rails.application.config.assets.unknown_asset_fallback = false 15 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrate/20190221152109_create_user_ratings.rb: -------------------------------------------------------------------------------- 1 | class CreateUserRatings < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :user_ratings do |t| 4 | t.integer :user_id, null: false 5 | t.float :initial_rating, null: false 6 | t.float :initial_rating_deviation, null: false 7 | t.float :initial_rating_volatility, null: false 8 | t.float :rating, null: false 9 | t.float :rating_deviation, null: false 10 | t.float :rating_volatility, null: false 11 | t.integer :rated_puzzle_attempts_count, null: false, default: 0 12 | t.timestamps null: false 13 | end 14 | add_index :user_ratings, :user_id, unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/game_modes/infinity/_recent_puzzle_item.slim: -------------------------------------------------------------------------------- 1 | - puzzle = puzzle_data[:puzzle] 2 | - solved_at = puzzle_data[:solved_at] 3 | - difficulty = puzzle_data[:difficulty] 4 | 5 | .recent-puzzle-item(data-puzzle-id=puzzle.puzzle_id) 6 | .puzzle-miniboard 7 | = linked_puzzle_miniboard(puzzle) 8 | .puzzle-info 9 | .puzzle-meta 10 | .puzzle-time= time_ago_in_words(solved_at) + " ago" 11 | .puzzle-difficulty Difficulty: #{difficulty} 12 | .puzzle-actions 13 | button.view-solution-btn( 14 | data-puzzle-id=puzzle.puzzle_id 15 | data-initial-fen=puzzle.initial_fen 16 | data-solution-lines=puzzle_data[:solution_lines].to_json 17 | ) Show solution 18 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mono/Q.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pixel/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/puzzles/puzzle_finder.rb: -------------------------------------------------------------------------------- 1 | # Find all puzzles that use a particular id 2 | # 3 | class PuzzleFinder 4 | 5 | # For finding all instances of a lichess puzzle 6 | def self.find_by_lichess_puzzle_id(id) 7 | puzzles = [] 8 | puzzles += CountdownPuzzle.where("(data ->> 'id')::int = ?", id).to_a 9 | puzzles += SpeedrunPuzzle.where("(data ->> 'id')::int = ?", id).to_a 10 | puzzles += InfinityPuzzle.where("(data ->> 'id')::int = ?", id).to_a 11 | puzzles += HastePuzzle.where("(data ->> 'id')::int = ?", id).to_a 12 | puzzles += RatedPuzzle.where("(data ->> 'id')::int = ?", id).to_a 13 | puzzles += RepetitionPuzzle.where("(data ->> 'id')::int = ?", id).to_a 14 | puzzles 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mpchess/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mpchess/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mpchess/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chessnut/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/rated_puzzle_attempt.rb: -------------------------------------------------------------------------------- 1 | class RatedPuzzleAttempt < ActiveRecord::Base 2 | # outcomes from the player's perspective vs. the puzzle 3 | OUTCOMES = %w( win loss draw ) 4 | 5 | UCI_MOVE_REGEX = /([a-h][1-8]){2}(qpnbr)?/ 6 | 7 | belongs_to :user_rating, counter_cache: true 8 | belongs_to :rated_puzzle, counter_cache: true 9 | 10 | validates :uci_moves, presence: true 11 | validates :elapsed_time_ms, presence: true 12 | validates :outcome, inclusion: OUTCOMES 13 | validate :check_uci_moves_format 14 | 15 | private 16 | 17 | def check_uci_moves_format 18 | if uci_moves.any? {|move| move !~ UCI_MOVE_REGEX } 19 | errors.add :uci_moves, "has invalid moves - #{uci_moves}" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/about.sass: -------------------------------------------------------------------------------- 1 | .about.container 2 | color: rgba(255, 255, 255, 0.9) 3 | margin: 45px auto 60px 4 | width: 600px 5 | 6 | a 7 | color: rgba(255, 255, 255, 0.8) 8 | text-decoration: underline 9 | font-weight: bold 10 | transition: color 0.12s ease 11 | 12 | &:hover 13 | color: rgba(255, 255, 255, 1) 14 | 15 | .question 16 | padding-bottom: 12px 17 | border-bottom: 1px solid rgba(255, 255, 255, 0.6) 18 | margin-bottom: 20px 19 | color: rgba(255, 255, 255, 0.9) 20 | font-weight: bold 21 | 22 | .answer 23 | margin: 10px 0 60px 24 | font-size: 16px 25 | line-height: 24px 26 | letter-spacing: 0.2px 27 | 28 | .cta 29 | margin-top: 55px 30 | font-size: 16px 31 | -------------------------------------------------------------------------------- /app/views/puzzle_sets/index.slim: -------------------------------------------------------------------------------- 1 | .container.puzzle-sets-index 2 | h2 Puzzle sets 3 | h3 Create your own puzzle sets from Lichess puzzles 4 | 5 | -#= "#{@puzzle_sets.length} puzzle sets" 6 | 7 | br 8 | a.blue-button(href="/puzzle-sets/new") Create puzzle set 9 | 10 | .puzzle-sets-list 11 | - @puzzle_sets.each do |puzzle_set| 12 | .puzzle-set 13 | a(href="/ps/#{puzzle_set.id}") 14 | .puzzle-set-name= puzzle_set.name 15 | .puzzle-set-info 16 | span= "#{puzzle_set.lichess_v2_puzzles.count} puzzles" 17 | span  -  18 | span= "created by #{puzzle_set.user.username}" 19 | span  -  20 | span= "created #{puzzle_set.created_at.strftime('%b %d, %Y')}" -------------------------------------------------------------------------------- /app/javascript/components/new_puzzle_player/views/instructions.ts: -------------------------------------------------------------------------------- 1 | // white to move 2 | 3 | import { subscribeOnce } from '@blitz/events' 4 | 5 | export default class Instructions { 6 | 7 | get el(): HTMLElement { 8 | return document.querySelector(`.instructions`) 9 | } 10 | 11 | constructor() { 12 | subscribeOnce('move:too_slow', () => { 13 | this.el.classList.add(`smaller`) 14 | }) 15 | subscribeOnce('puzzle:loaded', () => { 16 | this.el.classList.remove(`invisible`) 17 | }) 18 | subscribeOnce('board:flipped', isFlipped => { 19 | if (isFlipped) { 20 | this.el.textContent = `Black to move` 21 | } 22 | }) 23 | subscribeOnce('puzzles:start', () => this.el.classList.add('invisible')) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/pages/puzzle_list/style.sass: -------------------------------------------------------------------------------- 1 | body[data-controller=puzzles][data-action=index], body[data-action=puzzles] 2 | .puzzle-page 3 | margin-top: 30px 4 | 5 | h2 6 | margin-bottom: 15px 7 | 8 | .puzzles 9 | display: flex 10 | flex-wrap: wrap 11 | justify-content: space-between 12 | margin-bottom: 100px 13 | 14 | .blank 15 | width: 190px 16 | 17 | .miniboard-link 18 | margin: 20px 0 19 | 20 | .mini-chessboard 21 | width: 190px 22 | height: 190px 23 | 24 | .puzzle-info 25 | position: relative 26 | top: -10px 27 | display: flex 28 | justify-content: space-between 29 | 30 | .puzzle-num 31 | opacity: 0.5 32 | 33 | .puzzle-mistake-info 34 | color: var(--move-fail) 35 | -------------------------------------------------------------------------------- /db/migrate/20201229001317_create_lichess_v2_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateLichessV2Puzzles < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :lichess_v2_puzzles do |t| 4 | t.string :puzzle_id, null: false 5 | t.string :initial_fen, null: false 6 | t.text :moves_uci, array: true, null: false 7 | t.jsonb :lines_tree, null: false 8 | t.integer :rating, null: false 9 | t.integer :rating_deviation, null: false 10 | t.integer :popularity, null: false 11 | t.integer :num_plays, null: false 12 | t.text :themes, array: true, null: false 13 | t.string :game_url, null: false 14 | t.timestamps null: false 15 | end 16 | add_index :lichess_v2_puzzles, :puzzle_id, unique: true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/images/pieces/anarcandy/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:title) { "Password reset" } %> 2 | 3 |
4 |
5 |

Forgot your password?

6 | 7 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 8 | <%= render "devise/shared/error_messages", resource: resource %> 9 | 10 |
11 | <%= f.label :email %>
12 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 13 |
14 | 15 |
16 | <%= f.submit "Send me reset password instructions" %> 17 |
18 | <% end %> 19 | 20 | <%= render "devise/shared/links" %> 21 |
22 |
23 | -------------------------------------------------------------------------------- /ansible/blitz-puma.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Blitz Tactics Puma 3 | After=network.target postgresql.service 4 | Requires=postgresql.service 5 | 6 | [Service] 7 | Type=simple 8 | User={{ app_user }} 9 | WorkingDirectory={{ app_dir }} 10 | Environment=RAILS_ENV=production 11 | Environment=PIDFILE={{ app_dir }}/tmp/pids/puma.pid 12 | ExecStart=/bin/bash -lc "source /usr/local/share/chruby/chruby.sh && chruby ruby-{{ ruby_ver }} && bundle exec puma -C config/puma.rb" 13 | ExecReload=/bin/bash -lc "source /usr/local/share/chruby/chruby.sh && chruby ruby-{{ ruby_ver }} && bundle exec pumactl -P tmp/pids/puma.pid phased-restart" 14 | PIDFile={{ app_dir }}/tmp/pids/puma.pid 15 | Restart=always 16 | RestartSec=5 17 | TimeoutStartSec=30 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /app/javascript/types.ts: -------------------------------------------------------------------------------- 1 | // Represents data structures used in the JS part of the codebase 2 | // These are reflections of the data format used to persist the puzzles 3 | 4 | export type FEN = string 5 | 6 | export type UciMove = string 7 | 8 | export interface InitialMove { 9 | san: string, 10 | uci: UciMove, 11 | } 12 | 13 | export interface PuzzleLines { 14 | [uciMove: string]: PuzzleLines | 'win' | 'retry' 15 | } 16 | 17 | // fields for all puzzles fetched from the server 18 | export type Puzzle = { 19 | id: number 20 | fen: FEN 21 | lines: PuzzleLines 22 | initialMove: InitialMove 23 | } 24 | 25 | // For bootstrapping the page with JS data and query params 26 | export interface BlitzConfig { 27 | levelPath?: string 28 | position?: object 29 | loggedIn?: boolean 30 | } -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/components/chessground_board/svgs/board_bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/position.rb: -------------------------------------------------------------------------------- 1 | # deprecated custom positions for training 2 | 3 | class Position < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | before_validation :sanitize_fen 7 | validates :fen, presence: true 8 | validate :validate_fen_format 9 | 10 | def name_or_id 11 | name || "Position #{id}" 12 | end 13 | 14 | def belongs_to?(user) 15 | user&.id == user_id 16 | end 17 | 18 | private 19 | 20 | def sanitize_fen 21 | if self.fen.split(" ").length == 4 22 | self.fen = self.fen + " 0 1" 23 | end 24 | end 25 | 26 | def validate_fen_format 27 | if fen.split(" ").length != 6 28 | errors.add(:fen, "must have 6 components") 29 | end 30 | if fen.length > 90 31 | errors.add(:fen, "is invalid") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mpchess/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/completed_quest_world_level.rb: -------------------------------------------------------------------------------- 1 | class CompletedQuestWorldLevel < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :quest_world_level 4 | 5 | validates :user, presence: true 6 | validates :quest_world_level, presence: true 7 | validates :completed_at, presence: true 8 | validates :user_id, uniqueness: { scope: :quest_world_level_id } 9 | 10 | scope :for_user, ->(user) { where(user: user) } 11 | scope :for_level, ->(level) { where(quest_world_level: level) } 12 | scope :completed_on, ->(date) { where(completed_at: date.beginning_of_day..date.end_of_day) } 13 | 14 | def self.complete_level!(user, quest_world_level) 15 | create!( 16 | user: user, 17 | quest_world_level: quest_world_level, 18 | completed_at: Time.current 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/puzzles/index.slim: -------------------------------------------------------------------------------- 1 | .puzzle-page.container 2 | = "#{@puzzles.count} puzzles" 3 | 4 | .puzzles 5 | - @puzzles.each_with_index do |puzzle, i| 6 | .puzzle-item(data-puzzle-id="#{puzzle.id}") 7 | .puzzle-miniboard 8 | = linked_puzzle_miniboard(puzzle) 9 | .puzzle-info 10 | .puzzle-meta 11 | .puzzle-num= i + 1 12 | .puzzle-mistake-info 13 | .puzzle-actions 14 | button.view-solution-btn( 15 | data-puzzle-id=puzzle.puzzle_id 16 | data-initial-fen=puzzle.initial_fen 17 | data-solution-lines=puzzle.lines_tree.to_json 18 | ) Show solution 19 | - if @puzzles.count % 3 == 2 20 | .blank 21 | - elsif @puzzles.count % 3 == 1 22 | .blank 23 | .blank 24 | -------------------------------------------------------------------------------- /app/views/static/svgs/_volume_off.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/migrate/20190221152057_create_rated_puzzles.rb: -------------------------------------------------------------------------------- 1 | class CreateRatedPuzzles < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :rated_puzzles do |t| 4 | t.jsonb :data, null: false 5 | t.string :color, null: false 6 | t.string :puzzle_hash, null: false 7 | t.float :initial_rating, null: false 8 | t.float :initial_rating_deviation, null: false 9 | t.float :initial_rating_volatility, null: false 10 | t.float :rating, null: false 11 | t.float :rating_deviation, null: false 12 | t.float :rating_volatility, null: false 13 | t.integer :rated_puzzle_attempts_count, null: false, default: 0 14 | t.timestamps null: false 15 | end 16 | add_index :rated_puzzles, :rating 17 | add_index :rated_puzzles, :puzzle_hash, unique: true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/shapes/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.sass: -------------------------------------------------------------------------------- 1 | @forward "../../../vendor/assets/stylesheets/reset" 2 | @forward "css-variables" 3 | 4 | @forward "./base" 5 | @forward "./buttons" 6 | @forward "./main_header" 7 | @forward "./responsive" 8 | @forward "./adventure" 9 | 10 | @forward "./pages/about" 11 | @forward "./pages/achievements" 12 | @forward "./pages/admin_feature_flags" 13 | @forward "./pages/homepage" 14 | @forward "./pages/not_found" 15 | @forward "./pages/positions_index" 16 | @forward "./pages/puzzle_attempts" 17 | @forward "./pages/puzzle_reports" 18 | @forward "./pages/puzzle_sets" 19 | @forward "./pages/registration" 20 | @forward "./pages/scoreboard" 21 | @forward "./pages/user_profile" 22 | @forward "./pages/preferences" 23 | @forward "./pages/quest_edit" 24 | @forward "./pages/puzzle_explorer" 25 | @forward "./pages/adventure_index" -------------------------------------------------------------------------------- /app/javascript/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosPromise } from 'axios' 2 | 3 | const headersWithCsrfToken = (): AxiosRequestConfig => { 4 | const token: string = document 5 | .querySelector('meta[name="csrf-token"]') 6 | .getAttribute('content') 7 | return { 8 | headers: { 9 | 'X-CSRF-Token': token 10 | } 11 | } 12 | } 13 | 14 | export default { 15 | get(path: string): AxiosPromise { 16 | return axios.get(path) 17 | }, 18 | post(path: string, data = {}): AxiosPromise { 19 | return axios.post(path, data, headersWithCsrfToken()) 20 | }, 21 | put(path: string, data): AxiosPromise { 22 | return axios.put(path, data, headersWithCsrfToken()) 23 | }, 24 | patch(path: string, data): AxiosPromise { 25 | return axios.patch(path, data, headersWithCsrfToken()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/images/pieces/fresca/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/fresca/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/models/level_status.ts: -------------------------------------------------------------------------------- 1 | // Tracks progress within the level and whether the next level is unlocked 2 | // 3 | export default class LevelStatus { 4 | private numPuzzles = 0 5 | private puzzleCounter = 0 6 | public completed = false 7 | 8 | public setNumPuzzles(numPuzzles: number) { 9 | this.numPuzzles = numPuzzles 10 | } 11 | 12 | public nextPuzzle(): void { 13 | this.puzzleCounter += 1 14 | } 15 | 16 | public getProgress(): number { 17 | let progress = ~~( 100 * this.puzzleCounter / this.numPuzzles ) 18 | if (progress > 100) { 19 | progress = 100 20 | } 21 | return progress 22 | } 23 | 24 | public resetProgress(): void { 25 | this.puzzleCounter = 0 26 | } 27 | 28 | public nextLevelUnlocked(): boolean { 29 | return this.getProgress() === 100 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/views/level_indicator.ts: -------------------------------------------------------------------------------- 1 | import { subscribe, GameEvent } from '@blitz/events' 2 | 3 | // Level name, next level, etc. 4 | // 5 | export default class LevelIndicator { 6 | private levelNameEl: HTMLElement 7 | private nextStageEl: HTMLElement 8 | 9 | get el() { 10 | return document.querySelector(`.repetition-under-board`) 11 | } 12 | 13 | constructor() { 14 | this.levelNameEl = this.el.querySelector(`.level-name`) 15 | this.nextStageEl = this.el.querySelector(`.next-stage`) 16 | subscribe({ 17 | [GameEvent.PUZZLES_START]: () => { 18 | this.levelNameEl.classList.add(`faded`) 19 | }, 20 | 'level:unlocked': () => { 21 | this.levelNameEl.classList.add(`invisible`) 22 | this.nextStageEl.classList.remove(`invisible`) 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chessnut/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": ["es2020", "dom"], 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": ["node_modules/*", "app/javascript/*"], 13 | "@blitz/*": ["app/javascript/*"] 14 | }, 15 | "sourceMap": true, 16 | "target": "es5" 17 | }, 18 | "include": [ 19 | "app/javascript/**/*.ts", 20 | "app/javascript/**/*.tsx", 21 | "app/javascript/globals.d.ts", // Optional, but good for clarity 22 | "app/javascript/**/*.vue" 23 | ], 24 | "exclude": [ 25 | "**/*.spec.ts", 26 | "node_modules", 27 | "vendor", 28 | "public" 29 | ], 30 | "compileOnSave": false 31 | } 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '3.4.5' 4 | 5 | gem 'rails', '~> 8.0' 6 | gem 'puma', '~> 7.0' 7 | gem 'bootsnap', require: false 8 | 9 | # persistence 10 | gem 'pg', '~> 1.0' 11 | 12 | gem 'dotenv-rails' 13 | gem 'devise', '~> 4.3' 14 | gem 'slim' 15 | gem 'glicko2' 16 | gem 'nio4r', '~> 2.5.9' 17 | 18 | gem 'execjs' 19 | gem 'mini_racer', '~> 0.19', platforms: :ruby 20 | 21 | # assets 22 | gem 'propshaft' 23 | gem 'jsbundling-rails' 24 | 25 | # production 26 | gem 'mailgun-ruby' 27 | gem 'bugsnag' 28 | 29 | # ruby standard lib 30 | gem 'ostruct' 31 | 32 | group :development do 33 | gem 'listen' 34 | gem 'web-console', '>= 3.3.0' 35 | gem 'pry' 36 | end 37 | 38 | group :test do 39 | gem 'rspec' 40 | gem 'rspec-rails' 41 | gem 'capybara' 42 | gem 'capybara-selenium' 43 | gem 'selenium-webdriver' 44 | gem 'webdrivers' 45 | end 46 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/reillycraig/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | /data/lichess 11 | /data/ 12 | *.swp 13 | 14 | /public/assets 15 | 16 | /.vscode 17 | 18 | # Ignore all logfiles and tempfiles. 19 | /log/* 20 | !/log/.keep 21 | /tmp 22 | /public/packs 23 | /public/packs-test 24 | /node_modules 25 | yarn-debug.log* 26 | .yarn-integrity 27 | 28 | /.env.development 29 | /.env 30 | 31 | /public/packs 32 | /public/packs-test 33 | /node_modules 34 | /yarn-error.log 35 | yarn-debug.log* 36 | .yarn-integrity 37 | 38 | /app/assets/builds/* 39 | !/app/assets/builds/.keep 40 | -------------------------------------------------------------------------------- /app/assets/images/pieces/leipzig/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chess7/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/reillycraig/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/pages/puzzle_set/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import PuzzlePlayer from '@blitz/components/puzzle_player' 4 | import { subscribeOnce } from '@blitz/events' 5 | import PuzzleSetSidebar from './sidebar.vue' 6 | 7 | import './style.sass' 8 | 9 | // creates a chessboard and starts the puzzle player. after the player makes a move, 10 | // a new sidebar is created 11 | export default () => { 12 | new PuzzlePlayer({ 13 | shuffle: false, 14 | loopPuzzles: false, 15 | noCounter: true, 16 | noHint: true, 17 | source: `${window.location.pathname}/puzzles.json`, 18 | }) 19 | const vueAppSelector = '.puzzle-set .vue-app-mount' 20 | subscribeOnce('move:try', () => { 21 | createApp(PuzzleSetSidebar).mount(vueAppSelector) 22 | const el: HTMLDivElement = document.querySelector(vueAppSelector) 23 | el.style.display = 'block' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/images/pieces/chessnut/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/game_modes/repetition/views/progress_bar.ts: -------------------------------------------------------------------------------- 1 | // bar under main header showing how close you are to the next level 2 | 3 | import { subscribe, GameEvent } from '@blitz/events' 4 | 5 | export default class ProgressBar { 6 | private progressEl: HTMLElement 7 | private complete = false 8 | 9 | get el(): HTMLElement { 10 | return document.querySelector(`.progress-bar`) 11 | } 12 | 13 | constructor() { 14 | this.progressEl = this.el.querySelector(`.progress`) 15 | subscribe({ 16 | 'progress:update': percent => { 17 | if (!this.complete) { 18 | this.updateProgress(percent) 19 | } 20 | } 21 | }) 22 | } 23 | 24 | updateProgress(percent) { 25 | this.progressEl.style.width = `${percent}%` 26 | if (percent >= 100) { 27 | this.progressEl.classList.add(`complete`) 28 | this.complete = true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /app/assets/images/pieces/merida/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/cburnett/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/riohacha/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/riohacha/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/anarcandy/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/letter/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/merida/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/merida/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/mpchess/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/letter/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/pages/defined_position.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:title) { @route[:title].html_safe } %> 2 | <% content_for(:meta_description) { @route[:description].html_safe } %> 3 | 4 | <% has_description = %w( 5 | /lucena-position 6 | /philidor-position 7 | /vancura-position 8 | /saavedra-position 9 | ).include?(@route[:path]) %> 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | <% if has_description %> 18 |
19 |
20 | <%= render partial: "static/descriptions#{@route[:path].gsub(/-/, '_')}" %> 21 |
22 |
23 | <% end %> 24 | 25 | 34 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/fresca/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/fresca/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/completed_haste_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the score from completing a set of haste puzzles 2 | 3 | class CompletedHasteRound < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | validates :score, presence: true, numericality: { greater_than: 0 } 7 | 8 | def formatted_time_spent 9 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 10 | end 11 | 12 | # get high scores from a rolling time period 13 | def self.high_scores(since) 14 | where('created_at >= ?', since) 15 | .group(:user_id).maximum(:score) 16 | .sort_by {|user_id, score| -score }.take(5) 17 | .map do |user_id, score| 18 | [ 19 | User.find_by(id: user_id), 20 | score 21 | ] 22 | end 23 | end 24 | 25 | # best score of a particular day 26 | def self.personal_best(day) 27 | where( 28 | 'created_at >= ? AND created_at <= ?', 29 | day.beginning_of_day, 30 | day.end_of_day 31 | ).maximum(:score) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/user_data/completed_three_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the score from completing a set of threes puzzles 2 | 3 | class CompletedThreeRound < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | validates :score, presence: true, numericality: { greater_than: 0 } 7 | 8 | def formatted_time_spent 9 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 10 | end 11 | 12 | # get high scores from a rolling time period 13 | def self.high_scores(since) 14 | where('created_at >= ?', since) 15 | .group(:user_id).maximum(:score) 16 | .sort_by {|user_id, score| -score }.take(5) 17 | .map do |user_id, score| 18 | [ 19 | User.find_by(id: user_id), 20 | score 21 | ] 22 | end 23 | end 24 | 25 | # best score of a particular day 26 | def self.personal_best(day) 27 | where( 28 | 'created_at >= ? AND created_at <= ?', 29 | day.beginning_of_day, 30 | day.end_of_day 31 | ).maximum(:score) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/javascript/game_modes/speedrun/responsive.sass: -------------------------------------------------------------------------------- 1 | @media (max-aspect-ratio: 11/16) 2 | .speedrun-mode 3 | .container 4 | display: block !important 5 | 6 | .speedrun-under-board 7 | margin-top: 0 !important 8 | margin-left: 0 !important 9 | padding: 20px 10 | 11 | .timers 12 | flex-direction: row !important 13 | 14 | .make-a-move 15 | width: 100% !important 16 | 17 | .speedrun-complete 18 | width: 100% !important 19 | padding: 15px 0 20 | 21 | .timers-section 22 | flex-direction: column 23 | gap: 16px 24 | margin-bottom: 12px 25 | 26 | .speedrun-instructions 27 | width: 100% !important 28 | 29 | .choose-level 30 | display: flex 31 | flex-direction: row 32 | align-items: center 33 | justify-content: space-between 34 | width: 100% 35 | 36 | * 37 | margin: 0 !important 38 | 39 | .n-puzzles 40 | text-align: right 41 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/completed_openings_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the score from completing a set of opening puzzles 2 | 3 | class CompletedOpeningsRound < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | validates :score, presence: true, numericality: { greater_than: 0 } 7 | 8 | def formatted_time_spent 9 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 10 | end 11 | 12 | # get high scores from a rolling time period 13 | def self.high_scores(since) 14 | where('created_at >= ?', since) 15 | .group(:user_id).maximum(:score) 16 | .sort_by {|user_id, score| -score }.take(5) 17 | .map do |user_id, score| 18 | [ 19 | User.find_by(id: user_id), 20 | score 21 | ] 22 | end 23 | end 24 | 25 | # best score of a particular day 26 | def self.personal_best(day) 27 | where( 28 | 'created_at >= ? AND created_at <= ?', 29 | day.beginning_of_day, 30 | day.end_of_day 31 | ).maximum(:score) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kiwen-suwi/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/completed_mate_in_one_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the score from completing a set of mate-in-one puzzles 2 | 3 | class CompletedMateInOneRound < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | validates :score, presence: true, numericality: { greater_than: 0 } 7 | 8 | def formatted_time_spent 9 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 10 | end 11 | 12 | # get high scores from a rolling time period 13 | def self.high_scores(since) 14 | where('created_at >= ?', since) 15 | .group(:user_id).maximum(:score) 16 | .sort_by {|user_id, score| -score }.take(5) 17 | .map do |user_id, score| 18 | [ 19 | User.find_by(id: user_id), 20 | score 21 | ] 22 | end 23 | end 24 | 25 | # best score of a particular day 26 | def self.personal_best(day) 27 | where( 28 | 'created_at >= ? AND created_at <= ?', 29 | day.beginning_of_day, 30 | day.end_of_day 31 | ).maximum(:score) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/user_data/completed_rook_endgames_round.rb: -------------------------------------------------------------------------------- 1 | # tracks the score from completing a set of rook endgame puzzles 2 | 3 | class CompletedRookEndgamesRound < ActiveRecord::Base 4 | belongs_to :user 5 | 6 | validates :score, presence: true, numericality: { greater_than: 0 } 7 | 8 | def formatted_time_spent 9 | Time.at(elapsed_time_ms / 1000).strftime("%M:%S").gsub(/^0/, '') 10 | end 11 | 12 | # get high scores from a rolling time period 13 | def self.high_scores(since) 14 | where('created_at >= ?', since) 15 | .group(:user_id).maximum(:score) 16 | .sort_by {|user_id, score| -score }.take(5) 17 | .map do |user_id, score| 18 | [ 19 | User.find_by(id: user_id), 20 | score 21 | ] 22 | end 23 | end 24 | 25 | # best score of a particular day 26 | def self.personal_best(day) 27 | where( 28 | 'created_at >= ? AND created_at <= ?', 29 | day.beginning_of_day, 30 | day.end_of_day 31 | ).maximum(:score) 32 | end 33 | end -------------------------------------------------------------------------------- /app/models/importers/haste_puzzle_loader.rb: -------------------------------------------------------------------------------- 1 | module HastePuzzleLoader 2 | 3 | def self.create_haste_puzzles_from_json_file 4 | puts "#{HastePuzzle.count} haste puzzles in db. Creating haste puzzles..." 5 | num_checked = 0 6 | num_created = 0 7 | open(Rails.root.join(PuzzleLoader::HASTE_PUZZLE_SOURCE), "r") do |f| 8 | haste_puzzle_list = JSON.parse(f.read) 9 | ActiveRecord::Base.transaction do 10 | haste_puzzle_list.each do |puzzle| 11 | begin 12 | if HastePuzzle.create!( 13 | data: puzzle, 14 | color: puzzle["color"], 15 | difficulty: puzzle["difficulty"], 16 | ) 17 | num_created += 1 18 | end 19 | rescue 20 | # Puzzle is already created. Do nothing 21 | end 22 | num_checked += 1 23 | end 24 | puts "Created #{num_created} haste puzzles out of #{num_checked} puzzles in .json file" 25 | end 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20250821173256_rename_order_to_number_in_quest_tables.rb: -------------------------------------------------------------------------------- 1 | class RenameOrderToNumberInQuestTables < ActiveRecord::Migration[8.0] 2 | def up 3 | # Rename columns 4 | rename_column :quest_worlds, :order, :number 5 | rename_column :quest_world_levels, :order, :number 6 | 7 | # Convert from 0-indexing to 1-indexing 8 | QuestWorld.reset_column_information 9 | QuestWorld.update_all("number = number + 1") 10 | 11 | QuestWorldLevel.reset_column_information 12 | QuestWorldLevel.update_all("number = number + 1") 13 | end 14 | 15 | def down 16 | # Convert from 1-indexing back to 0-indexing 17 | QuestWorld.reset_column_information 18 | QuestWorld.update_all("number = number - 1") 19 | 20 | QuestWorldLevel.reset_column_information 21 | QuestWorldLevel.update_all("number = number - 1") 22 | 23 | # Rename columns back 24 | rename_column :quest_worlds, :number, :order 25 | rename_column :quest_world_levels, :number, :order 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/users/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::RegistrationsController < Devise::RegistrationsController 2 | def update 3 | self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key) 4 | 5 | prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email) 6 | 7 | resource_updated = update_resource(resource, account_update_params) 8 | yield resource if block_given? 9 | if resource_updated 10 | set_flash_message_for_update(resource, prev_unconfirmed_email) 11 | bypass_sign_in resource, scope: resource_name if sign_in_after_change_password? 12 | redirect_to preferences_path 13 | else 14 | clean_up_passwords resource 15 | set_minimum_password_length 16 | flash.now[:alert] = resource.errors.full_messages.join(', ') 17 | render 'users/preferences' 18 | end 19 | end 20 | 21 | protected 22 | 23 | def after_update_path_for(_resource) 24 | preferences_path 25 | end 26 | end 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/models/puzzle_set.rb: -------------------------------------------------------------------------------- 1 | class PuzzleSet < ActiveRecord::Base 2 | belongs_to :user 3 | has_many :lichess_v2_puzzles_puzzle_sets 4 | has_many :lichess_v2_puzzles, through: :lichess_v2_puzzles_puzzle_sets 5 | 6 | PUZZLE_LIMIT = 25_000 # arbitrary limit on # of puzzles per puzzle set 7 | 8 | def textarea_puzzle_ids 9 | lichess_v2_puzzles.pluck(:puzzle_id).join("\n") 10 | end 11 | 12 | def num_puzzles 13 | @num_puzzles ||= lichess_v2_puzzles.count 14 | end 15 | 16 | # TODO fix performance problems 17 | def random_level 18 | if num_puzzles < 10 19 | return lichess_v2_puzzles.map(&:bt_puzzle_data).shuffle 20 | end 21 | sorted_puzzle_ids = lichess_v2_puzzles.order('rating ASC').pluck(:id) 22 | bucket_size = (num_puzzles / 10.0).ceil 23 | serve_puzzle_ids = [] 24 | sorted_puzzle_ids.each_slice(bucket_size).each do |bucket| 25 | serve_puzzle_ids.push(*bucket.shuffle.take(10)) 26 | end 27 | LichessV2Puzzle.find_by_sorted(serve_puzzle_ids).map(&:bt_puzzle_data) 28 | end 29 | end -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/user_data/user_rating.rb: -------------------------------------------------------------------------------- 1 | class UserRating < ActiveRecord::Base 2 | belongs_to :user 3 | has_many :rated_puzzle_attempts 4 | 5 | after_initialize :initialize_glicko2_rating 6 | 7 | validates :initial_rating, presence: true 8 | validates :initial_rating_deviation, presence: true 9 | validates :initial_rating_volatility, presence: true 10 | 11 | validates :rating, presence: true 12 | validates :rating_deviation, presence: true 13 | validates :rating_volatility, presence: true 14 | 15 | # for display on the homepage 16 | def rating_string 17 | new_record? ? 'Unrated' : rating.round 18 | end 19 | 20 | private 21 | 22 | def initialize_glicko2_rating 23 | return if initial_rating.present? 24 | self.initial_rating = 1500 25 | self.initial_rating_deviation = 350 26 | self.initial_rating_volatility = 0.06 27 | self.rating = self.initial_rating 28 | self.rating_deviation = self.initial_rating_deviation 29 | self.rating_volatility = self.initial_rating_volatility 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/javascript/components/mini_chessboard/pieces.ts: -------------------------------------------------------------------------------- 1 | import m from 'mithril' 2 | 3 | interface PieceAttributes { 4 | oncreate?: (vnode: m.Component) => void 5 | } 6 | 7 | export default function virtualPiece(piece, oncreate = null): m.Component { 8 | const pieceAttrs: PieceAttributes = {} 9 | if (oncreate) { 10 | pieceAttrs.oncreate = oncreate 11 | } 12 | 13 | // Map single character piece types to full names (matching CSS selectors) 14 | const pieceTypeMap: { [key: string]: string } = { 15 | 'p': 'pawn', 16 | 'b': 'bishop', 17 | 'n': 'knight', 18 | 'r': 'rook', 19 | 'q': 'queen', 20 | 'k': 'king' 21 | } 22 | 23 | const fullPieceType = pieceTypeMap[piece.type] || piece.type 24 | const fullColorName = piece.color === 'w' ? 'white' : 'black' 25 | 26 | // Use CSS background images instead of inline SVG elements 27 | // Create piece elements like: (matching .cg-wrap piece.pawn.white format) 28 | return m(`piece.${fullPieceType}.${fullColorName}`, pieceAttrs) 29 | } 30 | -------------------------------------------------------------------------------- /app/assets/images/pieces/alpha/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 64ba24c96063e9219a61a146dae946f7c79312bc62df4bbff568366a3e92a4489d737b039719d58d19dcf88cb8b911075767b66a0d5db2fb2a4d900a50f419c5 15 | 16 | test: 17 | secret_key_base: 05c69ef6e4d909e5604d73e0dd68e9dca916ccaa8c70f62954a5f8b12cb78ac19de9e70d42da00b56c344e65489f3bf5ff3fe684769e5c6a32f0b46d0276146a 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /app/views/static/snippets/_miniboard.html.erb: -------------------------------------------------------------------------------- 1 | <% fen = local_assigns[:fen] %> 2 | <% initial_move = local_assigns[:initial_move] %> 3 | <% initial_move_san = local_assigns[:initial_move_san] %> 4 | <% flip = local_assigns[:flip] || false %> 5 | 6 |
10 | data-initial-move-san="<%= initial_move_san %>" 11 | <% elsif initial_move %> 12 | data-initial-move="<%= initial_move %>" 13 | <% end %> 14 | > 15 | <% polarity = %w( light dark ).cycle %> 16 | <% rows = (1..8).to_a.reverse %> 17 | <% cols = ('a'..'h').to_a %> 18 | <% if flip %> 19 | <% rows = rows.reverse %> 20 | <% cols = cols.reverse %> 21 | <% end %> 22 | <% rows.each do |row| %> 23 | <% cols.each do |col| %> 24 | <% square_id = "#{col}#{row}" %> 25 |
">
27 | <% end %> 28 | <% polarity.next %> 29 | <% end %> 30 |
31 | -------------------------------------------------------------------------------- /app/assets/images/pieces/kosal/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/pieces/pirouetti/bQ.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------