diff --git a/.ruby-version b/.ruby-version
index be94e6f..f989260 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.2.2
+3.4.4
diff --git a/Gemfile b/Gemfile
index 4b7cfb2..b5652f6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,36 +1,38 @@
-source "https://rubygems.org"
+# frozen_string_literal: true
-ruby "3.2.2"
+source 'https://rubygems.org'
+
+ruby '3.4.4'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
-gem "rails", "~> 7.1.5", ">= 7.1.5.1"
+gem 'rails', '~> 7.1.5', '>= 7.1.5.1'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
-gem "sprockets-rails"
+gem 'sprockets-rails'
# Use sqlite3 as the database for Active Record
-gem "sqlite3", ">= 1.4"
+gem 'sqlite3', '>= 1.4'
# Use the Puma web server [https://github.com/puma/puma]
-gem "puma", ">= 5.0"
+gem 'puma', '>= 5.0'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
-gem "importmap-rails"
+gem 'importmap-rails'
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
-gem "turbo-rails"
+gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
-gem "stimulus-rails"
+gem 'stimulus-rails'
# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails]
-gem "tailwindcss-rails", "~> 2.3"
+gem 'tailwindcss-rails', '~> 2.3'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
-gem "jbuilder"
+gem 'jbuilder'
# Use Redis adapter to run Action Cable in production
-gem "redis", ">= 4.0.1"
+gem 'redis', '>= 4.0.1'
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
@@ -39,38 +41,39 @@ gem "redis", ">= 4.0.1"
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
-gem "tzinfo-data", platforms: %i[ windows jruby ]
+gem 'tzinfo-data', platforms: %i[windows jruby]
# Reduces boot times through caching; required in config/boot.rb
-gem "bootsnap", require: false
+gem 'bootsnap', require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
-gem "bcrypt", "~> 3.1.7"
+gem 'bcrypt', '~> 3.1.7'
gem 'sidekiq'
gem 'sidekiq-scheduler'
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
- gem "debug", platforms: %i[ mri windows ]
- gem "rspec-rails", "~> 6.0"
- gem "factory_bot_rails", "~> 6.2"
- gem "faker", "~> 3.2"
- gem "pry"
+ gem 'debug', platforms: %i[mri windows]
+ gem 'factory_bot_rails', '~> 6.2'
+ gem 'faker', '~> 3.2'
+ gem 'pry'
+ gem 'rspec-rails', '~> 6.0'
end
group :test do
- gem "shoulda-matchers", "~> 5.3"
- gem "capybara", "~> 3.39"
- gem "simplecov", require: false
- gem "launchy"
+ gem 'capybara', '~> 3.39'
+ gem 'launchy'
+ gem 'selenium-webdriver'
+ gem 'shoulda-matchers', '~> 5.3'
+ gem 'simplecov', require: false
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
- gem "web-console"
+ gem 'web-console'
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
@@ -78,4 +81,3 @@ group :development do
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
-
diff --git a/Gemfile.lock b/Gemfile.lock
index 915a094..e78a835 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -80,12 +80,12 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
- base64 (0.2.0)
+ base64 (0.3.0)
bcrypt (3.1.20)
- benchmark (0.4.0)
- bigdecimal (3.1.9)
+ benchmark (0.4.1)
+ bigdecimal (3.2.2)
bindex (0.8.1)
- bootsnap (1.18.4)
+ bootsnap (1.18.6)
msgpack (~> 1.2)
builder (3.3.0)
capybara (3.40.0)
@@ -101,23 +101,24 @@ GEM
logger (~> 1.5)
coderay (1.1.3)
concurrent-ruby (1.3.5)
- connection_pool (2.5.2)
+ connection_pool (2.5.3)
crass (1.0.6)
date (3.4.1)
- debug (1.10.0)
+ debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
- diff-lcs (1.6.1)
+ diff-lcs (1.6.2)
docile (1.4.1)
- drb (2.2.1)
+ drb (2.2.3)
+ erb (5.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
- factory_bot (6.5.1)
+ factory_bot (6.5.4)
activesupport (>= 6.1.0)
- factory_bot_rails (6.4.4)
+ factory_bot_rails (6.5.0)
factory_bot (~> 6.5)
- railties (>= 5.0.0)
+ railties (>= 6.1.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
fugit (1.11.1)
@@ -139,13 +140,13 @@ GEM
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
- json (2.12.0)
+ json (2.12.2)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
logger (1.7.0)
- loofah (2.24.0)
+ loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -154,13 +155,13 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
- matrix (0.4.2)
+ matrix (0.4.3)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
mutex_m (0.3.0)
- net-imap (0.5.7)
+ net-imap (0.5.8)
date
net-protocol
net-pop (0.1.2)
@@ -192,16 +193,16 @@ GEM
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
- psych (5.2.3)
+ psych (5.2.6)
date
stringio
- public_suffix (6.0.1)
+ public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
- rack (3.1.13)
- rack-session (2.1.0)
+ rack (3.1.16)
+ rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -222,7 +223,7 @@ GEM
activesupport (= 7.1.5.1)
bundler (>= 1.15.0)
railties (= 7.1.5.1)
- rails-dom-testing (2.2.0)
+ rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -237,22 +238,24 @@ GEM
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
- rake (13.2.1)
- rdoc (6.13.1)
+ rake (13.3.0)
+ rdoc (6.14.1)
+ erb
psych (>= 4.0.0)
redis (5.4.0)
redis-client (>= 0.22.0)
- redis-client (0.24.0)
+ redis-client (0.25.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
- rspec-core (3.13.3)
+ rexml (3.4.1)
+ rspec-core (3.13.4)
rspec-support (~> 3.13.0)
- rspec-expectations (3.13.3)
+ rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-mocks (3.13.2)
+ rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.5)
@@ -263,19 +266,26 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
- rspec-support (3.13.2)
+ rspec-support (3.13.4)
+ rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
securerandom (0.4.1)
+ selenium-webdriver (4.33.0)
+ base64 (~> 0.2)
+ logger (~> 1.4)
+ rexml (~> 3.2, >= 3.2.5)
+ rubyzip (>= 1.2.2, < 3.0)
+ websocket (~> 1.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
- sidekiq (8.0.3)
+ sidekiq (8.0.4)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
rack (>= 3.1.0)
redis-client (>= 0.23.2)
- sidekiq-scheduler (6.0.0)
+ sidekiq-scheduler (6.0.1)
rufus-scheduler (~> 3.2)
sidekiq (>= 7.3, < 9)
simplecov (0.22.0)
@@ -292,14 +302,14 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
- sqlite3 (2.6.0-aarch64-linux-gnu)
- sqlite3 (2.6.0-aarch64-linux-musl)
- sqlite3 (2.6.0-arm-linux-gnu)
- sqlite3 (2.6.0-arm-linux-musl)
- sqlite3 (2.6.0-arm64-darwin)
- sqlite3 (2.6.0-x86_64-darwin)
- sqlite3 (2.6.0-x86_64-linux-gnu)
- sqlite3 (2.6.0-x86_64-linux-musl)
+ sqlite3 (2.7.0-aarch64-linux-gnu)
+ sqlite3 (2.7.0-aarch64-linux-musl)
+ sqlite3 (2.7.0-arm-linux-gnu)
+ sqlite3 (2.7.0-arm-linux-musl)
+ sqlite3 (2.7.0-arm64-darwin)
+ sqlite3 (2.7.0-x86_64-darwin)
+ sqlite3 (2.7.0-x86_64-linux-gnu)
+ sqlite3 (2.7.0-x86_64-linux-musl)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
@@ -315,7 +325,7 @@ GEM
railties (>= 7.0.0)
thor (1.3.2)
timeout (0.4.3)
- turbo-rails (2.0.13)
+ turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
@@ -325,18 +335,20 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
- websocket-driver (0.7.7)
+ websocket (1.2.11)
+ websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.7.2)
+ zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
+ arm-linux
arm-linux-gnu
arm-linux-musl
arm64-darwin
@@ -360,6 +372,7 @@ DEPENDENCIES
rails (~> 7.1.5, >= 7.1.5.1)
redis (>= 4.0.1)
rspec-rails (~> 6.0)
+ selenium-webdriver
shoulda-matchers (~> 5.3)
sidekiq
sidekiq-scheduler
@@ -373,7 +386,7 @@ DEPENDENCIES
web-console
RUBY VERSION
- ruby 3.2.2p53
+ ruby 3.4.4p34
BUNDLED WITH
- 2.6.4
+ 2.6.9
diff --git a/README.md b/README.md
index d7f4b9b..c7a9f6d 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Key features include:
- **Population Growth**: Watch your empire grow based on your tax policies and system types
- **Economic Strategy**: Balance taxation and population growth to maximize your empire's potential
- **Scheduled Maintenance**: Experience daily empire maintenance cycles that update your resources
+- **Building System**: Construct, upgrade, and demolish buildings to enhance your star systems
## Technical Details
@@ -22,13 +23,15 @@ Key features include:
- **Authentication**: Custom implementation using BCrypt (no Devise)
- **Background Jobs**: Sidekiq with sidekiq-scheduler for maintenance tasks
- **Ruby Version**: 3.2.2
+- **Procfile**: Uses Foreman/bin/dev to run Rails, Sidekiq, and TailwindCSS concurrently
## Setup Instructions
### Prerequisites
-- Ruby 3.2.2
-- Redis (for Sidekiq)
-- Node.js and Yarn (for TailwindCSS)
+- Ruby 3.4.4
+- Redis 8.0.2 (for Sidekiq)
+- Node.js 22.15.0
+- Yarn 1.22.19 (for TailwindCSS)
### Installation
@@ -49,7 +52,21 @@ yarn install
bin/rails db:create db:migrate
```
-4. Start the server, worker, and CSS compiler
+4. Seed the database with building types and (optionally) a sample empire
+```bash
+bin/rails db:seed
+```
+
+ - The seed file creates core building types. To see a sample building, ensure you have a user, an empire, and a star system. You can create these via the Rails console:
+```ruby
+# In rails console
+y = User.create!(email: "test@example.com", password: "password")
+e = Empire.create!(user: y, name: "Test Empire")
+s = StarSystem.create!(empire: e, name: "Sol", system_type: "terrestrial")
+```
+ - Then re-run `bin/rails db:seed` to create a sample building in your star system.
+
+5. Start the server, worker, and CSS compiler
```bash
bin/dev
```
@@ -68,6 +85,11 @@ Check test coverage with SimpleCov (results in coverage/ directory):
COVERAGE=true bundle exec rspec
```
+#### Testing Notes
+- Uses [DatabaseCleaner](https://github.com/DatabaseCleaner/database_cleaner) with truncation for feature tests to ensure database state is visible across Capybara and Rails processes.
+- Feature tests for modals and JavaScript use Selenium with headless Chrome. Progressive enhancement ensures modals are accessible and testable in both JS and non-JS environments.
+- Accessibility and progressive enhancement are prioritized for all UI features, including modals.
+
## Game Mechanics
### Empire Management
@@ -84,6 +106,14 @@ COVERAGE=true bundle exec rspec
- Credits: Generated through taxation
- Minerals, Energy, Food: Base resources for building and maintenance
+### Building System
+- Construct, upgrade, and demolish buildings in your star systems
+- Each building type (e.g., Government Administration, Mining Facility, Power Plant, Research Laboratory) provides unique benefits
+- Buildings have construction and demolition times, costs, and effects
+- Some buildings are unique per system, others can be built multiple times
+- Building status is updated during scheduled maintenance cycles
+- Building construction and demolition use accessible, progressively enhanced modals for confirmation
+
## Development Approach
This project follows Test-Driven Development (TDD) principles:
@@ -92,11 +122,11 @@ This project follows Test-Driven Development (TDD) principles:
3. Refactor for improved design
The application is built with a clean, modular architecture:
-- **Models**: Core domain objects (User, Empire, StarSystem)
+- **Models**: Core domain objects (User, Empire, StarSystem, Building, BuildingType)
- **Services**: Encapsulated business logic (EmpireBuilderService)
- **Jobs**: Background processing (MaintenanceJob, ScheduleMaintenanceJob)
- **Controllers**: Minimal request handling with business logic in services
## Project Status
-Voidfront Realms Elite is currently under active development. Core gameplay systems including user authentication, empire management, and star system management are functional. Future updates will include ship building, research, exploration, and more advanced gameplay features.
+Voidfront Realms Elite is currently under active development. Core gameplay systems including user authentication, empire management, star system management, and the building system are functional. Future updates will include ship building, research, exploration, and more advanced gameplay features.
diff --git a/Rakefile b/Rakefile
index 9a5ea73..488c551 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require_relative "config/application"
+require_relative 'config/application'
Rails.application.load_tasks
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
index d672697..9aec230 100644
--- a/app/channels/application_cable/channel.rb
+++ b/app/channels/application_cable/channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
index 0ff5442..8d6c2a1 100644
--- a/app/channels/application_cable/connection.rb
+++ b/app/channels/application_cable/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index beb0357..5779bea 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,26 +1,28 @@
+# frozen_string_literal: true
+
class ApplicationController < ActionController::Base
before_action :require_login
helper_method :current_user, :user_signed_in?
-
+
private
-
- def current_user
- @current_user ||= if session[:user_id]
- User.find_by(id: session[:user_id])
- elsif cookies.encrypted[:user_id]
- user = User.find_by(id: cookies.encrypted[:user_id])
- if user
- session[:user_id] = user.id
- user
- end
+
+ def current_user
+ @current_user ||= if session[:user_id]
+ User.find_by(id: session[:user_id])
+ elsif cookies.encrypted[:user_id]
+ user = User.find_by(id: cookies.encrypted[:user_id])
+ if user
+ session[:user_id] = user.id
+ user
end
- end
-
- def user_signed_in?
- !current_user.nil?
- end
-
- def require_login
- redirect_to root_path, alert: "You must be logged in to access this page." unless user_signed_in?
- end
+ end
+ end
+
+ def user_signed_in?
+ !current_user.nil?
+ end
+
+ def require_login
+ redirect_to root_path, alert: 'You must be logged in to access this page.' unless user_signed_in?
+ end
end
diff --git a/app/controllers/buildings_controller.rb b/app/controllers/buildings_controller.rb
new file mode 100644
index 0000000..5bf137d
--- /dev/null
+++ b/app/controllers/buildings_controller.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+class BuildingsController < ApplicationController
+ before_action :set_building, only: [:destroy]
+ before_action :authorize_building, only: [:destroy]
+
+ def create
+ Rails.logger.debug "BuildingsController#create called with params: #{params.inspect}"
+
+ star_system = StarSystem.find(params[:building][:star_system_id])
+ building_type = BuildingType.find(params[:building][:building_type_id])
+
+ # check authorization
+ unless star_system.empire == current_user.empire
+ redirect_to root_path, alert: 'You do not have permission to build in this star system.'
+ return
+ end
+
+ # check if at max buildings
+ if star_system.buildings.where(status: %w[operational under_construction]).count >= star_system.max_buildings
+ redirect_to edit_star_system_path(star_system), alert: 'Maximum buildings reached'
+ return
+ end
+
+ # check if unique building already exists
+ if building_type.unique_per_system && star_system.buildings.where(building_type: building_type).exists?
+ redirect_to edit_star_system_path(star_system), alert: 'This building type already exists in this star system'
+ return
+ end
+
+ # check resources
+ cost = building_type.cost_for_level(1)
+ empire = current_user.empire
+
+ if empire.credits < cost['credits'] || empire.minerals < cost['minerals'] || empire.energy < cost['energy']
+ redirect_to edit_star_system_path(star_system), alert: 'Insufficient Resources'
+ return
+ end
+
+ # all checks pass, good to go
+ construction_time = building_type.construction_time_for_level(1)
+
+ ActiveRecord::Base.transaction do
+ # Deduct resources
+ empire.update!(
+ credits: empire.credits - cost['credits'],
+ minerals: empire.minerals - cost['minerals'],
+ energy: empire.energy - cost['energy']
+ )
+
+ # Create building
+ Building.create!(
+ star_system: star_system,
+ building_type: building_type,
+ level: 1,
+ status: 'under_construction',
+ construction_start: Time.current,
+ construction_end: Time.current + construction_time
+ )
+ end
+
+ redirect_to edit_star_system_path(star_system), notice: 'Building construction started'
+ rescue StandardError => e
+ redirect_to edit_star_system_path(@star_system), alert: "Error starting construction: #{e.message}"
+ end
+
+ def destroy
+ @building.update(
+ status: 'being_demolished',
+ demolition_end: Time.current + @building.building_type.demolition_time_for_level(@building.level)
+ )
+
+ redirect_to edit_star_system_path(@building.star_system), notice: 'Building demolition started'
+ end
+
+ private
+
+ def set_building
+ @building = Building.find(params[:id])
+ end
+
+ def authorize_building
+ return if @building.star_system.empire == current_user.empire
+
+ redirect_to root_path, alert: 'You do not have permission to demolish this building'
+ end
+end
diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb
index 749f0b9..35fea22 100644
--- a/app/controllers/dashboards_controller.rb
+++ b/app/controllers/dashboards_controller.rb
@@ -1,4 +1,5 @@
+# frozen_string_literal: true
+
class DashboardsController < ApplicationController
- def index
- end
-end
\ No newline at end of file
+ def index; end
+end
diff --git a/app/controllers/empires_controller.rb b/app/controllers/empires_controller.rb
index 63cdf14..0823ce6 100644
--- a/app/controllers/empires_controller.rb
+++ b/app/controllers/empires_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmpiresController < ApplicationController
def edit
@empire = current_user.empire
@@ -6,7 +8,7 @@ def edit
def update
@empire = current_user.empire
if @empire.update(empire_params)
- redirect_to dashboard_path, notice: "Empire updated successfully"
+ redirect_to dashboard_path, notice: 'Empire updated successfully'
else
render :edit, status: :unprocessable_entity
end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 4ae9d0d..be4df2a 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
+
class HomeController < ApplicationController
skip_before_action :require_login, only: [:index]
-
- def index
- end
-end
\ No newline at end of file
+
+ def index; end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index f08b1c0..da03086 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,20 +1,19 @@
+# frozen_string_literal: true
+
class SessionsController < ApplicationController
- skip_before_action :require_login, only: [:new, :create]
- def new
- end
+ skip_before_action :require_login, only: %i[new create]
+ def new; end
def create
user = User.find_by(email: params[:email])
- if user && user.authenticate(params[:password])
+ if user&.authenticate(params[:password])
session[:user_id] = user.id
- if params[:remember_me] == '1'
- cookies.permanent.encrypted[:user_id] = user.id
- end
- redirect_to root_path, notice: "Login Successful"
+ cookies.permanent.encrypted[:user_id] = user.id if params[:remember_me] == '1'
+ redirect_to root_path, notice: 'Login Successful'
else
- flash.now[:alert] = "Invalid email or password"
+ flash.now[:alert] = 'Invalid email or password'
render :new, status: :unprocessable_entity
end
end
@@ -22,6 +21,6 @@ def create
def destroy
session[:user_id] = nil
cookies.permanent.encrypted[:user_id] = nil
- redirect_to root_path, notice: "You have been logged out"
+ redirect_to root_path, notice: 'You have been logged out'
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/star_systems_controller.rb b/app/controllers/star_systems_controller.rb
index d5a2569..9862740 100644
--- a/app/controllers/star_systems_controller.rb
+++ b/app/controllers/star_systems_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StarSystemsController < ApplicationController
before_action :authorize_star_system
@@ -8,7 +10,7 @@ def edit
def update
@star_system = StarSystem.find(params[:id])
if @star_system.update(star_system_params)
- redirect_to edit_star_system_path(@star_system), notice: "Star system updated successfully"
+ redirect_to edit_star_system_path(@star_system), notice: 'Star system updated successfully'
else
render :edit, status: :unprocessable_entity
end
@@ -22,8 +24,8 @@ def star_system_params
def authorize_star_system
@star_system = StarSystem.find(params[:id])
- unless @star_system.empire == current_user.empire
- redirect_to root_path, alert: "You do not have permission to administer this star system"
- end
+ return if @star_system.empire == current_user.empire
+
+ redirect_to root_path, alert: 'You do not have permission to administer this star system'
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index ba0ab50..54c5ae8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
class UsersController < ApplicationController
- skip_before_action :require_login, only: [:new, :create]
+ skip_before_action :require_login, only: %i[new create]
def new
@user = User.new
end
-
+
def create
@user = User.new(user_params)
-
+
if @user.save
empire_name = params[:user][:empire_name].presence
empire = EmpireBuilderService.new(@user).create_empire(empire_name)
@@ -16,17 +18,17 @@ def create
@user.errors.add(:empire_name, empire.errors.messages[:name].first) if empire.errors.messages[:name].present?
return render :new, status: :unprocessable_entity
end
-
+
session[:user_id] = @user.id
- redirect_to dashboard_path, notice: "Registration Successful"
+ redirect_to dashboard_path, notice: 'Registration Successful'
else
render :new, status: :unprocessable_entity
end
end
-
+
private
-
+
def user_params
params.require(:user).permit(:email, :username, :password, :password_confirmation)
end
-end
\ No newline at end of file
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index de6be79..15b06f0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module ApplicationHelper
end
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index d394c3d..bef3959 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
diff --git a/app/jobs/maintenance_job.rb b/app/jobs/maintenance_job.rb
index 2e86033..c1f8f5d 100644
--- a/app/jobs/maintenance_job.rb
+++ b/app/jobs/maintenance_job.rb
@@ -1,27 +1,48 @@
+# frozen_string_literal: true
+
class MaintenanceJob < ApplicationJob
queue_as :default
def perform(empire_id)
@empire = Empire.find(empire_id)
return unless @empire
-
- maintenance_tasks
+
+ maintenance_tasks
end
private
def maintenance_tasks
ActiveRecord::Base.transaction do
- @empire.update(credits: @empire.credits + tax_revenue) if tax_revenue > 0
+ # handle building status updates
+ update_building_statuses
+ # collect tax revenue
+ @empire.update(credits: @empire.credits + tax_revenue) if tax_revenue.positive?
+
+ # update population
@empire.star_systems.each do |system|
system.update(current_population: system.new_population)
end
end
end
+ def update_building_statuses
+ @empire.star_systems.each do |system|
+ # Find buildings where construction has finished
+ system.buildings.where(status: 'under_construction')
+ .where('construction_end <= ?', Time.current)
+ .update_all(status: 'operational')
+
+ # Remove buildings where demolition has finished
+ system.buildings.where(status: 'being_demolished')
+ .where('demolition_end <= ?', Time.current)
+ .destroy_all
+ end
+ end
+
def tax_revenue
- total_population = @empire.star_systems.sum(:current_population)
- (total_population * @empire.tax_rate / 100).floor
+ # Calculate tax income for each star system and sum them up
+ @empire.star_systems.sum(&:calculate_tax_income)
end
end
diff --git a/app/jobs/schedule_maintenance_job.rb b/app/jobs/schedule_maintenance_job.rb
index 561e25f..3d7698f 100644
--- a/app/jobs/schedule_maintenance_job.rb
+++ b/app/jobs/schedule_maintenance_job.rb
@@ -1,13 +1,13 @@
+# frozen_string_literal: true
+
class ScheduleMaintenanceJob < ApplicationJob
queue_as :default
def perform
Empire.find_each(batch_size: 100) do |empire|
- begin
- MaintenanceJob.perform_later(empire.id)
- rescue StandardError => e
- Rails.logger.error("Failed to schedule maintenance for Empire #{empire.id}: #{e.message}")
- end
+ MaintenanceJob.perform_later(empire.id)
+ rescue StandardError => e
+ Rails.logger.error("Failed to schedule maintenance for Empire #{empire.id}: #{e.message}")
end
end
end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 3c34c81..d84cb6e 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class ApplicationMailer < ActionMailer::Base
- default from: "from@example.com"
- layout "mailer"
+ default from: 'from@example.com'
+ layout 'mailer'
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index b63caeb..08dc537 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
diff --git a/app/models/building.rb b/app/models/building.rb
new file mode 100644
index 0000000..32bdde3
--- /dev/null
+++ b/app/models/building.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+class Building < ApplicationRecord
+ belongs_to :building_type
+ belongs_to :star_system
+
+ # Statuses
+ STATUSES = %w[under_construction being_demolished operational].freeze
+
+ # Validations
+ validates :level, presence: true, numericality: { only_integer: true, greater_than: 0 }
+ validates :status, presence: true, inclusion: { in: STATUSES }
+ validate :unique_per_system, if: -> { building_type&.unique_per_system? }
+
+ # Status methods
+ def under_construction?
+ status == 'under_construction'
+ end
+
+ def operational?
+ status == 'operational'
+ end
+
+ def being_demolished?
+ status == 'being_demolished'
+ end
+
+ # Progress calculation methods
+ def construction_progress_percentage
+ return 100 if operational? || being_demolished?
+ return 0 if !construction_start || !construction_end
+
+ elapsed_time = Time.current - construction_start
+ total_time = construction_end - construction_start
+
+ progress = (elapsed_time / total_time * 100).round
+ [progress, 100].min
+ end
+
+ def demolition_progress_percentage
+ return 0 unless being_demolished?
+ return 0 if !updated_at || !demolition_end
+
+ elapsed_time = Time.current - updated_at
+ total_time = demolition_end - updated_at
+
+ progress = (elapsed_time / total_time * 100).round
+ [progress, 100].min
+ end
+
+ # Effects methods
+ def current_effect(effect_key)
+ return nil if under_construction? || being_demolished?
+
+ building_type.effects_for_level(level)[effect_key]
+ end
+
+ private
+
+ def unique_per_system
+ return unless building_type.unique_per_system?
+
+ existing = Building.where(building_type_id: building_type_id, star_system_id: star_system_id)
+ existing = existing.where.not(id: id) if persisted?
+
+ return unless existing.exists?
+
+ errors.add(:building_type_id, 'already exists in this star system')
+ end
+end
diff --git a/app/models/building_type.rb b/app/models/building_type.rb
new file mode 100644
index 0000000..953d648
--- /dev/null
+++ b/app/models/building_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class BuildingType < ApplicationRecord
+ validates :key, presence: true, uniqueness: true
+ validates :name, presence: true
+ validates :description, presence: true
+ validates :max_level, presence: true, numericality: { greater_than: 0 }
+
+ def cost_for_level(level)
+ level_data.dig(level.to_s, 'cost')
+ end
+
+ def construction_time_for_level(level)
+ level_data.dig(level.to_s, 'construction_time')
+ end
+
+ def demolition_time_for_level(level)
+ level_data.dig(level.to_s, 'demolition_time')
+ end
+
+ def effects_for_level(level)
+ level_data.dig(level.to_s, 'effects')
+ end
+end
diff --git a/app/models/empire.rb b/app/models/empire.rb
index 35548b4..7e5907d 100644
--- a/app/models/empire.rb
+++ b/app/models/empire.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
class Empire < ApplicationRecord
belongs_to :user
has_many :star_systems, dependent: :destroy
validates :name, presence: true, uniqueness: true
- validates :credits, :minerals, :energy, :food,
+ validates :credits, :minerals, :energy, :food,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :tax_rate, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
-
+
def resources_summary
"Credits: #{credits} | Minerals: #{minerals} | Energy: #{energy} | Food: #{food}"
end
diff --git a/app/models/star_system.rb b/app/models/star_system.rb
index 22d38d7..4a3d031 100644
--- a/app/models/star_system.rb
+++ b/app/models/star_system.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
class StarSystem < ApplicationRecord
belongs_to :empire
-
- SYSTEM_TYPES = %w[terrestrial ocean desert tundra gas_giant asteroid_belt]
-
+ has_many :buildings, dependent: :destroy
+
+ SYSTEM_TYPES = %w[terrestrial ocean desert tundra gas_giant asteroid_belt].freeze
+
validates :name, presence: true
validates :system_type, presence: true, inclusion: { in: SYSTEM_TYPES }
validates :max_population, numericality: { only_integer: true, greater_than: 0 }
@@ -12,17 +15,17 @@ class StarSystem < ApplicationRecord
def base_growth_rate
case system_type
- when "terrestrial"
+ when 'terrestrial'
0.05
- when "ocean"
+ when 'ocean'
0.04
- when "tundra"
+ when 'tundra'
0.03
- when "desert"
+ when 'desert'
0.02
- when "gas_giant"
+ when 'gas_giant'
0.01
- when "asteroid_belt"
+ when 'asteroid_belt'
0.005
else
0.01 # Default fallback
@@ -41,7 +44,27 @@ def calculate_growth
def new_population
new_population = current_population + calculate_growth
- new_population = [new_population, 1].max # Can't go below 1
- [new_population, max_population].min # Can't exceed max
+ new_population = [new_population, 1].max # Can't go below 1
+ [new_population, max_population].min # Can't exceed max
+ end
+
+ def buildings_count
+ buildings.where(status: 'operational').count
+ end
+
+ def tax_modifier_from_buildings
+ buildings.where(status: 'operational').sum do |building|
+ building.current_effect('tax_modifier')
+ end
+ end
+
+ def calculate_tax_income
+ base_tax = (current_population * empire.tax_rate / 100.0).floor
+
+ modifier = tax_modifier_from_buildings
+
+ additional_tax = (base_tax * modifier).floor
+
+ base_tax + additional_tax
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 629c56b..bc13308 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class User < ApplicationRecord
has_secure_password
has_one :empire, dependent: :destroy
-
+
validates :email, presence: true, uniqueness: true
validates :username, presence: true, uniqueness: true
-end
\ No newline at end of file
+end
diff --git a/app/services/empire_builder_service.rb b/app/services/empire_builder_service.rb
index a8208e3..7419fe9 100644
--- a/app/services/empire_builder_service.rb
+++ b/app/services/empire_builder_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmpireBuilderService
def initialize(user)
@user = user
@@ -12,40 +14,38 @@ def create_empire(name = nil)
name: empire_name
)
- unless empire.save
- return empire
- end
+ return empire unless empire.save
create_starting_star_system(empire)
empire
end
- rescue => e
+ rescue StandardError => e
empire = Empire.new(user: @user)
empire.errors.add(:name, e.message)
empire
end
-
- private
- def generate_empire_name
- 10.times do
- name = "#{Faker::Adjective.positive}-#{Faker::Hipster.word}"
- return name unless Empire.exists?(name: name)
- end
+ private
- "#{Faker::Adjective.positive}-#{Faker::Hipster.word}-#{Faker::Number.hexadecimal(digits: 4)}"
+ def generate_empire_name
+ 10.times do
+ name = "#{Faker::Adjective.positive}-#{Faker::Hipster.word}"
+ return name unless Empire.exists?(name: name)
end
- def create_starting_star_system(empire)
- StarSystem.create!(
+ "#{Faker::Adjective.positive}-#{Faker::Hipster.word}-#{Faker::Number.hexadecimal(digits: 4)}"
+ end
+
+ def create_starting_star_system(empire)
+ StarSystem.create!(
name: Faker::Space.star,
- system_type: "terrestrial",
+ system_type: 'terrestrial',
max_population: 1000,
current_population: 500,
max_buildings: 10,
loyalty: 100,
empire: empire
- )
- end
-end
\ No newline at end of file
+ )
+ end
+end
diff --git a/app/views/star_systems/edit.html.erb b/app/views/star_systems/edit.html.erb
index f490a4f..25cfc26 100644
--- a/app/views/star_systems/edit.html.erb
+++ b/app/views/star_systems/edit.html.erb
@@ -1,4 +1,4 @@
-
+
+
Confirm Building Construction
+
+
+
Are you sure you want to construct a ?
+
This will cost:
+
+ Credits
+ Minerals
+ Energy
+
-
- <%= form.submit "Update Star System", class: "bg-space-nebula hover:bg-space-purple text-white font-medium py-2 px-4 rounded-lg transition" %>
- <%= link_to "Cancel", dashboard_path, class: "bg-transparent border border-space-purple/50 text-space-highlight hover:bg-space-blue/30 py-2 px-4 rounded-lg transition" %>
+
+
+ Cancel
+
+
+ Confirm Construction
+
- <% end %>
+
+
+
diff --git a/bin/bundle b/bin/bundle
index 50da5fd..ef688ec 100755
--- a/bin/bundle
+++ b/bin/bundle
@@ -8,46 +8,46 @@
# this file is here to facilitate running it.
#
-require "rubygems"
+require 'rubygems'
m = Module.new do
module_function
def invoked_as_script?
- File.expand_path($0) == File.expand_path(__FILE__)
+ File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
end
def env_var_version
- ENV["BUNDLER_VERSION"]
+ ENV['BUNDLER_VERSION']
end
def cli_arg_version
return unless invoked_as_script? # don't want to hijack other binstubs
- return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
+ return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
+
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
- if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
- bundler_version = a
- end
+ bundler_version = a if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
- bundler_version = $1
+
+ bundler_version = Regexp.last_match(1)
update_index = i
end
bundler_version
end
def gemfile
- gemfile = ENV["BUNDLE_GEMFILE"]
+ gemfile = ENV['BUNDLE_GEMFILE']
return gemfile if gemfile && !gemfile.empty?
- File.expand_path("../Gemfile", __dir__)
+ File.expand_path('../Gemfile', __dir__)
end
def lockfile
lockfile =
case File.basename(gemfile)
- when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
+ when 'gems.rb' then gemfile.sub(/\.rb$/, '.locked')
else "#{gemfile}.lock"
end
File.expand_path(lockfile)
@@ -55,8 +55,10 @@ m = Module.new do
def lockfile_version
return unless File.file?(lockfile)
+
lockfile_contents = File.read(lockfile)
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+
Regexp.last_match(1)
end
@@ -76,20 +78,24 @@ m = Module.new do
end
def load_bundler!
- ENV["BUNDLE_GEMFILE"] ||= gemfile
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
activate_bundler
end
def activate_bundler
gem_error = activation_error_handling do
- gem "bundler", bundler_requirement
+ gem 'bundler', bundler_requirement
end
return if gem_error.nil?
+
require_error = activation_error_handling do
- require "bundler/version"
+ require 'bundler/version'
+ end
+ if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
+ return
end
- return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
+
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
exit 42
end
@@ -104,6 +110,4 @@ end
m.load_bundler!
-if m.invoked_as_script?
- load Gem.bin_path("bundler", "bundle")
-end
+load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
diff --git a/bin/importmap b/bin/importmap
index 36502ab..d423864 100755
--- a/bin/importmap
+++ b/bin/importmap
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
-require_relative "../config/application"
-require "importmap/commands"
+require_relative '../config/application'
+require 'importmap/commands'
diff --git a/bin/rails b/bin/rails
index efc0377..a31728a 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
-APP_PATH = File.expand_path("../config/application", __dir__)
-require_relative "../config/boot"
-require "rails/commands"
+# frozen_string_literal: true
+
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
index 4fbf10b..c199955 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
-require_relative "../config/boot"
-require "rake"
+# frozen_string_literal: true
+
+require_relative '../config/boot'
+require 'rake'
Rake.application.run
diff --git a/bin/setup b/bin/setup
index 3cd5a9d..3a74034 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,8 +1,10 @@
#!/usr/bin/env ruby
-require "fileutils"
+# frozen_string_literal: true
+
+require 'fileutils'
# path to your application root.
-APP_ROOT = File.expand_path("..", __dir__)
+APP_ROOT = File.expand_path('..', __dir__)
def system!(*args)
system(*args, exception: true)
@@ -13,9 +15,9 @@ FileUtils.chdir APP_ROOT do
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
- puts "== Installing dependencies =="
- system! "gem install bundler --conservative"
- system("bundle check") || system!("bundle install")
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")
@@ -23,11 +25,11 @@ FileUtils.chdir APP_ROOT do
# end
puts "\n== Preparing database =="
- system! "bin/rails db:prepare"
+ system! 'bin/rails db:prepare'
puts "\n== Removing old logs and tempfiles =="
- system! "bin/rails log:clear tmp:clear"
+ system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
- system! "bin/rails restart"
+ system! 'bin/rails restart'
end
diff --git a/config.ru b/config.ru
index 4a3c09a..6dc8321 100644
--- a/config.ru
+++ b/config.ru
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
-require_relative "config/environment"
+require_relative 'config/environment'
run Rails.application
Rails.application.load_server
diff --git a/config/application.rb b/config/application.rb
index 623f704..ec263a2 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,17 +1,19 @@
-require_relative "boot"
+# frozen_string_literal: true
-require "rails"
+require_relative 'boot'
+
+require 'rails'
# Pick the frameworks you want:
-require "active_model/railtie"
-require "active_job/railtie"
-require "active_record/railtie"
-require "active_storage/engine"
-require "action_controller/railtie"
-require "action_mailer/railtie"
-require "action_mailbox/engine"
-require "action_text/engine"
-require "action_view/railtie"
-require "action_cable/engine"
+require 'active_model/railtie'
+require 'active_job/railtie'
+require 'active_record/railtie'
+require 'active_storage/engine'
+require 'action_controller/railtie'
+require 'action_mailer/railtie'
+require 'action_mailbox/engine'
+require 'action_text/engine'
+require 'action_view/railtie'
+require 'action_cable/engine'
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
@@ -26,7 +28,7 @@ class Application < Rails::Application
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
- config.autoload_lib(ignore: %w(assets tasks))
+ config.autoload_lib(ignore: %w[assets tasks])
# Configuration for the application, engines, and railties goes here.
#
diff --git a/config/boot.rb b/config/boot.rb
index 988a5dd..c04863f 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,6 @@
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+# frozen_string_literal: true
-require "bundler/setup" # Set up gems listed in the Gemfile.
-require "bootsnap/setup" # Speed up boot time by caching expensive operations.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/config/environment.rb b/config/environment.rb
index cac5315..d5abe55 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Load the Rails application.
-require_relative "application"
+require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 2e7fb48..80e0587 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -19,13 +21,13 @@
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
- if Rails.root.join("tmp/caching-dev.txt").exist?
+ if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{2.days.to_i}"
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
diff --git a/config/environments/production.rb b/config/environments/production.rb
index af8a1c4..fc472c3 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -52,17 +54,17 @@
config.force_ssl = true
# Log to STDOUT by default
- config.logger = ActiveSupport::Logger.new(STDOUT)
- .tap { |logger| logger.formatter = ::Logger::Formatter.new }
- .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
+ config.logger = ActiveSupport::Logger.new($stdout)
+ .tap { |logger| logger.formatter = ::Logger::Formatter.new }
+ .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
# Prepend all log lines with the following tags.
- config.log_tags = [ :request_id ]
+ config.log_tags = [:request_id]
# "info" includes generic and useful information about system operation, but avoids logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII). If you
# want to log everything, set the level to "debug".
- config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
+ config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
diff --git a/config/environments/test.rb b/config/environments/test.rb
index adbb4a6..f1d2fb5 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
@@ -15,12 +17,12 @@
# this is usually not necessary, and can slow down your test suite. However, it's
# recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
- config.eager_load = ENV["CI"].present?
+ config.eager_load = ENV['CI'].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{1.hour.to_i}"
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
diff --git a/config/importmap.rb b/config/importmap.rb
index 909dfc5..15fd627 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
# Pin npm packages by running ./bin/importmap
-pin "application"
-pin "@hotwired/turbo-rails", to: "turbo.min.js"
-pin "@hotwired/stimulus", to: "stimulus.min.js"
-pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
-pin_all_from "app/javascript/controllers", under: "controllers"
+pin 'application'
+pin '@hotwired/turbo-rails', to: 'turbo.min.js'
+pin '@hotwired/stimulus', to: 'stimulus.min.js'
+pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js'
+pin_all_from 'app/javascript/controllers', under: 'controllers'
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 2eeef96..bcafccd 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = "1.0"
+Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index b3076b3..35ab3fd 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy.
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index c2d89e2..c416e6a 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
-Rails.application.config.filter_parameters += [
- :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+Rails.application.config.filter_parameters += %i[
+ passw secret token _key crypt salt certificate otp ssn
]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 3860f65..9e049dc 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
index 7db3b95..e8d0b2a 100644
--- a/config/initializers/permissions_policy.rb
+++ b/config/initializers/permissions_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Define an application-wide HTTP permissions policy. For further
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 97bafe6..c5c3433 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'sidekiq'
require 'sidekiq-scheduler'
Sidekiq.configure_server do |config|
config.redis = { url: ENV['REDIS_URL'] || 'redis://localhost:6379/0' }
-
+
# Load the schedule
config.on(:startup) do
Sidekiq.schedule = YAML.load_file(File.join(Rails.root, 'config/sidekiq.yml'))[:scheduler][:schedule]
diff --git a/config/puma.rb b/config/puma.rb
index 7a709d7..5a61a65 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
@@ -7,20 +9,20 @@
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
-max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
-min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
+min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count
-rails_env = ENV.fetch("RAILS_ENV") { "development" }
+rails_env = ENV.fetch('RAILS_ENV', 'development')
-if rails_env == "production"
+if rails_env == 'production'
# If you are running more than 1 thread per process, the workers count
# should be equal to the number of processors (CPU cores) in production.
#
# It defaults to 1 because it's impossible to reliably detect how many
# CPU cores are available. Make sure to set the `WEB_CONCURRENCY` environment
# variable to match the number of processors.
- worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { 1 })
+ worker_count = Integer(ENV.fetch('WEB_CONCURRENCY', 1))
if worker_count > 1
workers worker_count
else
@@ -29,16 +31,16 @@
end
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
-worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
+worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch('PORT', 3000)
# Specifies the `environment` that Puma will run in.
environment rails_env
# Specifies the `pidfile` that Puma will use.
-pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
index f150af6..327fd09 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,16 +1,19 @@
+# frozen_string_literal: true
+
require 'sidekiq/web'
require 'sidekiq-scheduler/web'
Rails.application.routes.draw do
- get "up" => "rails/health#show", as: :rails_health_check
+ get 'up' => 'rails/health#show', as: :rails_health_check
mount Sidekiq::Web => '/sidekiq'
- root "home#index"
+ root 'home#index'
- resources :users, only: [:new, :create]
- resources :empires, only: [:edit, :update]
- resources :star_systems, only: [:edit, :update]
+ resources :users, only: %i[new create]
+ resources :empires, only: %i[edit update]
+ resources :star_systems, only: %i[edit update]
+ resources :buildings, only: %i[create destroy]
get '/dashboard', to: 'dashboards#index', as: :dashboard
diff --git a/db/migrate/20250426005300_create_users.rb b/db/migrate/20250426005300_create_users.rb
index 48a5277..f07d122 100644
--- a/db/migrate/20250426005300_create_users.rb
+++ b/db/migrate/20250426005300_create_users.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
diff --git a/db/migrate/20250430195738_create_empires.rb b/db/migrate/20250430195738_create_empires.rb
index d51c5a3..6ac054f 100644
--- a/db/migrate/20250430195738_create_empires.rb
+++ b/db/migrate/20250430195738_create_empires.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateEmpires < ActiveRecord::Migration[7.1]
def change
create_table :empires do |t|
diff --git a/db/migrate/20250430200649_create_star_systems.rb b/db/migrate/20250430200649_create_star_systems.rb
index 13f9958..d6e2529 100644
--- a/db/migrate/20250430200649_create_star_systems.rb
+++ b/db/migrate/20250430200649_create_star_systems.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateStarSystems < ActiveRecord::Migration[7.1]
def change
create_table :star_systems do |t|
diff --git a/db/migrate/20250518141932_add_tax_rate_to_empires.rb b/db/migrate/20250518141932_add_tax_rate_to_empires.rb
index a83072c..def86b6 100644
--- a/db/migrate/20250518141932_add_tax_rate_to_empires.rb
+++ b/db/migrate/20250518141932_add_tax_rate_to_empires.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddTaxRateToEmpires < ActiveRecord::Migration[7.1]
def change
add_column :empires, :tax_rate, :integer, default: 20
diff --git a/db/migrate/20250520201621_create_building_types.rb b/db/migrate/20250520201621_create_building_types.rb
new file mode 100644
index 0000000..e795548
--- /dev/null
+++ b/db/migrate/20250520201621_create_building_types.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateBuildingTypes < ActiveRecord::Migration[7.1]
+ def change
+ create_table :building_types do |t|
+ t.string :key, null: false
+ t.string :name, null: false
+ t.text :description, null: false
+ t.boolean :unique_per_system, default: false
+ t.integer :max_level, default: 1, null: false
+ t.json :level_data, default: {}, null: false
+ t.json :prerequisites, default: {}, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20250521011436_create_buildings.rb b/db/migrate/20250521011436_create_buildings.rb
new file mode 100644
index 0000000..777e6fa
--- /dev/null
+++ b/db/migrate/20250521011436_create_buildings.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateBuildings < ActiveRecord::Migration[7.1]
+ def change
+ create_table :buildings do |t|
+ t.references :building_type, null: false, foreign_key: true
+ t.references :star_system, null: false, foreign_key: true
+ t.integer :level, null: false, default: 1
+ t.string :status, null: false, default: 'under_construction'
+ t.datetime :construction_start
+ t.datetime :construction_end
+ t.datetime :demolition_end
+
+ t.timestamps
+ end
+
+ add_index :buildings, %i[building_type_id star_system_id], name: 'index_buildings_on_building_type_and_star_system'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c878c84..598f6ce 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@@ -10,44 +12,73 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2025_05_18_141932) do
- create_table "empires", force: :cascade do |t|
- t.string "name"
- t.integer "credits", default: 1000
- t.integer "minerals", default: 500
- t.integer "energy", default: 500
- t.integer "food", default: 500
- t.integer "user_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.integer "tax_rate", default: 20
- t.index ["user_id"], name: "index_empires_on_user_id", unique: true
+ActiveRecord::Schema[7.1].define(version: 20_250_521_011_436) do
+ create_table 'building_types', force: :cascade do |t|
+ t.string 'key', null: false
+ t.string 'name', null: false
+ t.text 'description', null: false
+ t.boolean 'unique_per_system', default: false
+ t.integer 'max_level', default: 1, null: false
+ t.json 'level_data', default: {}, null: false
+ t.json 'prerequisites', default: {}, null: false
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ end
+
+ create_table 'buildings', force: :cascade do |t|
+ t.integer 'building_type_id', null: false
+ t.integer 'star_system_id', null: false
+ t.integer 'level', default: 1, null: false
+ t.string 'status', default: 'under_construction', null: false
+ t.datetime 'construction_start'
+ t.datetime 'construction_end'
+ t.datetime 'demolition_end'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.index %w[building_type_id star_system_id], name: 'index_buildings_on_building_type_and_star_system'
+ t.index ['building_type_id'], name: 'index_buildings_on_building_type_id'
+ t.index ['star_system_id'], name: 'index_buildings_on_star_system_id'
+ end
+
+ create_table 'empires', force: :cascade do |t|
+ t.string 'name'
+ t.integer 'credits', default: 1000
+ t.integer 'minerals', default: 500
+ t.integer 'energy', default: 500
+ t.integer 'food', default: 500
+ t.integer 'user_id', null: false
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.integer 'tax_rate', default: 20
+ t.index ['user_id'], name: 'index_empires_on_user_id', unique: true
end
- create_table "star_systems", force: :cascade do |t|
- t.string "name"
- t.string "system_type"
- t.integer "max_population"
- t.integer "current_population", default: 10
- t.integer "max_buildings"
- t.integer "loyalty", default: 100
- t.integer "empire_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["empire_id"], name: "index_star_systems_on_empire_id"
+ create_table 'star_systems', force: :cascade do |t|
+ t.string 'name'
+ t.string 'system_type'
+ t.integer 'max_population'
+ t.integer 'current_population', default: 10
+ t.integer 'max_buildings'
+ t.integer 'loyalty', default: 100
+ t.integer 'empire_id', null: false
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.index ['empire_id'], name: 'index_star_systems_on_empire_id'
end
- create_table "users", force: :cascade do |t|
- t.string "email"
- t.string "password_digest"
- t.boolean "admin"
- t.string "username"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["email"], name: "index_users_on_email", unique: true
- t.index ["username"], name: "index_users_on_username", unique: true
+ create_table 'users', force: :cascade do |t|
+ t.string 'email'
+ t.string 'password_digest'
+ t.boolean 'admin'
+ t.string 'username'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.index ['email'], name: 'index_users_on_email', unique: true
+ t.index ['username'], name: 'index_users_on_username', unique: true
end
- add_foreign_key "empires", "users"
- add_foreign_key "star_systems", "empires"
+ add_foreign_key 'buildings', 'building_types'
+ add_foreign_key 'buildings', 'star_systems'
+ add_foreign_key 'empires', 'users'
+ add_foreign_key 'star_systems', 'empires'
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 4fbd6ed..af568f0 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
@@ -7,3 +9,115 @@
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end
+
+# Create building types
+building_types = [
+ {
+ key: 'government_administration',
+ name: 'Government Administration',
+ description: 'A central administrative complex that improves tax collection efficiency.',
+ unique_per_system: true,
+ max_level: 5,
+ level_data: {
+ '1' => {
+ construction_time: 8.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 200, minerals: 100, energy: 125 },
+ effects: { tax_modifier: 0.05 }
+ },
+ '2' => {
+ construction_time: 12.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 400, minerals: 200, energy: 250 },
+ effects: { tax_modifier: 0.10 }
+ },
+ '3' => {
+ construction_time: 16.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 800, minerals: 400, energy: 500 },
+ effects: { tax_modifier: 0.15 }
+ }
+ },
+ prerequisites: {}
+ },
+ {
+ key: 'mining_facility',
+ name: 'Mining Facility',
+ description: "Extracts valuable minerals from the planet's crust.",
+ unique_per_system: false,
+ max_level: 3,
+ level_data: {
+ '1' => {
+ construction_time: 6.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 150, minerals: 50, energy: 100 },
+ effects: { mineral_production: 10 }
+ },
+ '2' => {
+ construction_time: 10.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 300, minerals: 100, energy: 200 },
+ effects: { mineral_production: 25 }
+ }
+ },
+ prerequisites: {}
+ },
+ {
+ key: 'power_plant',
+ name: 'Power Plant',
+ description: "Generates energy for the star system's infrastructure.",
+ unique_per_system: false,
+ max_level: 3,
+ level_data: {
+ '1' => {
+ construction_time: 7.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 180, minerals: 80, energy: 50 },
+ effects: { energy_production: 15 }
+ },
+ '2' => {
+ construction_time: 11.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 360, minerals: 160, energy: 100 },
+ effects: { energy_production: 35 }
+ }
+ },
+ prerequisites: {}
+ },
+ {
+ key: 'research_lab',
+ name: 'Research Laboratory',
+ description: 'Advances scientific knowledge and technological capabilities.',
+ unique_per_system: true,
+ max_level: 4,
+ level_data: {
+ '1' => {
+ construction_time: 10.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 300, minerals: 150, energy: 200 },
+ effects: { research_points: 5 }
+ },
+ '2' => {
+ construction_time: 14.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: { credits: 600, minerals: 300, energy: 400 },
+ effects: { research_points: 12 }
+ }
+ },
+ prerequisites: {}
+ }
+]
+
+# Create or update building types
+building_types.each do |building_type_data|
+ BuildingType.find_or_create_by!(key: building_type_data[:key]) do |bt|
+ bt.name = building_type_data[:name]
+ bt.description = building_type_data[:description]
+ bt.unique_per_system = building_type_data[:unique_per_system]
+ bt.max_level = building_type_data[:max_level]
+ bt.level_data = building_type_data[:level_data]
+ bt.prerequisites = building_type_data[:prerequisites]
+ end
+end
+
+puts "✅ Created #{building_types.length} building types"
diff --git a/spec/factories/building_types.rb b/spec/factories/building_types.rb
new file mode 100644
index 0000000..92f70fa
--- /dev/null
+++ b/spec/factories/building_types.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :building_type do
+ key { 'government_administration' }
+ name { 'Government Administration Building' }
+ description { 'A central administrative complex that improves tax collection efficiency.' }
+ unique_per_system { true }
+ max_level { 5 }
+ level_data do
+ {
+ '1' => {
+ construction_time: 8.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: {
+ credits: 200,
+ minerals: 100,
+ energy: 125
+ },
+ effects: {
+ tax_modifier: 0.05
+ }
+ },
+ '2' => {
+ construction_time: 12.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: {
+ credits: 400,
+ minerals: 200,
+ energy: 250
+ },
+ effects: {
+ tax_modifier: 0.10
+ }
+ }
+ }
+ end
+ prerequisites { {} }
+ end
+end
diff --git a/spec/factories/buildings.rb b/spec/factories/buildings.rb
new file mode 100644
index 0000000..222fac6
--- /dev/null
+++ b/spec/factories/buildings.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :building do
+ association :building_type
+ association :star_system
+ level { 1 }
+ status { 'operational' }
+ construction_start { 10.hours.ago }
+ construction_end { 2.hours.ago }
+ demolition_end { nil }
+
+ trait :under_construction do
+ status { 'under_construction' }
+ construction_start { 2.hours.ago }
+ construction_end { 6.hours.from_now }
+ end
+
+ trait :being_demolished do
+ status { 'being_demolished' }
+ demolition_end { 1.hour.from_now }
+ end
+ end
+end
diff --git a/spec/factories/empires.rb b/spec/factories/empires.rb
index 2053eb4..c45347d 100644
--- a/spec/factories/empires.rb
+++ b/spec/factories/empires.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
FactoryBot.define do
factory :empire do
- name { "#{Faker::Space.galaxy} #{Faker::Number.between(from: 1, to: 10000)}" }
+ name { "#{Faker::Space.galaxy} #{Faker::Number.between(from: 1, to: 10_000)}" }
credits { 1000 }
minerals { 500 }
energy { 500 }
diff --git a/spec/factories/star_systems.rb b/spec/factories/star_systems.rb
index 5c33243..2d00bbf 100644
--- a/spec/factories/star_systems.rb
+++ b/spec/factories/star_systems.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
FactoryBot.define do
factory :star_system do
name { Faker::Space.star }
@@ -8,4 +10,4 @@
loyalty { 100 }
association :empire
end
-end
\ No newline at end of file
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 4a7b14c..282777d 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
FactoryBot.define do
factory :user do
email { Faker::Internet.unique.email }
username { Faker::Internet.username }
- password { "password123" }
+ password { 'password123' }
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/buildings/building_construction_spec.rb b/spec/features/buildings/building_construction_spec.rb
new file mode 100644
index 0000000..ccb33ef
--- /dev/null
+++ b/spec/features/buildings/building_construction_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+# spec/features/buildings/building_construction_spec.rb
+require 'rails_helper'
+
+RSpec.describe 'Building Construction', type: :feature do
+ let(:user) { create(:user) }
+ let(:empire) { create(:empire, user: user, credits: 1000, minerals: 500, energy: 500) }
+ let(:star_system) { create(:star_system, empire: empire, name: 'Alpha Centauri') }
+ let!(:building_type) do
+ create(:building_type, key: 'government_administration',
+ name: 'Government Administration',
+ description: 'Increases tax revenue by 5%',
+ unique_per_system: true,
+ level_data: {
+ '1' => {
+ construction_time: 8.hours.to_i,
+ demolition_time: 1.hour.to_i,
+ cost: {
+ credits: 200,
+ minerals: 100,
+ energy: 125
+ },
+ effects: {
+ tax_modifier: 0.05
+ }
+ }
+ })
+ end
+
+ before do
+ # Log in the user
+ visit login_path
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
+ end
+
+ scenario 'User can see available buildings on the star system page' do
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_content('Available Buildings')
+ expect(page).to have_content('Government Administration')
+ expect(page).to have_content('Increases tax revenue by 5%')
+ expect(page).to have_content('Cost: 200 Credits, 100 Minerals, 125 Energy')
+ end
+
+ scenario 'User can start construction of a building', js: true do
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_button('Build')
+
+ # Debug: Check if the hidden fields exist
+ expect(page).to have_field('building[building_type_id]', type: 'hidden')
+ expect(page).to have_field('building[star_system_id]', type: 'hidden')
+
+ # Get the initial count
+ initial_count = Building.count
+ puts "Initial building count: #{initial_count}"
+
+ # Click Build button to open modal
+ click_button 'Build'
+
+ # Modal should be visible
+ expect(page).to have_content('Confirm Building Construction')
+ expect(page).to have_content('Government Administration')
+ expect(page).to have_content('200 Credits')
+ expect(page).to have_content('100 Minerals')
+ expect(page).to have_content('125 Energy')
+
+ # Click Confirm Construction button in the modal
+ within('#buildModal') do
+ click_button 'Confirm Construction'
+ end
+
+ # Debug: Check if form was submitted
+ puts "Current URL after form submission: #{page.current_url}"
+ puts "Page has success message: #{page.has_content?('Building construction started')}"
+ puts "Page has error message: #{page.has_content?('Error')}"
+
+ # Debug: Check the building count after form submission
+ puts "Building count after form submission: #{Building.count}"
+
+ # Check that the building was created
+ expect(Building.count).to eq(initial_count + 1)
+
+ expect(page).to have_content('Building construction started')
+
+ # Check resources were deducted
+ expect(empire.reload.credits).to eq(800)
+ expect(empire.minerals).to eq(400)
+ expect(empire.energy).to eq(375)
+
+ # Check building exists and is under construction
+ building = Building.last
+ expect(building.building_type).to eq(building_type)
+ expect(building.star_system).to eq(star_system)
+ expect(building.status).to eq('under_construction')
+ expect(building.level).to eq(1)
+ expect(building.construction_start).not_to be_nil
+ expect(building.construction_end).not_to be_nil
+ end
+
+ scenario "User cannot build if they don't have enough resources" do
+ # Update empire to have insufficient resources
+ empire.update(credits: 100, minerals: 50, energy: 50)
+
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_button('Insufficient resources', disabled: true)
+ expect(page).to have_content('Insufficient resources')
+ end
+
+ scenario 'User cannot build if the star system is at max buildings' do
+ # Update star system to be at max buildings
+ star_system.update(max_buildings: 1)
+ create(:building, star_system: star_system, building_type: building_type, status: 'operational')
+
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_button('Already built', disabled: true)
+ expect(page).to have_content('Already built')
+ end
+
+ scenario 'User cannot build a unique building twice' do
+ # Create the building first
+ create(:building, star_system: star_system, building_type: building_type, status: 'operational')
+
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_content('Already built')
+ expect(page).not_to have_button('Build')
+ end
+
+ scenario 'User can see buildings under construction' do
+ # Create a building under construction
+ create(:building, :under_construction,
+ star_system: star_system,
+ building_type: building_type,
+ construction_start: 2.hours.ago,
+ construction_end: 6.hours.from_now)
+
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_content('Buildings Under Construction')
+ expect(page).to have_content('Government Administration')
+ expect(page).to have_content('Construction Progress')
+
+ # Check for progress bar (25% complete)
+ expect(page).to have_css(".progress-bar[style*='width: 25%']")
+ end
+
+ scenario 'User can see operational buildings' do
+ # Create a completed building
+ create(:building, star_system: star_system,
+ building_type: building_type, status: 'operational')
+
+ visit edit_star_system_path(star_system)
+
+ expect(page).to have_content('Operational Buildings')
+ expect(page).to have_content('Government Administration')
+ expect(page).to have_content('Increases tax revenue by 5%')
+ expect(page).to have_button('Demolish')
+ end
+
+ scenario 'User can demolish a building' do
+ # Create a completed building
+ building = create(:building, star_system: star_system,
+ building_type: building_type, status: 'operational')
+
+ visit edit_star_system_path(star_system)
+
+ expect do
+ click_button 'Demolish'
+ end.to change { building.reload.status }.from('operational').to('being_demolished')
+
+ expect(page).to have_content('Building demolition started')
+ expect(page).to have_content('Buildings Being Demolished')
+ expect(page).to have_content('Government Administration')
+ end
+end
diff --git a/spec/features/dashboards/dashboard_display_spec.rb b/spec/features/dashboards/dashboard_display_spec.rb
index 66015b3..d52bead 100644
--- a/spec/features/dashboards/dashboard_display_spec.rb
+++ b/spec/features/dashboards/dashboard_display_spec.rb
@@ -1,37 +1,39 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "Empire Dashboard", type: :feature do
+RSpec.describe 'Empire Dashboard', type: :feature do
let(:user) { create(:user) }
- let!(:empire) { create(:empire, name: "Test Empire", user: user) }
- let!(:star_system) { create(:star_system, name: "Alpha Centauri", system_type: "terrestrial", empire: empire) }
+ let!(:empire) { create(:empire, name: 'Test Empire', user: user) }
+ let!(:star_system) { create(:star_system, name: 'Alpha Centauri', system_type: 'terrestrial', empire: empire) }
before do
# Log in the user
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_button "Log in"
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
visit dashboard_path
end
- it "displays the empire name" do
- expect(page).to have_content("Test Empire")
+ it 'displays the empire name' do
+ expect(page).to have_content('Test Empire')
end
- it "displays empire resources" do
- expect(page).to have_content("Credits")
+ it 'displays empire resources' do
+ expect(page).to have_content('Credits')
expect(page).to have_content(empire.credits.to_s)
- expect(page).to have_content("Minerals")
+ expect(page).to have_content('Minerals')
expect(page).to have_content(empire.minerals.to_s)
- expect(page).to have_content("Energy")
+ expect(page).to have_content('Energy')
expect(page).to have_content(empire.energy.to_s)
- expect(page).to have_content("Food")
+ expect(page).to have_content('Food')
expect(page).to have_content(empire.food.to_s)
end
- it "displays star systems" do
- expect(page).to have_content("Star Systems")
- expect(page).to have_content("Alpha Centauri")
- expect(page).to have_content("terrestrial".humanize)
+ it 'displays star systems' do
+ expect(page).to have_content('Star Systems')
+ expect(page).to have_content('Alpha Centauri')
+ expect(page).to have_content('terrestrial'.humanize)
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/empires/manage_empire_spec.rb b/spec/features/empires/manage_empire_spec.rb
index 14b3974..e275b5f 100644
--- a/spec/features/empires/manage_empire_spec.rb
+++ b/spec/features/empires/manage_empire_spec.rb
@@ -1,76 +1,78 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "Manage Empire", type: :feature do
+RSpec.describe 'Manage Empire', type: :feature do
let(:user) { create(:user) }
- let!(:empire) { create(:empire, name: "Test Empire", user: user, tax_rate: 20) }
- let!(:star_system) { create(:star_system, name: "Alpha Centauri", system_type: "terrestrial", empire: empire) }
+ let!(:empire) { create(:empire, name: 'Test Empire', user: user, tax_rate: 20) }
+ let!(:star_system) { create(:star_system, name: 'Alpha Centauri', system_type: 'terrestrial', empire: empire) }
before do
# Log in the user
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_button "Log in"
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
end
- scenario "User can see tax rate on dashboard" do
+ scenario 'User can see tax rate on dashboard' do
visit dashboard_path
-
- expect(page).to have_content("Tax Rate")
- expect(page).to have_content("20%")
+
+ expect(page).to have_content('Tax Rate')
+ expect(page).to have_content('20%')
end
- scenario "User can access empire management page" do
+ scenario 'User can access empire management page' do
visit dashboard_path
-
- expect(page).to have_link("Manage Empire")
- click_link "Manage Empire"
-
+
+ expect(page).to have_link('Manage Empire')
+ click_link 'Manage Empire'
+
expect(current_path).to eq(edit_empire_path(empire))
- expect(page).to have_content("Manage Your Empire")
- expect(page).to have_field("Name", with: "Test Empire")
- expect(page).to have_field("empire[tax_rate]", with: "20")
+ expect(page).to have_content('Manage Your Empire')
+ expect(page).to have_field('Name', with: 'Test Empire')
+ expect(page).to have_field('empire[tax_rate]', with: '20')
end
- scenario "User can update empire name" do
+ scenario 'User can update empire name' do
visit edit_empire_path(empire)
-
- fill_in "Name", with: "Galactic Federation"
- click_button "Update Empire"
-
+
+ fill_in 'Name', with: 'Galactic Federation'
+ click_button 'Update Empire'
+
expect(current_path).to eq(dashboard_path)
- expect(page).to have_content("Empire updated successfully")
- expect(page).to have_content("Galactic Federation")
- expect(page).not_to have_content("Test Empire")
+ expect(page).to have_content('Empire updated successfully')
+ expect(page).to have_content('Galactic Federation')
+ expect(page).not_to have_content('Test Empire')
end
- scenario "User can update tax rate" do
+ scenario 'User can update tax rate' do
visit edit_empire_path(empire)
- fill_in "empire[tax_rate]", with: "30"
- click_button "Update Empire"
-
+ fill_in 'empire[tax_rate]', with: '30'
+ click_button 'Update Empire'
+
expect(current_path).to eq(dashboard_path)
- expect(page).to have_content("Empire updated successfully")
- expect(page).to have_content("Tax Rate")
- expect(page).to have_content("30%")
- expect(page).not_to have_content("20%")
+ expect(page).to have_content('Empire updated successfully')
+ expect(page).to have_content('Tax Rate')
+ expect(page).to have_content('30%')
+ expect(page).not_to have_content('20%')
end
- scenario "User cannot update with invalid tax rate" do
+ scenario 'User cannot update with invalid tax rate' do
visit edit_empire_path(empire)
-
- fill_in "empire[tax_rate]", with: "101"
- click_button "Update Empire"
-
- expect(page).to have_content("Tax rate must be less than or equal to 100")
+
+ fill_in 'empire[tax_rate]', with: '101'
+ click_button 'Update Empire'
+
+ expect(page).to have_content('Tax rate must be less than or equal to 100')
end
- scenario "User cannot update with invalid name" do
+ scenario 'User cannot update with invalid name' do
visit edit_empire_path(empire)
-
- fill_in "Name", with: ""
- click_button "Update Empire"
-
+
+ fill_in 'Name', with: ''
+ click_button 'Update Empire'
+
expect(page).to have_content("Name can't be blank")
end
end
diff --git a/spec/features/home_page_spec.rb b/spec/features/home_page_spec.rb
index 90987d9..6f03ef3 100644
--- a/spec/features/home_page_spec.rb
+++ b/spec/features/home_page_spec.rb
@@ -1,33 +1,35 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "Home Page", type: :feature do
+RSpec.describe 'Home Page', type: :feature do
before do
visit root_path
end
- it "displays the game title" do
- expect(page).to have_content("Voidfront Realms Elite")
+ it 'displays the game title' do
+ expect(page).to have_content('Voidfront Realms Elite')
end
- it "displays the welcome message" do
- expect(page).to have_content("Welcome to Voidfront Realms Elite")
+ it 'displays the welcome message' do
+ expect(page).to have_content('Welcome to Voidfront Realms Elite')
end
- it "displays the game description" do
- expect(page).to have_content("Manage your own star empire")
+ it 'displays the game description' do
+ expect(page).to have_content('Manage your own star empire')
end
it "has a 'Play Now' button" do
- expect(page).to have_link("Play Now")
+ expect(page).to have_link('Play Now')
end
it "has a 'Learn More' button" do
- expect(page).to have_link("Learn More")
+ expect(page).to have_link('Learn More')
end
- it "displays game features" do
- expect(page).to have_content("Resource Management")
- expect(page).to have_content("Fleet Construction")
- expect(page).to have_content("Research & Technology")
+ it 'displays game features' do
+ expect(page).to have_content('Resource Management')
+ expect(page).to have_content('Fleet Construction')
+ expect(page).to have_content('Research & Technology')
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/star_systems/star_system_administration_spec.rb b/spec/features/star_systems/star_system_administration_spec.rb
index 519f015..779e7e8 100644
--- a/spec/features/star_systems/star_system_administration_spec.rb
+++ b/spec/features/star_systems/star_system_administration_spec.rb
@@ -1,56 +1,58 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "Star System Administration", type: :feature do
+RSpec.describe 'Star System Administration', type: :feature do
# First user
let(:user) { create(:user) }
let!(:empire) { create(:empire, user: user) }
- let!(:star_system) { create(:star_system, name: "Alpha Centauri", empire: empire) }
+ let!(:star_system) { create(:star_system, name: 'Alpha Centauri', empire: empire) }
# Second user and empire for authorization tests
let(:other_user) { create(:user) }
let!(:other_empire) { create(:empire, user: other_user) }
- let!(:other_star_system) { create(:star_system, name: "Sirius", empire: other_empire) }
+ let!(:other_star_system) { create(:star_system, name: 'Sirius', empire: other_empire) }
before do
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_on "Log in"
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_on 'Log in'
end
it "User can see 'Administer' button for each star system on dashboard" do
visit dashboard_path
- expect(page).to have_link("Administer")
+ expect(page).to have_link('Administer')
end
- it "can access the edit page for their own star system" do
+ it 'can access the edit page for their own star system' do
visit dashboard_path
- click_link "Administer", match: :first
+ click_link 'Administer', match: :first
expect(current_path).to eq(edit_star_system_path(star_system))
- expect(page).to have_content("Administer Star System")
- expect(page).to have_field("Name", with: "Alpha Centauri")
- expect(page).to_not have_field("Name", with: "Sirius")
+ expect(page).to have_content('Administer Star System')
+ expect(page).to have_field('Name', with: 'Alpha Centauri')
+ expect(page).to_not have_field('Name', with: 'Sirius')
end
- it "can update the star system name" do
+ it 'can update the star system name' do
visit edit_star_system_path(star_system)
- fill_in "Name", with: "Polaris"
- click_on "Update Star System"
+ fill_in 'Name', with: 'Polaris'
+ click_on 'Update Star System'
expect(current_path).to eq(edit_star_system_path(star_system))
- expect(page).to have_content("Star system updated successfully")
- expect(star_system.reload.name).to eq("Polaris")
+ expect(page).to have_content('Star system updated successfully')
+ expect(star_system.reload.name).to eq('Polaris')
end
- it "cannot update star system with a blank name" do
+ it 'cannot update star system with a blank name' do
visit edit_star_system_path(star_system)
- fill_in "Name", with: ""
- click_on "Update Star System"
+ fill_in 'Name', with: ''
+ click_on 'Update Star System'
expect(page).to have_content("Name can't be blank")
end
diff --git a/spec/features/user_authentication/user_login_spec.rb b/spec/features/user_authentication/user_login_spec.rb
index f8291f9..ce99cac 100644
--- a/spec/features/user_authentication/user_login_spec.rb
+++ b/spec/features/user_authentication/user_login_spec.rb
@@ -1,101 +1,103 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "User Login", type: :feature do
- let(:user) { create(:user, email: "test@example.com", username: "testuser", password: "password123") }
+RSpec.describe 'User Login', type: :feature do
+ let(:user) { create(:user, email: 'test@example.com', username: 'testuser', password: 'password123') }
- scenario "User can see the login page" do
+ scenario 'User can see the login page' do
visit login_path
- expect(page).to have_content("Log in to Your Account")
- expect(page).to have_field("Email")
- expect(page).to have_field("Password")
- expect(page).to have_button("Log in")
+ expect(page).to have_content('Log in to Your Account')
+ expect(page).to have_field('Email')
+ expect(page).to have_field('Password')
+ expect(page).to have_button('Log in')
end
- scenario "User can log in with valid credentials" do
+ scenario 'User can log in with valid credentials' do
user # create the user before visiting the page
-
+
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_button "Log in"
-
- expect(page).to have_content("Login Successful")
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
+
+ expect(page).to have_content('Login Successful')
expect(page).to have_content("Welcome, #{user.username}")
expect(current_path).to eq(root_path)
end
- scenario "User cannot log in with invalid email" do
+ scenario 'User cannot log in with invalid email' do
visit login_path
- fill_in "Email", with: "wrong@example.com"
- fill_in "Password", with: "password123"
- click_button "Log in"
-
- expect(page).to have_content("Invalid email or password")
- expect(page).not_to have_content("Welcome")
+ fill_in 'Email', with: 'wrong@example.com'
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
+
+ expect(page).to have_content('Invalid email or password')
+ expect(page).not_to have_content('Welcome')
end
- scenario "User cannot log in with invalid password" do
+ scenario 'User cannot log in with invalid password' do
user # create the user
-
+
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "wrongpassword"
- click_button "Log in"
-
- expect(page).to have_content("Invalid email or password")
- expect(page).not_to have_content("Welcome")
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'wrongpassword'
+ click_button 'Log in'
+
+ expect(page).to have_content('Invalid email or password')
+ expect(page).not_to have_content('Welcome')
end
- xscenario "User is remembered after checking remember me" do
+ xscenario 'User is remembered after checking remember me' do
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- check "Remember me"
- click_button "Log in"
-
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ check 'Remember me'
+ click_button 'Log in'
+
expect(page).to have_content("Welcome, #{user.username}")
-
+
Capybara.reset_session!
-
+
visit root_path
save_and_open_page
expect(page).to have_content("Welcome, #{user.username}")
end
- scenario "User is not remembered without checking remember me" do
+ scenario 'User is not remembered without checking remember me' do
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
# Don't check "Remember me"
- click_button "Log in"
-
+ click_button 'Log in'
+
expect(page).to have_content("Welcome, #{user.username}")
-
+
# Clear the session cookies
Capybara.reset_session!
-
+
# Visit the site again - user should NOT be logged in
visit root_path
expect(page).not_to have_content("Welcome, #{user.username}")
- expect(page).to have_link("Login")
+ expect(page).to have_link('Login')
end
- scenario "Remembered user is properly logged out" do
+ scenario 'Remembered user is properly logged out' do
# Login with remember me
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- check "Remember me"
- click_button "Log in"
-
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ check 'Remember me'
+ click_button 'Log in'
+
# Now log out
- click_link "Logout"
- expect(page).to have_content("You have been logged out")
-
+ click_link 'Logout'
+ expect(page).to have_content('You have been logged out')
+
# Even after a "browser restart" the user should be logged out
Capybara.reset_session!
visit root_path
expect(page).not_to have_content("Welcome, #{user.username}")
- expect(page).to have_link("Login")
+ expect(page).to have_link('Login')
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/user_authentication/user_logout_spec.rb b/spec/features/user_authentication/user_logout_spec.rb
index 90eea9e..e7020d4 100644
--- a/spec/features/user_authentication/user_logout_spec.rb
+++ b/spec/features/user_authentication/user_logout_spec.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "User Logout", type: :feature do
- let(:user) { create(:user, email: "test@example.com", username: "testuser", password: "password123") }
+RSpec.describe 'User Logout', type: :feature do
+ let(:user) { create(:user, email: 'test@example.com', username: 'testuser', password: 'password123') }
- scenario "User can log out" do
+ scenario 'User can log out' do
# Log in first
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_button "Log in"
-
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
+
# Verify logged in
expect(page).to have_content("Welcome, #{user.username}")
-
+
# Log out
- click_link "Logout"
-
+ click_link 'Logout'
+
# Verify logged out
- expect(page).to have_content("You have been logged out")
- expect(page).to have_link("Login")
+ expect(page).to have_content('You have been logged out')
+ expect(page).to have_link('Login')
expect(page).not_to have_content("Welcome, #{user.username}")
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/user_authentication/user_registration_spec.rb b/spec/features/user_authentication/user_registration_spec.rb
index 4cc0b67..17f66e0 100644
--- a/spec/features/user_authentication/user_registration_spec.rb
+++ b/spec/features/user_authentication/user_registration_spec.rb
@@ -1,63 +1,65 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "User Registration", type: :feature do
- scenario "User successfully registers" do
+RSpec.describe 'User Registration', type: :feature do
+ scenario 'User successfully registers' do
visit new_user_path
- fill_in "Email", with: "test@example.com"
- fill_in "Username", with: "testuser"
- fill_in "Password", with: "password123"
- fill_in "Password confirmation", with: "password123"
+ fill_in 'Email', with: 'test@example.com'
+ fill_in 'Username', with: 'testuser'
+ fill_in 'Password', with: 'password123'
+ fill_in 'Password confirmation', with: 'password123'
- expect {
- click_button "Register"
- }.to change(User, :count).by(1)
+ expect do
+ click_button 'Register'
+ end.to change(User, :count).by(1)
- expect(page).to have_content("Registration Successful")
+ expect(page).to have_content('Registration Successful')
expect(current_path).to eq(dashboard_path)
end
- scenario "User attempts to register with invalid information" do
+ scenario 'User attempts to register with invalid information' do
visit new_user_path
-
+
# Leave fields blank to trigger validation errors
- click_button "Register"
-
+ click_button 'Register'
+
expect(page).to have_content("Email can't be blank")
expect(page).to have_content("Username can't be blank")
expect(page).to have_content("Password can't be blank")
-
+
expect(User.count).to eq(0)
end
- scenario "User attempts to register with mismatched passwords" do
+ scenario 'User attempts to register with mismatched passwords' do
visit new_user_path
-
- fill_in "Email", with: "test@example.com"
- fill_in "Username", with: "testuser"
- fill_in "Password", with: "password123"
- fill_in "Password confirmation", with: "differentpassword"
-
- click_button "Register"
-
+
+ fill_in 'Email', with: 'test@example.com'
+ fill_in 'Username', with: 'testuser'
+ fill_in 'Password', with: 'password123'
+ fill_in 'Password confirmation', with: 'differentpassword'
+
+ click_button 'Register'
+
expect(page).to have_content("Password confirmation doesn't match")
expect(User.count).to eq(0)
end
- scenario "User attempts to register with an email that is already taken" do
+ scenario 'User attempts to register with an email that is already taken' do
# Create a user first
- User.create(email: "taken@example.com", username: "existinguser", password: "password123")
-
+ User.create(email: 'taken@example.com', username: 'existinguser', password: 'password123')
+
visit new_user_path
-
- fill_in "Email", with: "taken@example.com"
- fill_in "Username", with: "newuser"
- fill_in "Password", with: "password123"
- fill_in "Password confirmation", with: "password123"
-
- click_button "Register"
-
- expect(page).to have_content("Email has already been taken")
+
+ fill_in 'Email', with: 'taken@example.com'
+ fill_in 'Username', with: 'newuser'
+ fill_in 'Password', with: 'password123'
+ fill_in 'Password confirmation', with: 'password123'
+
+ click_button 'Register'
+
+ expect(page).to have_content('Email has already been taken')
expect(User.count).to eq(1)
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/user_authentication/user_registration_with_empire_name_spec.rb b/spec/features/user_authentication/user_registration_with_empire_name_spec.rb
index c5819dc..e1195e5 100644
--- a/spec/features/user_authentication/user_registration_with_empire_name_spec.rb
+++ b/spec/features/user_authentication/user_registration_with_empire_name_spec.rb
@@ -1,42 +1,44 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "User Registration with Empire Name", type: :feature do
- scenario "User successfully registers with an empire name" do
+RSpec.describe 'User Registration with Empire Name', type: :feature do
+ scenario 'User successfully registers with an empire name' do
visit new_user_path
- fill_in "Email", with: "test@example.com"
- fill_in "Username", with: "testuser"
- fill_in "Password", with: "password123"
- fill_in "Password confirmation", with: "password123"
- fill_in "Empire name (optional)", with: "Galactic Federation"
+ fill_in 'Email', with: 'test@example.com'
+ fill_in 'Username', with: 'testuser'
+ fill_in 'Password', with: 'password123'
+ fill_in 'Password confirmation', with: 'password123'
+ fill_in 'Empire name (optional)', with: 'Galactic Federation'
- expect {
- click_button "Register"
- }.to change(User, :count).by(1)
- .and change(Empire, :count).by(1)
+ expect do
+ click_button 'Register'
+ end.to change(User, :count).by(1)
+ .and change(Empire, :count).by(1)
# Check redirect to dashboard
expect(current_path).to eq(dashboard_path)
- expect(page).to have_content("Registration Successful")
- expect(page).to have_content("Galactic Federation")
+ expect(page).to have_content('Registration Successful')
+ expect(page).to have_content('Galactic Federation')
end
- scenario "User attempts to register with an empire name that is already taken" do
+ scenario 'User attempts to register with an empire name that is already taken' do
# Create an existing empire with a name
existing_user = create(:user)
- create(:empire, name: "Galactic Federation", user: existing_user)
-
+ create(:empire, name: 'Galactic Federation', user: existing_user)
+
visit new_user_path
-
- fill_in "Email", with: "new@example.com"
- fill_in "Username", with: "newuser"
- fill_in "Password", with: "password123"
- fill_in "Password confirmation", with: "password123"
- fill_in "Empire name (optional)", with: "Galactic Federation"
-
- click_button "Register"
-
- expect(page).to have_content("Name has already been taken")
+
+ fill_in 'Email', with: 'new@example.com'
+ fill_in 'Username', with: 'newuser'
+ fill_in 'Password', with: 'password123'
+ fill_in 'Password confirmation', with: 'password123'
+ fill_in 'Empire name (optional)', with: 'Galactic Federation'
+
+ click_button 'Register'
+
+ expect(page).to have_content('Name has already been taken')
expect(User.count).to eq(1) # Just the existing user
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/user_authorization/user_authorization_spec.rb b/spec/features/user_authorization/user_authorization_spec.rb
index 5624989..c804084 100644
--- a/spec/features/user_authorization/user_authorization_spec.rb
+++ b/spec/features/user_authorization/user_authorization_spec.rb
@@ -1,49 +1,51 @@
+# frozen_string_literal: true
+
require 'rails_helper'
-RSpec.describe "Authorzation", type: :feature do
+RSpec.describe 'Authorzation', type: :feature do
let(:user) { create(:user) }
- let!(:empire) { create(:empire, name: "Test Empire", user: user) }
- let!(:star_system) { create(:star_system, name: "Alpha Centauri", system_type: "terrestrial", empire: empire) }
-
- describe "Accessing protected resources" do
- context "when user is not authenticated" do
- it "redirects to login page when trying to access dashboard" do
+ let!(:empire) { create(:empire, name: 'Test Empire', user: user) }
+ let!(:star_system) { create(:star_system, name: 'Alpha Centauri', system_type: 'terrestrial', empire: empire) }
+
+ describe 'Accessing protected resources' do
+ context 'when user is not authenticated' do
+ it 'redirects to login page when trying to access dashboard' do
visit dashboard_path
expect(current_path).to eq(root_path)
- expect(page).to have_content("You must be logged in to access this page.")
+ expect(page).to have_content('You must be logged in to access this page.')
end
end
- context "when user is authenticated" do
+ context 'when user is authenticated' do
before do
visit login_path
- fill_in "Email", with: user.email
- fill_in "Password", with: "password123"
- click_button "Log in"
+ fill_in 'Email', with: user.email
+ fill_in 'Password', with: 'password123'
+ click_button 'Log in'
end
- it "alllows access to dashboard" do
+ it 'alllows access to dashboard' do
visit dashboard_path
expect(current_path).to eq(dashboard_path)
- expect(page).to have_content("Dashboard")
+ expect(page).to have_content('Dashboard')
end
end
end
- describe "accessing public resources" do
- it "allows access to home page without authentication" do
+ describe 'accessing public resources' do
+ it 'allows access to home page without authentication' do
visit root_path
expect(current_path).to eq(root_path)
end
-
- it "allows access to login page without authentication" do
+
+ it 'allows access to login page without authentication' do
visit login_path
expect(current_path).to eq(login_path)
end
-
- it "allows access to registration page without authentication" do
+
+ it 'allows access to registration page without authentication' do
visit new_user_path
expect(current_path).to eq(new_user_path)
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/jobs/maintenance_job_spec.rb b/spec/jobs/maintenance_job_spec.rb
index 4f3e820..329c11a 100644
--- a/spec/jobs/maintenance_job_spec.rb
+++ b/spec/jobs/maintenance_job_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe MaintenanceJob, type: :job do
@@ -7,32 +9,81 @@
let!(:empire) { create(:empire, user: user, credits: 1000, tax_rate: 10) }
let!(:star_system) { create(:star_system, empire: empire, current_population: 500) }
let!(:star_system_2) { create(:star_system, empire: empire, current_population: 300) }
+ let!(:building_type) { create(:building_type) }
- describe "#perform" do
+ describe '#perform' do
it 'collects taxes based on population and tax rate' do
- expect {
+ expect do
MaintenanceJob.perform_now(empire.id)
- }.to change { empire.reload.credits }.by(80)
+ end.to change { empire.reload.credits }.by(80)
end
it 'doesnt collect taxes if the tax rate is 0' do
empire.update(tax_rate: 0)
- expect {
+ expect do
MaintenanceJob.perform_now(empire.id)
- }.not_to change { empire.reload.credits }
+ end.not_to(change { empire.reload.credits })
end
end
- describe "population growth during maintenance" do
+ describe 'population growth during maintenance' do
let(:user) { create(:user) }
let(:empire) { create(:empire, user: user, tax_rate: 20) }
- let(:star_system) { create(:star_system, empire: empire, current_population: 500, system_type: "terrestrial") }
-
- it "grows the population of each star system" do
- system = create(:star_system, empire: empire, current_population: 500, system_type: "terrestrial")
- expect {
+ let(:star_system) { create(:star_system, empire: empire, current_population: 500, system_type: 'terrestrial') }
+
+ it 'grows the population of each star system' do
+ system = create(:star_system, empire: empire, current_population: 500, system_type: 'terrestrial')
+ expect do
MaintenanceJob.perform_now(empire.id)
- }.to change { system.reload.current_population }
+ end.to(change { system.reload.current_population })
+ end
+ end
+
+ describe 'handling building status updates' do
+ let(:user) { create(:user) }
+ let(:empire) { create(:empire, user: user) }
+ let(:star_system) { create(:star_system, empire: empire) }
+ let(:building_type) { create(:building_type) }
+
+ it 'completes buildings whose construction time has passed' do
+ building = create(:building,
+ star_system: star_system,
+ building_type: building_type,
+ construction_end: 1.minute.ago)
+
+ MaintenanceJob.perform_now(empire.id)
+
+ expect(building.reload.status).to eq('operational')
+ end
+
+ it "doesn't complete buildings that haven't finished construction" do
+ building = create(:building, :under_construction,
+ star_system: star_system,
+ building_type: building_type,
+ construction_end: 1.hour.from_now)
+
+ MaintenanceJob.perform_now(empire.id)
+
+ expect(building.reload.status).to eq('under_construction')
+ end
+
+ it 'removes buildings that have been demolished' do
+ create(:building, :being_demolished,
+ star_system: star_system,
+ building_type: building_type,
+ demolition_end: 1.minute.ago)
+
+ expect { MaintenanceJob.perform_now(empire.id) }.to change { Building.count }.by(-1)
+ end
+
+ it "doesn't remove buildings still being demolished" do
+ building = create(:building, :being_demolished,
+ star_system: star_system,
+ building_type: building_type,
+ demolition_end: 1.hour.from_now)
+
+ expect { MaintenanceJob.perform_now(empire.id) }.not_to(change { Building.count })
+ expect(building.reload.status).to eq('being_demolished')
end
end
end
diff --git a/spec/jobs/schedule_maintenance_job_spec.rb b/spec/jobs/schedule_maintenance_job_spec.rb
index 2000fba..872b6f3 100644
--- a/spec/jobs/schedule_maintenance_job_spec.rb
+++ b/spec/jobs/schedule_maintenance_job_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe ScheduleMaintenanceJob, type: :job do
@@ -5,23 +7,23 @@
let!(:empire) { create(:empire) }
let!(:empire2) { create(:empire) }
-
- describe "#perform" do
+
+ describe '#perform' do
it 'enqueues a maintenance job for each empire' do
- expect {
+ expect do
ScheduleMaintenanceJob.perform_now
- }.to change { enqueued_jobs.size }.by(2)
+ end.to change { enqueued_jobs.size }.by(2)
expect(enqueued_jobs.map { |job| job[:args][0] }).to include(empire.id, empire2.id)
- expect(enqueued_jobs.map { |job| job[:job].to_s }).to all(eq "MaintenanceJob")
+ expect(enqueued_jobs.map { |job| job[:job].to_s }).to all(eq 'MaintenanceJob')
end
it 'does not fail if there are no eimpires' do
Empire.destroy_all
- expect {
+ expect do
ScheduleMaintenanceJob.perform_now
- }.not_to raise_error
+ end.not_to raise_error
end
end
end
diff --git a/spec/models/building_spec.rb b/spec/models/building_spec.rb
new file mode 100644
index 0000000..17eadc0
--- /dev/null
+++ b/spec/models/building_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Building, type: :model do
+ # Association Tests
+ it { should belong_to(:building_type) }
+ it { should belong_to(:star_system) }
+
+ # Validation Tests
+ it { should validate_presence_of(:level) }
+ it { should validate_numericality_of(:level).only_integer.is_greater_than(0) }
+ it { should validate_presence_of(:status) }
+ it { should validate_inclusion_of(:status).in_array(%w[under_construction being_demolished operational]) }
+
+ # Factory Tests
+ it 'has a valid factory' do
+ expect(build(:building)).to be_valid
+ end
+
+ it 'has a valid factory for under construction' do
+ expect(build(:building, :under_construction)).to be_valid
+ end
+
+ it 'has a valid factory for being demolished' do
+ expect(build(:building, :being_demolished)).to be_valid
+ end
+
+ # Instance Methods
+ describe '#under_construction?' do
+ it 'returns true if the building is under construction' do
+ building = build(:building, :under_construction)
+ expect(building.under_construction?).to be true
+ end
+
+ it 'returns false if the building is not under construction' do
+ building = build(:building)
+ expect(building.under_construction?).to be false
+ end
+ end
+
+ describe '#operational?' do
+ it 'returns true if the building is operational' do
+ building = build(:building, status: 'operational')
+ expect(building.operational?).to be true
+ end
+
+ it 'returns false if the building is not operational' do
+ building = build(:building, status: 'under_construction')
+ expect(building.operational?).to be false
+ end
+ end
+
+ describe '#being_demolished?' do
+ it 'returns true if the building is being demolished' do
+ building = build(:building, :being_demolished)
+ expect(building.being_demolished?).to be true
+ end
+
+ it 'returns false if the building is not being demolished' do
+ building = build(:building)
+ expect(building.being_demolished?).to be false
+ end
+ end
+
+ describe '#construction_progress_percentage' do
+ it 'returns 100 if construction is complete' do
+ building = build(:building, status: 'operational')
+ expect(building.construction_progress_percentage).to eq(100)
+ end
+
+ it 'calculates the correct percentage for in progress construction' do
+ # set specific times for predictable result
+ start_time = 2.hours.ago
+ end_time = 2.hours.from_now
+
+ building = build(:building, status: 'under_construction',
+ construction_start: start_time,
+ construction_end: end_time)
+
+ allow(Time).to receive(:now).and_return(start_time + 2.hours)
+ expect(building.construction_progress_percentage).to eq(50)
+ end
+ end
+
+ describe '#demolition_progress_percentage' do
+ it 'returns 0 if demolition has not started' do
+ building = build(:building, status: 'operational')
+ expect(building.demolition_progress_percentage).to eq(0)
+ end
+
+ it 'calculates the correct percentage for in progress demolition' do
+ # set up 1 hour demolition process, half complete
+ demolition_start = 30.minutes.ago
+ demolition_end = 30.minutes.from_now
+
+ building = build(:building, :being_demolished, demolition_end: demolition_end)
+
+ # We'll need to stub the created_at since demolition_start comes from updated_at
+ allow(building).to receive(:updated_at).and_return(demolition_start)
+ allow(Time).to receive(:current).and_return(demolition_start + 30.minutes)
+
+ expect(building.demolition_progress_percentage).to eq(50)
+ end
+ end
+
+ describe '#current_effect' do
+ let(:building_type) { create(:building_type) }
+ let(:building) { create(:building, building_type: building_type, level: 1, status: 'operational') }
+
+ it 'returns the correct effect for the current level' do
+ expect(building.current_effect('tax_modifier')).to eq(0.05)
+ end
+
+ it 'returns nil if the building is under construction' do
+ building = build(:building, :under_construction, building_type: building_type)
+ expect(building.current_effect('tax_modifier')).to be_nil
+ end
+
+ it 'returns nil if the building is being demolished' do
+ building = build(:building, :being_demolished, building_type: building_type)
+ expect(building.current_effect('tax_modifier')).to be_nil
+ end
+ end
+
+ describe 'uniqueness validation' do
+ let(:building_type) { create(:building_type, unique_per_system: true) }
+ let(:star_system) { create(:star_system) }
+
+ it 'prevents creating duplicate buildings of unique type in same system' do
+ create(:building, building_type: building_type, star_system: star_system)
+
+ duplicate = build(:building, building_type: building_type, star_system: star_system)
+
+ expect(duplicate).not_to be_valid
+ expect(duplicate.errors[:building_type_id]).to include('already exists in this star system')
+ end
+
+ it 'allows creating of buildings of unique type in different systems' do
+ create(:building, building_type: building_type, star_system: create(:star_system))
+
+ another_system = create(:star_system)
+ another_building = build(:building, building_type: building_type, star_system: another_system)
+
+ expect(another_building).to be_valid
+ end
+
+ it 'allows creating buildings of non-unique type in same system' do
+ non_unique_type = create(:building_type, unique_per_system: false)
+
+ create(:building, building_type: non_unique_type, star_system: star_system)
+ duplicate = build(:building, building_type: non_unique_type, star_system: star_system)
+
+ expect(duplicate).to be_valid
+ end
+ end
+end
diff --git a/spec/models/building_type_spec.rb b/spec/models/building_type_spec.rb
new file mode 100644
index 0000000..cd8814f
--- /dev/null
+++ b/spec/models/building_type_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe BuildingType, type: :model do
+ subject { create(:building_type) }
+ # Validation tests
+ it { should validate_presence_of(:key) }
+ it { should validate_uniqueness_of(:key) }
+ it { should validate_presence_of(:name) }
+ it { should validate_presence_of(:description) }
+ it { should validate_presence_of(:max_level) }
+ it { should validate_numericality_of(:max_level).is_greater_than(0) }
+
+ # Factory test
+ it 'has a valid factory' do
+ expect(build(:building_type)).to be_valid
+ end
+
+ # Instance methods
+ describe '#cost_for_level' do
+ let(:building_type) { create(:building_type) }
+
+ it 'returns the correct cost for a given level' do
+ expect(building_type.cost_for_level(1)).to eq({
+ 'credits' => 200,
+ 'minerals' => 100,
+ 'energy' => 125
+ })
+ end
+ end
+
+ describe '#construction_time_for_level' do
+ let(:building_type) { create(:building_type) }
+
+ it 'returns the correct construction time for a given level' do
+ expect(building_type.construction_time_for_level(1)).to eq(8.hours.to_i)
+ end
+ end
+
+ describe '#effects_for_level' do
+ let(:building_type) { create(:building_type) }
+
+ it 'returns the correct effects for a given level' do
+ expect(building_type.effects_for_level(1)).to eq({
+ 'tax_modifier' => 0.05
+ })
+ end
+ end
+end
diff --git a/spec/models/empire_spec.rb b/spec/models/empire_spec.rb
index 4a9e2b2..0530e58 100644
--- a/spec/models/empire_spec.rb
+++ b/spec/models/empire_spec.rb
@@ -1,32 +1,36 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe Empire, type: :model do
subject { create(:empire) }
-
+
# Association tests
it { should belong_to(:user) }
- it { should have_many(:star_systems)}
-
+ it { should have_many(:star_systems) }
+
# Validation tests
it { should validate_presence_of(:name) }
- it { should validate_uniqueness_of(:name)}
+ it { should validate_uniqueness_of(:name) }
it { should validate_numericality_of(:credits).only_integer.is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:minerals).only_integer.is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:energy).only_integer.is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:food).only_integer.is_greater_than_or_equal_to(0) }
- it { should validate_numericality_of(:tax_rate).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100) }
-
+ it {
+ should validate_numericality_of(:tax_rate).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100)
+ }
+
# Factory test
- it "has a valid factory" do
+ it 'has a valid factory' do
expect(build(:empire)).to be_valid
end
# Default values test
- describe "default values" do
- it "sets default resource values when created without specifying them" do
+ describe 'default values' do
+ it 'sets default resource values when created without specifying them' do
user = create(:user)
- empire = Empire.create(name: "Test Empire", user: user)
-
+ empire = Empire.create(name: 'Test Empire', user: user)
+
expect(empire.credits).to eq(1000)
expect(empire.minerals).to eq(500)
expect(empire.energy).to eq(500)
@@ -36,10 +40,10 @@
end
# Instance method tests
- describe "#resources_summary" do
- it "returns a formatted string of resources" do
+ describe '#resources_summary' do
+ it 'returns a formatted string of resources' do
empire = build(:empire, credits: 1000, minerals: 500, energy: 500, food: 500)
- expect(empire.resources_summary).to eq("Credits: 1000 | Minerals: 500 | Energy: 500 | Food: 500")
+ expect(empire.resources_summary).to eq('Credits: 1000 | Minerals: 500 | Energy: 500 | Food: 500')
end
end
end
diff --git a/spec/models/star_system_spec.rb b/spec/models/star_system_spec.rb
index 4779854..721d3a8 100644
--- a/spec/models/star_system_spec.rb
+++ b/spec/models/star_system_spec.rb
@@ -1,67 +1,74 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe StarSystem, type: :model do
# Association tests
it { should belong_to(:empire) }
-
+ it { should have_many(:buildings) }
+
# Validation tests
it { should validate_presence_of(:name) }
it { should validate_presence_of(:system_type) }
- it { should validate_inclusion_of(:system_type).in_array(%w[terrestrial ocean desert tundra gas_giant asteroid_belt]) }
+ it {
+ should validate_inclusion_of(:system_type).in_array(%w[terrestrial ocean desert tundra gas_giant asteroid_belt])
+ }
it { should validate_numericality_of(:max_population).only_integer.is_greater_than(0) }
it { should validate_numericality_of(:current_population).only_integer.is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:max_buildings).only_integer.is_greater_than(0) }
- it { should validate_numericality_of(:loyalty).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100) }
-
+ it {
+ should validate_numericality_of(:loyalty).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(100)
+ }
+
# Factory test
- it "has a valid factory" do
+ it 'has a valid factory' do
expect(build(:star_system)).to be_valid
end
- describe "#base_growth_rate" do
- it "returns correct growth rate for each system type" do
- expect(build(:star_system, system_type: "terrestrial").base_growth_rate).to eq(0.05)
- expect(build(:star_system, system_type: "ocean").base_growth_rate).to eq(0.04)
- expect(build(:star_system, system_type: "tundra").base_growth_rate).to eq(0.03)
- expect(build(:star_system, system_type: "desert").base_growth_rate).to eq(0.02)
- expect(build(:star_system, system_type: "gas_giant").base_growth_rate).to eq(0.01)
- expect(build(:star_system, system_type: "asteroid_belt").base_growth_rate).to eq(0.005)
+ describe '#base_growth_rate' do
+ it 'returns correct growth rate for each system type' do
+ expect(build(:star_system, system_type: 'terrestrial').base_growth_rate).to eq(0.05)
+ expect(build(:star_system, system_type: 'ocean').base_growth_rate).to eq(0.04)
+ expect(build(:star_system, system_type: 'tundra').base_growth_rate).to eq(0.03)
+ expect(build(:star_system, system_type: 'desert').base_growth_rate).to eq(0.02)
+ expect(build(:star_system, system_type: 'gas_giant').base_growth_rate).to eq(0.01)
+ expect(build(:star_system, system_type: 'asteroid_belt').base_growth_rate).to eq(0.005)
end
end
- describe "#tax_growth_modifier" do
+ describe '#tax_growth_modifier' do
let(:empire) { build(:empire) }
let(:system) { build(:star_system, empire: empire) }
- it "returns bonus modifier for low tax rates" do
+ it 'returns bonus modifier for low tax rates' do
empire.tax_rate = 0
expect(system.tax_growth_modifier).to eq(2.0)
-
+
empire.tax_rate = 20
expect(system.tax_growth_modifier).to be_within(0.01).of(1.4)
end
- it "returns penalty modifier for high tax rates" do
+ it 'returns penalty modifier for high tax rates' do
empire.tax_rate = 80
expect(system.tax_growth_modifier).to be_within(0.01).of(-0.4)
-
+
empire.tax_rate = 100
expect(system.tax_growth_modifier).to eq(-1.0)
end
end
- describe "#calculate_growth" do
+ describe '#calculate_growth' do
let(:empire) { build(:empire, tax_rate: 30) }
- let(:system) { build(:star_system, system_type: "terrestrial", empire: empire, current_population: 100) }
+ let(:system) { build(:star_system, system_type: 'terrestrial', empire: empire, current_population: 100) }
- it "calculates growth based on system type and tax modifier" do
+ it 'calculates growth based on system type and tax modifier' do
# With 30% tax rate, modifier should be approximately 1.1
# Terrestrial base growth is 5%
# So growth should be about 5% * 1.1 = 5.5% of 100 = 5.5 people
expect(system.calculate_growth).to eq(5) # Rounded down
end
-
- it "returns negative growth when conditions are harsh" do
+
+ it 'returns negative growth when conditions are harsh' do
empire.tax_rate = 90
# With 90% tax rate, modifier should be -0.7
# Terrestrial base growth is 5%
@@ -69,51 +76,109 @@
expect(system.calculate_growth).to eq(-3) # Rounded down
end
end
-
- describe "#new_population" do
- it "calculates population based on calculated growth increase" do
+ describe '#new_population' do
+ it 'calculates population based on calculated growth increase' do
empire = build(:empire, tax_rate: 20)
- system = build(:star_system,
- system_type: "terrestrial",
- current_population: 100,
- max_population: 200,
- empire: empire)
-
+ system = build(:star_system,
+ system_type: 'terrestrial',
+ current_population: 100,
+ max_population: 200,
+ empire: empire)
+
expect(system.new_population).to eq(106)
end
-
- it "calculates population based on calculated growth decrease" do
+
+ it 'calculates population based on calculated growth decrease' do
empire = build(:empire, tax_rate: 100)
- system = build(:star_system,
- system_type: "terrestrial",
- current_population: 100,
- max_population: 200,
- empire: empire)
-
+ system = build(:star_system,
+ system_type: 'terrestrial',
+ current_population: 100,
+ max_population: 200,
+ empire: empire)
+
expect(system.new_population).to eq(95)
end
-
- it "does not exceed max population" do
+
+ it 'does not exceed max population' do
empire = build(:empire, tax_rate: 20)
- system = build(:star_system,
- system_type: "terrestrial",
- current_population: 195,
- max_population: 200,
- empire: empire)
-
+ system = build(:star_system,
+ system_type: 'terrestrial',
+ current_population: 195,
+ max_population: 200,
+ empire: empire)
+
expect(system.new_population).to eq(200)
end
-
- it "does not drop below 1 population" do
+
+ it 'does not drop below 1 population' do
empire = build(:empire, tax_rate: 100)
- system = build(:star_system,
- system_type: "terrestrial",
- current_population: 1,
- max_population: 200,
- empire: empire)
-
+ system = build(:star_system,
+ system_type: 'terrestrial',
+ current_population: 1,
+ max_population: 200,
+ empire: empire)
+
expect(system.new_population).to eq(1)
end
end
+
+ describe '#buildings_count' do
+ let(:system) { create(:star_system) }
+ let(:building_type) { create(:building_type, unique_per_system: false) }
+
+ it 'returns the number of operational buildings' do
+ create(:building, star_system: system, status: 'operational', building_type: building_type)
+ create(:building, star_system: system, status: 'operational', building_type: building_type)
+ create(:building, :under_construction, star_system: system, building_type: building_type)
+
+ expect(system.buildings_count).to eq(2)
+ end
+ end
+
+ describe 'tax_modifier from buildings' do
+ let(:system) { build(:star_system) }
+ let(:building_type) { create(:building_type) }
+
+ it 'returns 0 when there are no buildings' do
+ expect(system.tax_modifier_from_buildings).to eq(0)
+ end
+
+ it 'calculates tax modifier from operational buildings' do
+ create(:building,
+ star_system: system,
+ status: 'operational',
+ building_type: building_type,
+ level: 1)
+
+ expect(system.tax_modifier_from_buildings).to eq(0.05)
+ end
+
+ it 'ignores buildings under construction' do
+ create(:building, :under_construction,
+ star_system: system,
+ building_type: building_type,
+ level: 1)
+
+ expect(system.tax_modifier_from_buildings).to eq(0)
+ end
+
+ it 'ignores buildings being demolished' do
+ create(:building, :being_demolished,
+ star_system: system,
+ building_type: building_type,
+ level: 1)
+
+ expect(system.tax_modifier_from_buildings).to eq(0)
+ end
+
+ it 'calculates tax modifier from multiple buildings' do
+ building_type_2 = create(:building_type, key: 'tax_office',
+ level_data: { '1' => { effects: { tax_modifier: 0.03 } } })
+ create(:building, star_system: system, status: 'operational', building_type: building_type, level: 1)
+ create(:building, star_system: system, status: 'operational', building_type: building_type_2, level: 1)
+
+ expect(system.tax_modifier_from_buildings).to eq(0.08)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 26eb89b..921f2ba 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe User, type: :model do
@@ -8,12 +10,12 @@
it { should validate_uniqueness_of(:username) }
# Factory test
- it "has a valid factory" do
+ it 'has a valid factory' do
expect(build(:user)).to be_valid
end
- it "is not valid without an email" do
+ it 'is not valid without an email' do
user = build(:user, email: nil)
expect(user).not_to be_valid
end
-end
\ No newline at end of file
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index d5df01f..b960ee3 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
-abort("The Rails environment is running in production mode!") if Rails.env.production?
+abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
@@ -70,4 +72,4 @@
with.test_framework :rspec
with.library :rails
end
-end
\ No newline at end of file
+end
diff --git a/spec/services/empire_builder_service_spec.rb b/spec/services/empire_builder_service_spec.rb
index fdde6fe..66219d3 100644
--- a/spec/services/empire_builder_service_spec.rb
+++ b/spec/services/empire_builder_service_spec.rb
@@ -1,46 +1,48 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe EmpireBuilderService do
- describe "#create_empire" do
+ describe '#create_empire' do
let(:user) { create(:user) }
- it "creates an empire for the user" do
- expect {
+ it 'creates an empire for the user' do
+ expect do
EmpireBuilderService.new(user).create_empire
- }.to change(Empire, :count).by(1)
+ end.to change(Empire, :count).by(1)
end
-
- it "generates a name for the empire if none provided" do
+
+ it 'generates a name for the empire if none provided' do
empire = EmpireBuilderService.new(user).create_empire
expect(empire.name).not_to be_nil
end
-
- it "creates a starting star system for the empire" do
+
+ it 'creates a starting star system for the empire' do
empire = EmpireBuilderService.new(user).create_empire
expect(empire.star_systems.count).to eq(1)
end
-
- it "creates a terrestrial type star system" do
+
+ it 'creates a terrestrial type star system' do
empire = EmpireBuilderService.new(user).create_empire
- expect(empire.star_systems.first.system_type).to eq("terrestrial")
+ expect(empire.star_systems.first.system_type).to eq('terrestrial')
end
-
- it "uses the provided name if given" do
- empire = EmpireBuilderService.new(user).create_empire("Custom Empire")
- expect(empire.name).to eq("Custom Empire")
+
+ it 'uses the provided name if given' do
+ empire = EmpireBuilderService.new(user).create_empire('Custom Empire')
+ expect(empire.name).to eq('Custom Empire')
end
- xit "returns empire with errors if name is not unique" do
- create(:empire, name: "Duplicate Empire")
+ xit 'returns empire with errors if name is not unique' do
+ create(:empire, name: 'Duplicate Empire')
- empire = EmpireBuilderService.new(user).create_empire("Duplicate Empire")
+ empire = EmpireBuilderService.new(user).create_empire('Duplicate Empire')
- expect(empire.errors[:name]).to include("has already been taken")
+ expect(empire.errors[:name]).to include('has already been taken')
expect(empire).not_to be_persisted
- end
+ end
- it "generates unique names if random names clash" do
- 5.times do
+ it 'generates unique names if random names clash' do
+ 5.times do
other_user = create(:user)
EmpireBuilderService.new(other_user).create_empire
end
@@ -51,4 +53,4 @@
expect(empire).to be_persisted
end
end
-end
\ No newline at end of file
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e7f3757..b47a1c6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'simplecov'
SimpleCov.start 'rails' do
add_filter '/bin/'
@@ -10,9 +12,9 @@
add_filter '/app/jobs/'
add_filter '/app/mailers/'
- add_group "Models", "app/models"
- add_group "Controllers", "app/controllers"
- add_group "Services", "app/services"
+ add_group 'Models', 'app/models'
+ add_group 'Controllers', 'app/controllers'
+ add_group 'Services', 'app/services'
SimpleCov.use_merging true
end
@@ -62,51 +64,49 @@
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups
-# The settings below are suggested to provide a good initial experience
-# with RSpec, but feel free to customize to your heart's content.
-=begin
- # This allows you to limit a spec run to individual examples or groups
- # you care about by tagging them with `:focus` metadata. When nothing
- # is tagged with `:focus`, all examples get run. RSpec also provides
- # aliases for `it`, `describe`, and `context` that include `:focus`
- # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
- config.filter_run_when_matching :focus
-
- # Allows RSpec to persist some state between runs in order to support
- # the `--only-failures` and `--next-failure` CLI options. We recommend
- # you configure your source control system to ignore this file.
- config.example_status_persistence_file_path = "spec/examples.txt"
-
- # Limits the available syntax to the non-monkey patched syntax that is
- # recommended. For more details, see:
- # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
- config.disable_monkey_patching!
-
- # Many RSpec users commonly either run the entire suite or an individual
- # file, and it's useful to allow more verbose output when running an
- # individual spec file.
- if config.files_to_run.one?
- # Use the documentation formatter for detailed output,
- # unless a formatter has already been configured
- # (e.g. via a command-line flag).
- config.default_formatter = "doc"
- end
-
- # Print the 10 slowest examples and example groups at the
- # end of the spec run, to help surface which specs are running
- # particularly slow.
- config.profile_examples = 10
-
- # Run specs in random order to surface order dependencies. If you find an
- # order dependency and want to debug it, you can fix the order by providing
- # the seed, which is printed after each run.
- # --seed 1234
- config.order = :random
-
- # Seed global randomization in this process using the `--seed` CLI option.
- # Setting this allows you to use `--seed` to deterministically reproduce
- # test failures related to randomization by passing the same `--seed` value
- # as the one that triggered the failure.
- Kernel.srand config.seed
-=end
+ # The settings below are suggested to provide a good initial experience
+ # with RSpec, but feel free to customize to your heart's content.
+ # # This allows you to limit a spec run to individual examples or groups
+ # # you care about by tagging them with `:focus` metadata. When nothing
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ # config.filter_run_when_matching :focus
+ #
+ # # Allows RSpec to persist some state between runs in order to support
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # # you configure your source control system to ignore this file.
+ # config.example_status_persistence_file_path = "spec/examples.txt"
+ #
+ # # Limits the available syntax to the non-monkey patched syntax that is
+ # # recommended. For more details, see:
+ # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ # config.disable_monkey_patching!
+ #
+ # # Many RSpec users commonly either run the entire suite or an individual
+ # # file, and it's useful to allow more verbose output when running an
+ # # individual spec file.
+ # if config.files_to_run.one?
+ # # Use the documentation formatter for detailed output,
+ # # unless a formatter has already been configured
+ # # (e.g. via a command-line flag).
+ # config.default_formatter = "doc"
+ # end
+ #
+ # # Print the 10 slowest examples and example groups at the
+ # # end of the spec run, to help surface which specs are running
+ # # particularly slow.
+ # config.profile_examples = 10
+ #
+ # # Run specs in random order to surface order dependencies. If you find an
+ # # order dependency and want to debug it, you can fix the order by providing
+ # # the seed, which is printed after each run.
+ # # --seed 1234
+ # config.order = :random
+ #
+ # # Seed global randomization in this process using the `--seed` CLI option.
+ # # Setting this allows you to use `--seed` to deterministically reproduce
+ # # test failures related to randomization by passing the same `--seed` value
+ # # as the one that triggered the failure.
+ # Kernel.srand config.seed
end