├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------