diff --git a/internal/design/design.go b/internal/design/design.go index d81141a..a1834ea 100644 --- a/internal/design/design.go +++ b/internal/design/design.go @@ -99,6 +99,21 @@ type WebServer struct { MonthlyCostUsd int `json:"monthlyCostUsd"` } +type Request struct { + ID string + Timestamp int + LatencyMS int + Origin string + Type string + Path []string +} + +type TickSnapshot struct { + TickMs int + QueueSizes map[string]int + NodeHealth map[string]string +} + func (n *Node) UnmarshalJSON(data []byte) error { type Alias Node // avoid infinite recursion aux := &struct { diff --git a/internal/simulation/engine.go b/internal/simulation/engine.go new file mode 100644 index 0000000..13d9d52 --- /dev/null +++ b/internal/simulation/engine.go @@ -0,0 +1,52 @@ +package simulation + +import ( + "systemdesigngame/internal/design" +) + +type Request struct { + ID string + Timestamp int + LatencyMS int + Origin string + Type string + Path []string +} + +type SimulationNode interface { + ID() string + Type() string + Tick(tick int) + Receive(req *Request) + Emit() []*Request + IsAlive() bool +} + +type Engine struct { + Nodes map[string]SimulationNode + Timeline []TickSnapshot + Duration int + TickMs int +} + +type TickSnapshot struct { + TickMs int + QueueSizes map[string]int + NodeHealth map[string]string +} + +func NewEngineFromDesign(design simulation.Design, duration int, tickMs int) *Engine { + nodes := design.ToSimulationNode() + nodeMap := make(make[string]SimulatSimulationNode) + for _, n := range nodes { + nodeMap[n.ID] = n + } + + return &Engine{ + Nodes: nodeMap, + Duration: duration, + TickMs: tickMs, + } +} + + diff --git a/internal/simulation/loadbalancer.go b/internal/simulation/loadbalancer.go new file mode 100644 index 0000000..0f91823 --- /dev/null +++ b/internal/simulation/loadbalancer.go @@ -0,0 +1,53 @@ +package simulation + +type LoadBalancerNode struct { + ID string + Queue []*Request + Targets []string + Counter int + Alive bool + Processed []*Request +} + +func (lb *LoadBalancerNode) GetID() string { + return lb.ID +} + +func (lb *LoadBalancerNode) Type() string { + return "loadBalancer" +} + +func (lb *LoadBalancerNode) IsAlive() bool { + return lb.Alive +} + +func (lb *LoadBalancerNode) Receive(req *Request) { + lb.Queue = append(lb.Queue) +} + +func (lb *LoadBalancerNode) Tick(tick int) { + lb.Processed = nil + + for _, req := range lb.Queue { + if len(lb.Targets) == 0 { + continue + } + + target := lb.Targets[lb.Counter%len(lb.Targets)] + lb.Counter++ + + req.Path = append([]string{target}, req.Path...) + + req.LatencyMS += 10 + + lb.Processed = append(lb.Processed, req) + } + + lb.Queue = lb.Queue[:0] +} + +func (lb *LoadBalancerNode) Emit() []*Request { + out := lb.Processed + lb.Processed = nil + return out +} diff --git a/internal/simulation/webservernode.go b/internal/simulation/webservernode.go new file mode 100644 index 0000000..8a47a61 --- /dev/null +++ b/internal/simulation/webservernode.go @@ -0,0 +1,60 @@ +package simulation + +type WebServerNode struct { + ID string + Queue []*Request + CapacityRPS int + BaseLatencyMs int + PenaltyPerRPS float64 + Processed []*Request + Alive bool +} + +func (ws *WebServerNode) GetID() string { + return ws.ID +} + +func (ws *WebServerNode) Type() string { + return "webserver" +} + +func (ws *WebServerNode) IsAlive() bool { + return ws.Alive +} + +func (ws *WebServerNode) Tick(tick int) { + toProcess := min(ws.CapacityRPS, len(ws.Queue)) + for i := 0; i < toProcess; i++ { + req := ws.Queue[0] + req.LatencyMS += ws.BaseLatencyMs + ws.Processed = append(ws.Processed, req) + + ws.Queue[i] = nil + } + + ws.Queue = ws.Queue[toProcess:] + + if len(ws.Queue) > ws.CapacityRPS { + overload := len(ws.Queue) - ws.CapacityRPS + for _, req := range ws.Queue { + req.LatencyMS += int(ws.PenaltyPerRPS * float64(overload)) + } + } +} + +func (ws *WebServerNode) Receive(req *Request) { + ws.Queue = append(ws.Queue, req) +} + +func (ws *WebServerNode) Emit() []*Request { + out := ws.Processed + ws.Processed = nil + return out +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/router/handlers/results.go b/router/handlers/results.go new file mode 100644 index 0000000..c14dd04 --- /dev/null +++ b/router/handlers/results.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +type ResultHandler struct { + Tmpl *template.Template +} + +func (r *ResultHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + data := struct { + Title string + }{ + Title: "Title", + } + + if err := r.Tmpl.ExecuteTemplate(w, "success.html", data); err != nil { + http.Error(w, "Template Error", http.StatusInternalServerError) + } +} diff --git a/router/handlers/play.go b/router/handlers/simulate.go similarity index 100% rename from router/handlers/play.go rename to router/handlers/simulate.go diff --git a/static/difficulty-select.html b/static/difficulty-select.html new file mode 100644 index 0000000..1e57e67 --- /dev/null +++ b/static/difficulty-select.html @@ -0,0 +1,625 @@ + + + + + + System Design Game - Select Difficulty + + + +
+

System Design Game

+
+ +
+
+

Select Difficulty

+

+ Choose your challenge level. Each difficulty offers unique constraints and learning opportunities + to help you master system design at your own pace. +

+
+ +
+ +
+
+
🌱
+

Easy

+
+ +

+ Perfect for beginners. Learn the fundamentals with guided tutorials and forgiving constraints. +

+ +
    +
  • Extended time limits
  • +
  • Helpful hints and tips
  • +
  • Relaxed performance requirements
  • +
  • Step-by-step guidance
  • +
  • Basic component set
  • +
+ +
+
+
+
Time Limit
+
45 min
+
+
+
Max RPS
+
1K
+
+
+
Budget
+
$500
+
+
+
Availability
+
99%
+
+
+
+ + +
+ + +
+
+
+

Medium

+
+ +

+ Balanced challenge for intermediate learners. Real-world constraints with moderate complexity. +

+ +
    +
  • Standard time constraints
  • +
  • Realistic performance targets
  • +
  • Full component library
  • +
  • Trade-off decisions required
  • +
  • Cost optimization needed
  • +
+ +
+
+
+
Time Limit
+
30 min
+
+
+
Max RPS
+
10K
+
+
+
Budget
+
$2K
+
+
+
Availability
+
99.9%
+
+
+
+ + +
+ + +
+
+
🔥
+

Hard

+
+ +

+ Expert-level challenges. Tight constraints and complex requirements that mirror real FAANG interviews. +

+ +
    +
  • Strict time pressure
  • +
  • High-scale requirements
  • +
  • Complex failure scenarios
  • +
  • Advanced optimization needed
  • +
  • Multiple constraint conflicts
  • +
+ +
+
+
+
Time Limit
+
20 min
+
+
+
Max RPS
+
100K
+
+
+
Budget
+
$5K
+
+
+
Availability
+
99.99%
+
+
+
+ + +
+
+
+ + + + diff --git a/static/failure.html b/static/failure.html new file mode 100644 index 0000000..72579a4 --- /dev/null +++ b/static/failure.html @@ -0,0 +1,739 @@ + + + + + + System Design Game - System Failed + + + +
+ +
+

System Design Game

+
+ +
+

SYSTEM OVERLOAD

+

Your architecture couldn't handle the load

+ +
+
+ Critical system failure detected. Your design exceeded operational limits. +
+ +
+
+
Target RPS
+
10,000
+
+
+
Achieved RPS
+
2,847
+
+
+
Max Latency
+
200ms
+
+
+
Actual Latency
+
1,247ms
+
+
+ +
+
[ERROR] Database connection pool exhausted
+
[ERROR] Load balancer timeout after 30s
+
[ERROR] Cache miss ratio: 89%
+
[FATAL] System unresponsive - shutting down
+
+
+ +
+ + + Main Menu + +
+
+ + + + diff --git a/static/game-mode.html b/static/game-mode.html new file mode 100644 index 0000000..61594af --- /dev/null +++ b/static/game-mode.html @@ -0,0 +1,590 @@ + + + + + + System Design Game - Choose Your Path + + + +
+
+

System Design Game

+ + + + + Login with GitHub + +
+ +
+
+

Choose Your Path

+

Master system design through structured challenges or practice with unlimited creativity

+
+ +
+ +
+
+
🏆
+
+
Campaign Mode
+
Structured learning path
+
+
+ +
+
    +
  • Progressive difficulty levels
  • +
  • Guided tutorials and hints
  • +
  • Unlock achievements
  • +
  • Track your progress
  • +
+
+ +
+
Current Progress
+
+ Level: + 3/12 +
+
+
+
+
+ + + ▶ Continue Campaign + +
+ + +
+
+
🎯
+
+
Practice Mode
+
Free-form exploration
+
+
+ +
+
    +
  • Unlimited sandbox access
  • +
  • Custom challenge creation
  • +
  • No time constraints
  • +
  • Experiment freely
  • +
+
+ +
+
Recent Activity
+
+
• E-commerce Platform Design
+
• Chat Application Architecture
+
• Video Streaming Service
+
+
+ + + 🎯 Start Practicing + +
+
+ + +
+
+
12
+
Campaign Levels
+
+
+
+
Practice Scenarios
+
+
+
11
+
Component Types
+
+
+
24/7
+
Available
+
+
+
+
+ + + + diff --git a/static/game.html b/static/game.html index e6cd876..ceff018 100644 --- a/static/game.html +++ b/static/game.html @@ -4,574 +4,8 @@ System Design Game - - + +
@@ -587,7 +21,6 @@ Login with GitHub {{ end }} -
diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..81efecb --- /dev/null +++ b/static/style.css @@ -0,0 +1,567 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap'); + +/* === CSS VARIABLES === */ +:root { + /* Colors */ + --color-bg-body: #161b22; + --color-bg-dark: #121212; + --color-bg-sidebar: #111; + --color-bg-component: #1e1e1e; + --color-bg-hover: #2a2a2a; + --color-bg-accent: #005f87; + --color-bg-tab-active: #1a3d2a; + --color-border: #444; + --color-border-accent: #00ff88; + --color-border-panel: #30363d; + --color-text-primary: #ccc; + --color-text-muted: #888; + --color-text-accent: #00ff88; + --color-text-white: #fff; + --color-text-dark: #333; + --color-button: #238636; + --color-button-disabled: #555; + --color-connection: #333; + --color-connection-selected: #007bff; + --color-tooltip-bg: #333; + --color-tooltip-text: #fff; + + /* Sizes */ + --radius-small: 4px; + --radius-medium: 6px; + --radius-large: 8px; + --font-family-mono: 'JetBrains Mono', monospace; + --font-family-code: 'Fira Code', monospace; + --component-padding: 8px; + --component-gap: 12px; +} + +/* === RESET & BASE STYLES === */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: var(--font-family-mono); + color: var(--color-text-primary); + display: flex; + flex-direction: row; + min-height: 100vh; + background: radial-gradient(circle at 30% 50%, rgba(0, 255, 136, 0.1), transparent 50%), + radial-gradient(circle at 70% 80%, rgba(255, 107, 53, 0.1), transparent 50%), + var(--color-bg-body) +} + +/* === LAYOUT === */ +#page-container { + display: flex; + flex-direction: column; + width: 100%; +} + +#sd-header { + width: 100%; + padding: 12px 24px; + font-weight: bold; + color: var(--color-text-accent); + border-bottom: 1px solid var(--color-text-dark); + display: flex; + align-items: center; + justify-content: space-between; +} + +.header-text { + font-size: 24px; + margin: 0; + text-shadow: 0 0 10px rgba(0, 255, 136, 0.8); + +} + +#main-content { + display: flex; + flex-direction: row; + height: 100%; +} + +/* === SIDEBAR === */ +#sidebar { + width: 100%; + background-color: var(--color-bg-sidebar); + display: flex; + flex-wrap: wrap; + flex-direction: row; + gap: var(--component-gap); +} + +.sidebar-title { + color: #8b949e; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 15px; + padding-bottom: 8px; + padding-left: 8px; + border-bottom: 1px solid #303638; +} + +/* === COMPONENT ICONS === */ +.component-icon, +#arrow-tool { + position: relative; + padding: var(--component-padding) 12px; + background-color: var(--color-bg-component); + border: 1px solid var(--color-border); + border-radius: var(--radius-medium); + text-align: center; + cursor: grab; + user-select: none; + font-size: 16px; + color: var(--color-text-primary); + transition: background-color 0.1s ease; +} + +.component-icon:hover, +#arrow-tool:hover { + background-color: var(--color-bg-hover); + border-color: var(--color-border-accent); +} + +.component-icon:active, +#arrow-tool:active { + cursor: grabbing; +} + +#arrow-tool.active { + background-color: var(--color-bg-accent); + color: var(--color-text-white); + border-color: var(--color-button); +} + +/* === TOOLTIP === */ +.tooltip { + visibility: hidden; + opacity: 0; + position: absolute; + top: 100%; + left: 0; + z-index: 10; + background: var(--color-tooltip-bg); + color: var(--color-tooltip-text); + padding: 6px 8px; + border-radius: var(--radius-small); + white-space: nowrap; + font-size: 14px; + line-height: 1.4; + margin-top: 4px; + transition: opacity 0.2s; +} + +.component-icon:hover .tooltip { + visibility: visible; + opacity: 1; + z-index: 1000; +} + +.component-icon.dragging .tooltip { + display: none; +} + +/* === CANVAS === */ +#canvas-wrapper { + flex: 1; + display: flex; + flex-direction: column; + border-radius: var(--radius-large); + border: 2px solid var(--color-border-panel); + overflow: hidden; + background: var(--color-bg-dark); + margin: 12px 12px 12px 0; + padding: 16px; +} + +#canvas-container { + flex: 1; + position: relative; + background: var(--color-bg-dark); + height: 100%; + margin-top: 16px; +} + +#canvas { + width: 100%; + height: 90%; + background: var(--color-bg-dark); + border: 2px dashed var(--color-border-panel); + border-radius: var(--radius-large); +} + +.dropped { + cursor: move; +} + +.dropped.selected rect { + stroke: #00bcd4; + stroke-width: 2; +} + +/* === TOOLBAR === */ +#canvas-toolbar { + position: absolute; + top: 12px; + left: 12px; + z-index: 20; + display: flex; + gap: 8px; + background: var(--color-bg-component); + border: 1px solid var(--color-border); + border-radius: var(--radius-small); + padding: 6px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); +} + +.toolbar-btn { + background: none; + border: 1px solid var(--color-border); + color: var(--color-text-primary); + padding: 6px 10px; + border-radius: var(--radius-small); + font-size: 14px; + cursor: pointer; + font-family: var(--font-family-mono); +} + +.toolbar-btn:hover { + background-color: var(--color-bg-hover); + border-color: var(--color-border-accent); +} + +.toolbar-btn.active { + background-color: var(--color-bg-accent); + color: var(--color-text-white); + border-color: var(--color-button); +} + +/* === PANELS === */ +#info-panel { + position: absolute; + top: 12px; + right: 12px; + background: var(--color-bg-dark); + color: var(--color-text-primary); + padding: 1rem; + border-radius: var(--radius-large); + font-family: monospace; + font-size: 14px; + min-width: 220px; + z-index: 10; + border: 1px solid var(--color-text-dark); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); +} + +#node-props-panel { + position: absolute; + width: 220px; + background-color: var(--color-bg-sidebar); + border: 1px solid var(--color-border); + border-radius: var(--radius-small); + padding: 12px; + color: var(--color-text-white); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); + display: none; + z-index: 10; +} + +#node-props-panel h3 { + margin-top: 0; + font-size: 15px; + color: var(--color-text-primary); +} + +#node-props-panel .form-group { + margin-bottom: 10px; +} + +#node-props-panel label { + display: block; + font-weight: bold; + margin-bottom: 4px; +} + +#node-props-panel select { + width: 100%; + padding: 4px; + font-size: 14px; +} + +.prop-group { + display: none; + margin-bottom: 12px; +} + +.prop-group label, +.prop-group input { + display: block; + width: 100%; + margin-top: 6px; + font-size: 13px; +} + +.panel-title { + font-weight: bold; + color: var(--color-text-white); + font-size: 15px; + margin-bottom: 0.5rem; +} + +.panel-metric { + margin-bottom: 0.4rem; +} + +.panel-metric .label { + display: inline-block; + width: 140px; + color: var(--color-text-muted); +} + +/* === INPUTS & BUTTONS === */ +input[type="text"], +input[type="number"] { + padding: 6px; + background-color: #222; + border: 1px solid var(--color-border); + color: var(--color-text-white); + border-radius: var(--radius-small); + font-family: var(--font-family-code); +} + +#node-props-save, +#run-button { + margin-top: 8px; + padding: 10px; + background-color: var(--color-button); + color: var(--color-text-white); + border: none; + border-radius: var(--radius-small); + cursor: pointer; + font-size: 14px; +} + +#run-button:disabled, +#node-props-panel button:disabled { + background-color: var(--color-button-disabled); + cursor: not-allowed; +} + +#github-login-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background-color: #fff; + color: #000000; + text-decoration: none; + border-radius: var(--radius-medium); + font-weight: 500; + font-family: var(--font-family-mono); + font-size: 12px; + border: 1px solid #2ea043; + transition: background-color 0.2s ease; + float: right; +} + +#github-login-btn:hover { + background-color: #ccc; +} + +#github-login-btn img { + width: 18px; + height: 18px; +} + +/* === TABS === */ +.tabs { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.tab-labels { + display: flex; + cursor: pointer; +} + +.tab-labels label { + padding: 10px 20px; + background: var(--color-bg-body); + margin-right: 4px; + margin-bottom: 20px; + border-radius: var(--radius-small); +} + +.tab-content { + border-top: 1px solid var(--color-border-panel); + padding: 20px 0 0; + display: none; + height: 100%; +} + +input[name="tab"] { + display: none; +} + +#tab1:checked ~ .tabs .tab-labels label[for="tab1"], +#tab2:checked ~ .tabs .tab-labels label[for="tab2"], +#tab3:checked ~ .tabs .tab-labels label[for="tab3"] { + background: var(--color-bg-tab-active); + font-weight: bold; + color: var(--color-text-accent); +} + +#tab1:checked ~ .tabs #content1, +#tab2:checked ~ .tabs #content2, +#tab3:checked ~ .tabs #content3 { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* === CHALLENGE PANEL === */ +#challenge-container { + width: 15%; + background: var(--color-bg-dark); + margin: 12px 12px; + border: 2px solid var(--color-border-panel); + border-radius: var(--radius-large); + padding: 0 12px; +} + +.challenge-list { + list-style: none; + margin: 0; + padding: 0; +} + +.challenge-item { + padding: 10px; + margin: 5px 0; + background: #21262d; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + border-left: 3px solid transparent; + list-style: none; +} + +.challenge-item:hover { + background: #30363d; +} + +.challenge-item.active { + background: #1a3d2a; + border-left-color: #00ff88; +} + +.challenge-name { + font-weight: 500; + margin-bottom: 5px; +} + +.challenge-difficulty { + font-size: 0.8rem; + color: #0b949e; +} + +.challenge-difficulty.easy { + color: #3fb950; +} + +.challenge-difficulty.medium { + color: #d29922; +} + +.challenge-difficulty.hard { + color: #f85149; +} + +/* === REQUIREMENTS === */ +.requirements-section { + background: #161b22; + border: 1px solid #30363d; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.requirements-list { + margin: 0; + padding: 0; + list-style: none; +} + +.requirement-item { + position: relative; + padding: 8px 0 8px 25px; + margin: 0; + border-bottom: 1px solid #30363d; +} + +.requirement-item:before { + content: "✓"; + color: #00ff88; + position: absolute; + left: 0; +} + +/* === MODAL === */ +.modal { + position: absolute; + top: 30%; + left: 50%; + transform: translate(-50%, -30%); + background: #121212; + padding: 20px; + border-radius: 8px; + border: 1px solid #444; + z-index: 999; + color: #ccc; +} + +.modal-content label { + display: block; + margin: 10px 0; +} + +.modal-actions { + margin-top: 10px; + text-align: right; +} + +.modal input, +.modal select { + width: 100%; + padding: 6px; + margin-top: 4px; + background: #222; + border: 1px solid #444; + color: #fff; + border-radius: 4px; +} + +/* === MISC === */ +#score-panel { + margin-top: 16px; +} + +.userbox { + display: flex; + align-items: center; + gap: 12px; +} +.avatar { + width: 24px; + height: 24px; + border-radius: 12px; +} + diff --git a/static/success.html b/static/success.html new file mode 100644 index 0000000..c655b01 --- /dev/null +++ b/static/success.html @@ -0,0 +1,657 @@ + + + + + + System Success - System Design Game + + + + +
+
+

System Design Game

+
+
+ ✅ SYSTEM SUCCESS +
+
+
+ +
+
+

🏆 Mission Accomplished

+

+ Your architecture scaled successfully and met all performance targets. Well done! +

+ +
+
+
Target RPS
+
10,000
+
+
+
Achieved RPS
+
10,417
+
+
+
Max Latency
+
200ms
+
+
+
Actual Latency
+
87ms
+
+
+ +
+
[INFO] System initialized and scaled successfully
+
[INFO] Load balancer handled traffic with 0% errors
+
[INFO] Cache hit ratio: 97%
+
[SUCCESS] SLA met - all objectives achieved
+
+ +
+ + + 🏠 Main Menu + +
+
+
+ + +
+ + +