Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions katas/4-gilded_traffic_light/eddbot/README.md
Original file line number Diff line number Diff line change
@@ -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/edward.s/main.rb
```

37 changes: 37 additions & 0 deletions katas/4-gilded_traffic_light/eddbot/lib/printer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class Printer
def initialize(out: STDOUT)
@out = out
end

def print_traffic_lights(lights:)
lights.each do |light|
puts("Direction: #{light[:direction]}, State: #{state_colour(state: light[:state])}, Time left: #{light[:timer]}s")
end
end

def print_pedestrian_signals(signals:)
puts "\nPedestrian signals:"
signals.each do |direction, can_walk|
signal_text = can_walk ? colourizer(:green, "WALK") : colourizer(:red, "DON'T WALK")
puts " #{direction}: #{signal_text}"
end

puts "---------------------------------"
end

private

def state_colour(state:) = colourizer(state.to_sym, state)

def colourizer(color, input)
case color
when :red then "\e[31m#{input}\e[0m"
when :green then "\e[32m#{input}\e[0m"
when :amber then "\e[33m#{input}\e[0m"
else
input
end
end

attr_reader :out
end
93 changes: 93 additions & 0 deletions katas/4-gilded_traffic_light/eddbot/main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require 'bundler/inline'
require_relative 'lib/printer'

gemfile do
source 'https://rubygems.org'

gem 'minitest'
end

class TrafficLightSystem
def initialize(out: STDOUT)
@lights = [
{ direction: "North-South", state: "red", timer: 10 },
{ direction: "East-West", state: "green", timer: 8 }
]

@pedestrian_signals = {
"North-South" => false,
"East-West" => false
}

@printer = Printer.new(out:)
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")

printer.print_traffic_lights(lights:)

# Update timers and transition states in one big chunk
lights.each do |light|
light[:timer] -= 1
if light[:timer] <= 0
# Big case statement for state transitions
case light[:state]
when "red"
# Switch from red -> green
light[:state] = "green"
light[:timer] = 8
pedestrian_signals[light[:direction]] = false
# Force the opposite light red if it's green or amber
opposite = lights.find { |l| l[:direction] != light[:direction] }
if %w[green amber].include?(opposite[:state])
opposite[:state] = "red"
opposite[:timer] = 10
pedestrian_signals[opposite[:direction]] = true
end

when "green"
# Switch from green -> amber
light[:state] = "amber"
light[:timer] = 3
pedestrian_signals[light[:direction]] = false

when "amber"
# Switch from amber -> red
light[:state] = "red"
light[:timer] = 10
pedestrian_signals[light[:direction]] = true

else
# Fallback
light[:state] = "red"
light[:timer] = 10
pedestrian_signals[light[:direction]] = true
end
end
end

printer.print_pedestrian_signals(signals: pedestrian_signals)
end
private

attr_reader :printer, :lights, :pedestrian_signals
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
66 changes: 66 additions & 0 deletions katas/4-gilded_traffic_light/eddbot/test.rb
Original file line number Diff line number Diff line change
@@ -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