diff --git a/katas/4-gilded_traffic_light/fell/.rubocop.yml b/katas/4-gilded_traffic_light/fell/.rubocop.yml new file mode 100644 index 0000000..29ac8a6 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/.rubocop.yml @@ -0,0 +1,5 @@ +AllCops: + NewCops: enable +# This is the default on ruby 3.4, so we don't need it +Style/FrozenStringLiteralComment: + EnforcedStyle: never diff --git a/katas/4-gilded_traffic_light/fell/Gemfile b/katas/4-gilded_traffic_light/fell/Gemfile new file mode 100644 index 0000000..a2f702d --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +ruby '3.4.1' + +gem 'minitest' +gem 'rubocop' diff --git a/katas/4-gilded_traffic_light/fell/Gemfile.lock b/katas/4-gilded_traffic_light/fell/Gemfile.lock new file mode 100644 index 0000000..9f7e8e3 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/Gemfile.lock @@ -0,0 +1,44 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.9.1) + language_server-protocol (3.17.0.3) + minitest (5.25.4) + parallel (1.26.3) + parser (3.3.7.0) + ast (~> 2.4.1) + racc + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.10.0) + rubocop (1.71.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.37.0) + parser (>= 3.3.1.0) + ruby-progressbar (1.13.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + minitest + rubocop + +RUBY VERSION + ruby 3.4.1p0 + +BUNDLED WITH + 2.6.2 diff --git a/katas/4-gilded_traffic_light/fell/README.md b/katas/4-gilded_traffic_light/fell/README.md new file mode 100644 index 0000000..f33e33d --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/README.md @@ -0,0 +1,58 @@ +# The Gilded Traffic Light + +Refactor a messy legacy traffic light system into a proper domain model. + +You’ll start with a single-file procedural Ruby script that simulates an intersection with two directions of traffic lights and pedestrian signals. Your job is to gradually extract meaningful objects and ensure the behaviour doesn't change. + +## Problem Description + +Imagine a simplified but messy traffic controller. We have: + +- Two directions of traffic (North-South and East-West). +- Each direction cycles through Red → Green → Amber → Red. +- Pedestrian signals can only display “WALK” when that direction is red. + +In the provided code, everything (timers, states, and transitions) is lumped into one file and one giant method. Refactor it into a more object-oriented design, preserving functionality. + +## Requirements and Constraints + +### Requirements + +- The system should prevent conflicting green lights and only allow pedestrians to cross on red. +- The code must run forever unless interrupted. +- Do not break the existing console output or sequence logic. +- Preserve the timers for each colour transition. + +### Constraints + +- You must not remove or skip any stage of the light cycle (Red, Green, Amber). +- Keep the pedestrian signals tied to the traffic light states. + +## Examples and Test Cases + +A sample test file has been provided. Ensure these tests continue to pass as you refactor and rearrange the code. + +## Instructions + +Open the single-file Ruby script that contains the messy procedural code. +Run it directly to see the console output and confirm how the sequence changes over time. + +Refactor the system one step at a time. Introduce appropriate classes to develop a deeper and more meaningful domain model, while preserving all the existing behaviour. + +(Best to run the tests after each change to ensure everything still passes). + +## Evaluation Criteria + +- All tests pass for the final refactored solution. +- Quality of design: Classes and methods have clear responsibilities. +- Maintainability: Future changes (e.g. adding more directions or sensors) should be straightforward. + + +--- + +## How to run your main file + +``` +bundle exec ruby katas/4-gilded_traffic_light/fell/main.rb +``` + diff --git a/katas/4-gilded_traffic_light/fell/Rakefile.rb b/katas/4-gilded_traffic_light/fell/Rakefile.rb new file mode 100644 index 0000000..01d6868 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/Rakefile.rb @@ -0,0 +1,7 @@ +require 'minitest/test_task' + +Minitest::TestTask.create do |t| + # t.framework = %(require "test/test_helper.rb") + t.libs = %w[test .] + t.test_globs = ['test/**/*_test.rb'] +end diff --git a/katas/4-gilded_traffic_light/fell/lib/colorize.rb b/katas/4-gilded_traffic_light/fell/lib/colorize.rb new file mode 100644 index 0000000..5d338f2 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/lib/colorize.rb @@ -0,0 +1,19 @@ +# Provides helper methods for colorized text +module Colorize + RED = -"\e[31m" + GREEN = -"\e[32m" + AMBER = -"\e[33m" + RESET = -"\e[0m" + + def red(string) + "#{RED}#{string}#{RESET}" + end + + def green(string) + "#{GREEN}#{string}#{RESET}" + end + + def amber(string) + "#{AMBER}#{string}#{RESET}" + end +end diff --git a/katas/4-gilded_traffic_light/fell/main.rb b/katas/4-gilded_traffic_light/fell/main.rb new file mode 100644 index 0000000..fdb71ec --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/main.rb @@ -0,0 +1,58 @@ +require_relative 'traffic_light' + +# Main class providing orchestration of multiple traffic lights +class TrafficLightSystem + def initialize + @lights = [ + TrafficLight.new(direction: 'North-South', state: 'red', timer: 10), + TrafficLight.new(direction: 'East-West', state: 'green', timer: 8) + ] + end + + # Advances the system by one second: prints current status, decrements timers, + # and updates states as needed, all in one giant procedural block. + def run + # Clear the screen each time (so this is consistent with the original request). + # You could skip this if you don't want console clearing in tests, but it's here for completeness. + # system("clear") || system("cls") + + # Print traffic lights + puts @lights.map(&:to_s) + + progress_lights + + # Print pedestrian signals + puts "\nPedestrian signals:" + puts(@lights.map { |l| " #{l.pedestrian_signal_status}" }) + + puts '---------------------------------' + end + + private + + def progress_lights + @lights.each do |light| + light.progress! + + # TODO: This is only necessary because the timings are such that we will + # have a clash each cycle; an ideal solution would be to change the + # timings, but the existing output remaining the same is a requirement + # + # Nothing to do unless there is goign to be a conflict + next unless @lights.all?(&:allows_traffic?) + + # Since we're turning green, then the other light must turn red + @lights.find { it != light }.red! + end + end +end + +# Only run the perpetual loop if this file is executed directly. +# Each call to #run advances the system by one second. Tests can call #run manually. +if $PROGRAM_NAME == __FILE__ + system = TrafficLightSystem.new + loop do + system.run + sleep 1 + end +end diff --git a/katas/4-gilded_traffic_light/fell/pedestrian_signal.rb b/katas/4-gilded_traffic_light/fell/pedestrian_signal.rb new file mode 100644 index 0000000..ddf237b --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/pedestrian_signal.rb @@ -0,0 +1,31 @@ +require_relative 'lib/colorize' + +# Tracks whether pedestrians can walk or not and provides presentation logic +class PedestrianSignal + include Colorize + + WALK = -'WALK' + DONT_WALK = -"DON'T WALK" + + def initialize(can_walk:) + @can_walk = can_walk + end + + attr_accessor :can_walk + + def to_s + if can_walk + can_walk_text + else + cannot_walk_text + end + end + + def can_walk_text + green(WALK) + end + + def cannot_walk_text + red(DONT_WALK) + end +end diff --git a/katas/4-gilded_traffic_light/fell/test/colorize_test.rb b/katas/4-gilded_traffic_light/fell/test/colorize_test.rb new file mode 100644 index 0000000..ec0b9d2 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/test/colorize_test.rb @@ -0,0 +1,26 @@ +require_relative '../lib/colorize' +require 'minitest/autorun' + +class ColorizeTest < Minitest::Test + class ColorizedClass + include Colorize + end + + def test_red + test_instance = ColorizeTest::ColorizedClass.new + + assert_equal "\e[31mtest_string\e[0m", test_instance.red('test_string') + end + + def test_green + test_instance = ColorizeTest::ColorizedClass.new + + assert_equal "\e[32mtest_string\e[0m", test_instance.green('test_string') + end + + def test_amber + test_instance = ColorizeTest::ColorizedClass.new + + assert_equal "\e[33mtest_string\e[0m", test_instance.amber('test_string') + end +end diff --git a/katas/4-gilded_traffic_light/fell/test/main_test.rb b/katas/4-gilded_traffic_light/fell/test/main_test.rb new file mode 100644 index 0000000..79851dd --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/test/main_test.rb @@ -0,0 +1,66 @@ +# traffic_light_system_test.rb + +require_relative '../main' +require 'minitest/autorun' + +class TrafficLightSystemTest < Minitest::Test + def test_initial_output + system = TrafficLightSystem.new + out, _err = capture_io do + system.run + end + + assert_includes out, "Direction: North-South, State: \e[31mred\e[0m, Time left: 10s" + assert_includes out, "Direction: East-West, State: \e[32mgreen\e[0m, Time left: 8s" + + assert_includes out, "North-South: \e[31mDON'T WALK\e[0m" + assert_includes out, "East-West: \e[31mDON'T WALK\e[0m" + end + + def test_red_transitions_to_green + system = TrafficLightSystem.new + 10.times { system.run } + + out, _err = capture_io do + system.run + end + + assert_includes out, "Direction: North-South, State: \e[32mgreen\e[0m, Time left: 8s" + end + + def test_green_transitions_to_amber + system = TrafficLightSystem.new + 8.times { system.run } + + out, _err = capture_io do + system.run + end + + assert_includes out, "Direction: East-West, State: \e[33mamber\e[0m, Time left: 3s" + end + + def test_amber_transitions_to_red + system = TrafficLightSystem.new + 11.times { system.run } # now East-West should be amber(3) + + out, _err = capture_io do + system.run + end + + assert_includes out, "Direction: East-West, State: \e[31mred\e[0m, Time left: 8s" + assert_includes out, "East-West: \e[32mWALK\e[0m" + end + + def test_opposite_direction_forced_red + system = TrafficLightSystem.new + + 10.times { system.run } + out, _err = capture_io do + system.run + end + + assert_includes out, "Direction: North-South, State: \e[32mgreen\e[0m, Time left: 8s" + assert_includes out, "Direction: East-West, State: \e[31mred\e[0m, Time left: 9s" + assert_includes out, "East-West: \e[32mWALK\e[0m" + end +end diff --git a/katas/4-gilded_traffic_light/fell/test/pedestrian_signal_test.rb b/katas/4-gilded_traffic_light/fell/test/pedestrian_signal_test.rb new file mode 100644 index 0000000..1331d25 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/test/pedestrian_signal_test.rb @@ -0,0 +1,26 @@ +require_relative '../pedestrian_signal' +require 'minitest/autorun' + +class PedestrianSignalTest < Minitest::Test + def test_can_walk + signal = PedestrianSignal.new(can_walk: true) + + assert_equal true, signal.can_walk + + signal.can_walk = false + + assert_equal false, signal.can_walk + end + + def test_can_walk_to_s + signal = PedestrianSignal.new(can_walk: true) + + assert_equal "\e[32mWALK\e[0m", signal.to_s + end + + def test_cannot_walk_to_s + signal = PedestrianSignal.new(can_walk: false) + + assert_equal "\e[31mDON'T WALK\e[0m", signal.to_s + end +end diff --git a/katas/4-gilded_traffic_light/fell/test/traffic_light_state_test.rb b/katas/4-gilded_traffic_light/fell/test/traffic_light_state_test.rb new file mode 100644 index 0000000..055c1ad --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/test/traffic_light_state_test.rb @@ -0,0 +1,75 @@ +require_relative '../traffic_light' +require 'minitest/autorun' + +class TrafficLightStateTest < Minitest::Test + def red_test + red = TrafficLightState.red + + assert_equal false, red.allows_traffic? + assert_equal true, red.allows_pedestrians? + end + + def green_test + green = TrafficLightState.green + + assert_equal true, green.allows_traffic? + assert_equal false, green.allows_pedestrians? + end + + def amber_test + amber = TrafficLightState.amber + + assert_equal true, amber.allows_traffic? + assert_equal false, amber.allows_pedestrians? + end + + def complete_test + state = TrafficLightState.new(state: TrafficLightState::RED, timer: 0) + + assert_equal true, state.complete? + end + + def incomplete_test + state = TrafficLightState.new(state: TrafficLightState::RED, timer: 1) + + assert_equal false, state.complete? + end + + def next_state_test + assert_equal TrafficLightState.green, TrafficLightState.red.next_state + assert_equal TrafficLightState.amber, TrafficLightState.green.next_state + assert_equal TrafficLightState.red, TrafficLightState.amber.next_state + end + + def progress_timer_test + state = TrafficLightState.new(state: TrafficLightState::RED, timer: 1) + + assert_equal(false, timer.complete?) + state.progress_timer + assert_equal(true, timer.complete?) + end + + def to_s_contains_red_state + state = TrafficLightState.new(state: TrafficLightState::RED, timer: 1) + + assert_includes state.to_s, "State: \e[31mred\e[0m" + end + + def to_s_contains_green_state + state = TrafficLightState.new(state: TrafficLightState::GREEN, timer: 1) + + assert_includes state.to_s, "State: \e[32mgreen\e[0m" + end + + def to_s_contains_amber_state + state = TrafficLightState.new(state: TrafficLightState::AMBER, timer: 1) + + assert_includes state.to_s, "State: \e[33mamber\e[0m" + end + + def to_s_contains_timer + state = TrafficLightState.new(state: TrafficLightState::AMBER, timer: 1) + + assert_includes state.to_s, 'Time left: 1s' + end +end diff --git a/katas/4-gilded_traffic_light/fell/test/traffic_light_test.rb b/katas/4-gilded_traffic_light/fell/test/traffic_light_test.rb new file mode 100644 index 0000000..a7278e8 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/test/traffic_light_test.rb @@ -0,0 +1,127 @@ +require_relative '../traffic_light' +require 'minitest/autorun' + +class TrafficLightTest < Minitest::Test + def test_direction_to_s + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::GREEN) + light.to_s + + assert_includes light.to_s, 'Direction: Widdershins' + end + + def test_time_left_to_s + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::GREEN) + light.to_s + + assert_includes light.to_s, 'Time left: 55s' + end + + def test_red_light_to_s + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::RED) + + light.to_s + + assert_includes light.to_s, "\e[31mred\e[0m" + end + + def test_green_light_to_s + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::GREEN) + + light.to_s + + assert_includes light.to_s, "\e[32mgreen\e[0m" + end + + def test_amber_light_to_s + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::AMBER) + + light.to_s + + assert_includes light.to_s, "\e[33mamber\e[0m" + end + + def test_progress_with_incomplete_timer + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::GREEN) + + light.progress! + + assert_includes light.to_s, 'green' + end + + def test_progress_progreses_state_with_complete_timer + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::GREEN) + light.progress! + + assert_includes light.to_s, 'amber' + end + + def test_inital_pedestrian_signal_status + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::RED) + + # Even on red, pedestrians cannot walk initially + assert_includes light.pedestrian_signal_status, "Widdershins: \e[31mDON'T WALK\e[0m" + end + + def test_can_walk_pedestrian_signal_status + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::AMBER) + + # Progress to red light for traffic + light.next_state! + + assert_includes light.pedestrian_signal_status, "Widdershins: \e[32mWALK\e[0m" + end + + def test_cannot_walk_pedestrian_signal_status + light = TrafficLight.new(direction: 'Widdershins', timer: 55, state: TrafficLightState::GREEN) + + # Run through the whole cycle once + 3.times do + light.next_state! + end + + assert_includes light.pedestrian_signal_status, "Widdershins: \e[31mDON'T WALK\e[0m" + end + + def test_red + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::GREEN) + + light.red! + + assert_equal false, light.allows_traffic? + assert_equal true, light.allows_pedestrians? + end + + def test_next_state + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::RED) + + light.next_state! + + assert_includes light.to_s, 'green' + + light.next_state! + + assert_includes light.to_s, 'amber' + + light.next_state! + + assert_includes light.to_s, 'red' + end + + def test_allows_traffic_red + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::RED) + + assert_equal false, light.allows_traffic? + end + + def test_allows_traffic_green + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::GREEN) + + assert_equal true, light.allows_traffic? + end + + def test_allows_traffic_amber + light = TrafficLight.new(direction: 'Widdershins', timer: 1, state: TrafficLightState::AMBER) + + assert_equal true, light.allows_traffic? + end +end diff --git a/katas/4-gilded_traffic_light/fell/traffic_light.rb b/katas/4-gilded_traffic_light/fell/traffic_light.rb new file mode 100644 index 0000000..4b8f43e --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/traffic_light.rb @@ -0,0 +1,72 @@ +require_relative 'pedestrian_signal' +require_relative 'traffic_light_state' + +# Tracks the state, timer, and pedestrian signals, +# and provides presentation logic. +class TrafficLight + def initialize(direction:, state:, timer:) + @direction = direction + # The pedestrian signals always start out not being able to allow + # pedestrians; This isn't a good representation of the safety of the system, + # but it's a requirement that we need to maintain. + @pedestrian_signal = PedestrianSignal.new(can_walk: false) + @state = TrafficLightState.new(state:, timer:) + end + + def to_s + "Direction: #{direction}, #{state}" + end + + def pedestrian_signal_status + "#{direction}: #{pedestrian_signal}" + end + + def allows_traffic? + state.allows_traffic? + end + + # Unused outside of this class, but part of the public interface + def allows_pedestrians? + # If we've got a pedestrian_signal we should use that as we start our with + # pedestrians being unable to walk, but otherwise we defer to the state of + # the lights to decide. + @pedestrian_signal&.can_walk || state.allows_pedestrians? + end + + def progress! + state.progress_timer + next_state! if state.complete? + end + + # takes lights round in red -> green -> amber -> red cycle + def next_state! + # Reset pedestrian signal to allow for tying pedestrian signals to lights + # after the first cycle (where they start false) + self.pedestrian_signal = nil + + # Progress through the cycle of states + self.state = state.next_state + end + + # Used by orchestrator to ensure multiple green lights at once does not + # happen. TODO: Once we can resolve that, we shouldn't have external actors + # setting this object's state directly. + def red! + # Allow pedestrians to walk, since we're stopping traffic + self.pedestrian_signal = nil + + self.state = TrafficLightState.red + end + + private + + def pedestrian_signal + @pedestrian_signal ||= PedestrianSignal.new( + can_walk: allows_pedestrians? + ) + end + + attr_reader :direction + attr_accessor :state + attr_writer :pedestrian_signal +end diff --git a/katas/4-gilded_traffic_light/fell/traffic_light_state.rb b/katas/4-gilded_traffic_light/fell/traffic_light_state.rb new file mode 100644 index 0000000..011d160 --- /dev/null +++ b/katas/4-gilded_traffic_light/fell/traffic_light_state.rb @@ -0,0 +1,88 @@ +require_relative 'lib/colorize' + +# Handles the state of the traffic light +class TrafficLightState + include Colorize + + RED = -'red' + GREEN = -'green' + AMBER = -'amber' + + DEFAULTS = { + RED => { timer: 10 }, + GREEN => { timer: 8 }, + AMBER => { timer: 3 } + }.freeze + + GOING_STATES = [ + GREEN, + AMBER + ].freeze + + class << self + def red + new(state: RED, **DEFAULTS[RED]) + end + + def green + new(state: GREEN, **DEFAULTS[GREEN]) + end + + def amber + new(state: AMBER, **DEFAULTS[AMBER]) + end + end + + def initialize(state:, timer:) + @state = state + @timer = timer + end + + def allows_traffic? + GOING_STATES.include?(state) + end + + def allows_pedestrians? + # Pedestrians may only cross when it's safe to do so. + !allows_traffic? + end + + def complete? + !timer.positive? + end + + # states follow a red -> green -> amber -> red cycle + def next_state + case state + when RED + self.class.green + when GREEN + self.class.amber + when AMBER + self.class.red + end + end + + def progress_timer + self.timer -= 1 + end + + def to_s + "State: #{colorized_state}, Time left: #{timer}s" + end + + private + + attr_accessor :state, :timer + + def colorized_state + case state + when RED + red(state) + when GREEN + green(state) + when AMBER + amber(state) + end + end +end