equilibria-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +240 -0
  4. package/dist/builders/YAMLBuilder.d.ts +77 -0
  5. package/dist/builders/YAMLBuilder.d.ts.map +1 -0
  6. package/dist/builders/YAMLBuilder.js +251 -0
  7. package/dist/builders/YAMLBuilder.js.map +1 -0
  8. package/dist/economics/constants.d.ts +39 -0
  9. package/dist/economics/constants.d.ts.map +1 -0
  10. package/dist/economics/constants.js +46 -0
  11. package/dist/economics/constants.js.map +1 -0
  12. package/dist/economics/formulas.d.ts +19 -0
  13. package/dist/economics/formulas.d.ts.map +1 -0
  14. package/dist/economics/formulas.js +260 -0
  15. package/dist/economics/formulas.js.map +1 -0
  16. package/dist/economics/index.d.ts +9 -0
  17. package/dist/economics/index.d.ts.map +1 -0
  18. package/dist/economics/index.js +9 -0
  19. package/dist/economics/index.js.map +1 -0
  20. package/dist/economics/validators.d.ts +18 -0
  21. package/dist/economics/validators.d.ts.map +1 -0
  22. package/dist/economics/validators.js +111 -0
  23. package/dist/economics/validators.js.map +1 -0
  24. package/dist/errors/index.d.ts +172 -0
  25. package/dist/errors/index.d.ts.map +1 -0
  26. package/dist/errors/index.js +313 -0
  27. package/dist/errors/index.js.map +1 -0
  28. package/dist/formatters/index.d.ts +8 -0
  29. package/dist/formatters/index.d.ts.map +1 -0
  30. package/dist/formatters/index.js +8 -0
  31. package/dist/formatters/index.js.map +1 -0
  32. package/dist/formatters/redux.d.ts +15 -0
  33. package/dist/formatters/redux.d.ts.map +1 -0
  34. package/dist/formatters/redux.js +35 -0
  35. package/dist/formatters/redux.js.map +1 -0
  36. package/dist/formatters/yaml.d.ts +18 -0
  37. package/dist/formatters/yaml.d.ts.map +1 -0
  38. package/dist/formatters/yaml.js +40 -0
  39. package/dist/formatters/yaml.js.map +1 -0
  40. package/dist/index.d.ts +11 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +19 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/server.d.ts +14 -0
  45. package/dist/server.d.ts.map +1 -0
  46. package/dist/server.js +86 -0
  47. package/dist/server.js.map +1 -0
  48. package/dist/templates/advancedMicro.d.ts +8 -0
  49. package/dist/templates/advancedMicro.d.ts.map +1 -0
  50. package/dist/templates/advancedMicro.js +834 -0
  51. package/dist/templates/advancedMicro.js.map +1 -0
  52. package/dist/templates/consumer.d.ts +12 -0
  53. package/dist/templates/consumer.d.ts.map +1 -0
  54. package/dist/templates/consumer.js +1978 -0
  55. package/dist/templates/consumer.js.map +1 -0
  56. package/dist/templates/elasticity.d.ts +8 -0
  57. package/dist/templates/elasticity.d.ts.map +1 -0
  58. package/dist/templates/elasticity.js +500 -0
  59. package/dist/templates/elasticity.js.map +1 -0
  60. package/dist/templates/externalities.d.ts +11 -0
  61. package/dist/templates/externalities.d.ts.map +1 -0
  62. package/dist/templates/externalities.js +997 -0
  63. package/dist/templates/externalities.js.map +1 -0
  64. package/dist/templates/financeBehavioral.d.ts +8 -0
  65. package/dist/templates/financeBehavioral.d.ts.map +1 -0
  66. package/dist/templates/financeBehavioral.js +860 -0
  67. package/dist/templates/financeBehavioral.js.map +1 -0
  68. package/dist/templates/growth.d.ts +8 -0
  69. package/dist/templates/growth.d.ts.map +1 -0
  70. package/dist/templates/growth.js +740 -0
  71. package/dist/templates/growth.js.map +1 -0
  72. package/dist/templates/index.d.ts +31 -0
  73. package/dist/templates/index.d.ts.map +1 -0
  74. package/dist/templates/index.js +91 -0
  75. package/dist/templates/index.js.map +1 -0
  76. package/dist/templates/inequality.d.ts +8 -0
  77. package/dist/templates/inequality.d.ts.map +1 -0
  78. package/dist/templates/inequality.js +562 -0
  79. package/dist/templates/inequality.js.map +1 -0
  80. package/dist/templates/intertemporalMacro.d.ts +8 -0
  81. package/dist/templates/intertemporalMacro.d.ts.map +1 -0
  82. package/dist/templates/intertemporalMacro.js +550 -0
  83. package/dist/templates/intertemporalMacro.js.map +1 -0
  84. package/dist/templates/isLM.d.ts +8 -0
  85. package/dist/templates/isLM.d.ts.map +1 -0
  86. package/dist/templates/isLM.js +747 -0
  87. package/dist/templates/isLM.js.map +1 -0
  88. package/dist/templates/macro.d.ts +8 -0
  89. package/dist/templates/macro.d.ts.map +1 -0
  90. package/dist/templates/macro.js +600 -0
  91. package/dist/templates/macro.js.map +1 -0
  92. package/dist/templates/marketStructures.d.ts +11 -0
  93. package/dist/templates/marketStructures.d.ts.map +1 -0
  94. package/dist/templates/marketStructures.js +1135 -0
  95. package/dist/templates/marketStructures.js.map +1 -0
  96. package/dist/templates/newKeynesian.d.ts +8 -0
  97. package/dist/templates/newKeynesian.d.ts.map +1 -0
  98. package/dist/templates/newKeynesian.js +633 -0
  99. package/dist/templates/newKeynesian.js.map +1 -0
  100. package/dist/templates/oligopoly.d.ts +11 -0
  101. package/dist/templates/oligopoly.d.ts.map +1 -0
  102. package/dist/templates/oligopoly.js +1113 -0
  103. package/dist/templates/oligopoly.js.map +1 -0
  104. package/dist/templates/ppf.d.ts +8 -0
  105. package/dist/templates/ppf.d.ts.map +1 -0
  106. package/dist/templates/ppf.js +439 -0
  107. package/dist/templates/ppf.js.map +1 -0
  108. package/dist/templates/producer.d.ts +11 -0
  109. package/dist/templates/producer.d.ts.map +1 -0
  110. package/dist/templates/producer.js +979 -0
  111. package/dist/templates/producer.js.map +1 -0
  112. package/dist/templates/production.d.ts +8 -0
  113. package/dist/templates/production.d.ts.map +1 -0
  114. package/dist/templates/production.js +574 -0
  115. package/dist/templates/production.js.map +1 -0
  116. package/dist/templates/supplyDemand.d.ts +8 -0
  117. package/dist/templates/supplyDemand.d.ts.map +1 -0
  118. package/dist/templates/supplyDemand.js +1282 -0
  119. package/dist/templates/supplyDemand.js.map +1 -0
  120. package/dist/templates/tradeGrowth.d.ts +8 -0
  121. package/dist/templates/tradeGrowth.d.ts.map +1 -0
  122. package/dist/templates/tradeGrowth.js +637 -0
  123. package/dist/templates/tradeGrowth.js.map +1 -0
  124. package/dist/tools/index.d.ts +25 -0
  125. package/dist/tools/index.d.ts.map +1 -0
  126. package/dist/tools/index.js +54 -0
  127. package/dist/tools/index.js.map +1 -0
  128. package/dist/tools/models.d.ts +8 -0
  129. package/dist/tools/models.d.ts.map +1 -0
  130. package/dist/tools/models.js +828 -0
  131. package/dist/tools/models.js.map +1 -0
  132. package/dist/tools/output.d.ts +8 -0
  133. package/dist/tools/output.d.ts.map +1 -0
  134. package/dist/tools/output.js +236 -0
  135. package/dist/tools/output.js.map +1 -0
  136. package/dist/tools/templates.d.ts +8 -0
  137. package/dist/tools/templates.d.ts.map +1 -0
  138. package/dist/tools/templates.js +247 -0
  139. package/dist/tools/templates.js.map +1 -0
  140. package/dist/tools/validation.d.ts +8 -0
  141. package/dist/tools/validation.d.ts.map +1 -0
  142. package/dist/tools/validation.js +181 -0
  143. package/dist/tools/validation.js.map +1 -0
  144. package/dist/types/index.d.ts +187 -0
  145. package/dist/types/index.d.ts.map +1 -0
  146. package/dist/types/index.js +7 -0
  147. package/dist/types/index.js.map +1 -0
  148. package/dist/utils/cache.d.ts +99 -0
  149. package/dist/utils/cache.d.ts.map +1 -0
  150. package/dist/utils/cache.js +192 -0
  151. package/dist/utils/cache.js.map +1 -0
  152. package/dist/utils/index.d.ts +8 -0
  153. package/dist/utils/index.d.ts.map +1 -0
  154. package/dist/utils/index.js +8 -0
  155. package/dist/utils/index.js.map +1 -0
  156. package/dist/utils/logger.d.ts +128 -0
  157. package/dist/utils/logger.d.ts.map +1 -0
  158. package/dist/utils/logger.js +251 -0
  159. package/dist/utils/logger.js.map +1 -0
  160. package/dist/validation/index.d.ts +42 -0
  161. package/dist/validation/index.d.ts.map +1 -0
  162. package/dist/validation/index.js +282 -0
  163. package/dist/validation/index.js.map +1 -0
  164. package/package.json +73 -0
@@ -0,0 +1,1113 @@
1
+ /**
2
+ * Oligopoly Templates
3
+ *
4
+ * Templates for oligopoly models including Cournot, Bertrand, Stackelberg,
5
+ * kinked demand, and monopolistic competition.
6
+ *
7
+ * Phase 3.5: Market Structures - Oligopoly
8
+ */
9
+ // ============================================================================
10
+ // PHASE 3.5: OLIGOPOLY
11
+ // ============================================================================
12
+ /**
13
+ * Cournot Duopoly
14
+ *
15
+ * Simultaneous quantity competition between two firms.
16
+ * Shows reaction functions and Nash equilibrium.
17
+ */
18
+ const cournotDuopoly = {
19
+ id: 'cournot_duopoly',
20
+ name: 'Cournot Duopoly',
21
+ description: 'Simultaneous quantity competition. Each firm chooses output taking rival\'s output as given. Nash equilibrium at intersection of reaction functions.',
22
+ category: 'market_structures',
23
+ level: 'undergraduate',
24
+ tags: ['Cournot', 'oligopoly', 'quantity competition', 'reaction function', 'Nash equilibrium'],
25
+ parameters: {
26
+ demandIntercept: {
27
+ type: 'number',
28
+ default: 100,
29
+ min: 50,
30
+ max: 200,
31
+ description: 'Market demand intercept (a)',
32
+ },
33
+ demandSlope: {
34
+ type: 'number',
35
+ default: 1,
36
+ min: 0.5,
37
+ max: 3,
38
+ description: 'Market demand slope (b)',
39
+ },
40
+ mc1: {
41
+ type: 'number',
42
+ default: 10,
43
+ min: 0,
44
+ max: 40,
45
+ description: 'Firm 1 marginal cost',
46
+ },
47
+ mc2: {
48
+ type: 'number',
49
+ default: 10,
50
+ min: 0,
51
+ max: 40,
52
+ description: 'Firm 2 marginal cost',
53
+ },
54
+ },
55
+ generate: (params) => {
56
+ const a = params.demandIntercept ?? 100;
57
+ const b = params.demandSlope ?? 1;
58
+ const c1 = params.mc1 ?? 10;
59
+ const c2 = params.mc2 ?? 10;
60
+ return {
61
+ metadata: {
62
+ specVersion: '1.3',
63
+ title: 'Cournot Duopoly',
64
+ description: 'Simultaneous quantity competition',
65
+ },
66
+ parameters: {
67
+ a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
68
+ b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
69
+ c1: { value: c1, label: 'MC₁', min: 0, max: 40 },
70
+ c2: { value: c2, label: 'MC₂', min: 0, max: 40 },
71
+ // Reaction functions:
72
+ // q1 = (a - c1 - b*q2) / (2b) = (a - c1)/(2b) - q2/2
73
+ // q2 = (a - c2 - b*q1) / (2b) = (a - c2)/(2b) - q1/2
74
+ // Nash equilibrium (solving simultaneously):
75
+ // q1* = (a - 2c1 + c2) / (3b)
76
+ // q2* = (a - 2c2 + c1) / (3b)
77
+ q1_star: {
78
+ expression: '(a - 2 * c1 + c2) / (3 * b)',
79
+ label: 'q₁*',
80
+ readonly: true,
81
+ },
82
+ q2_star: {
83
+ expression: '(a - 2 * c2 + c1) / (3 * b)',
84
+ label: 'q₂*',
85
+ readonly: true,
86
+ },
87
+ // Total output and price
88
+ Q_star: { expression: 'q1_star + q2_star', label: 'Q*', readonly: true },
89
+ P_star: { expression: 'a - b * Q_star', label: 'P*', readonly: true },
90
+ // Profits
91
+ profit1: {
92
+ expression: '(P_star - c1) * q1_star',
93
+ label: 'π₁',
94
+ readonly: true,
95
+ },
96
+ profit2: {
97
+ expression: '(P_star - c2) * q2_star',
98
+ label: 'π₂',
99
+ readonly: true,
100
+ },
101
+ // Reaction function intercepts
102
+ rf1_intercept: { expression: '(a - c1) / (2 * b)', hidden: true },
103
+ rf2_intercept: { expression: '(a - c2) / (2 * b)', hidden: true },
104
+ // Monopoly output for comparison
105
+ q_monopoly: { expression: '(a - c1) / (2 * b)', hidden: true },
106
+ // Competitive output
107
+ q_comp: { expression: '(a - c1) / b', hidden: true },
108
+ // Chart bounds
109
+ chartMax: { expression: 'max(rf1_intercept, rf2_intercept) * 1.2', hidden: true },
110
+ },
111
+ charts: [
112
+ {
113
+ id: 'reaction',
114
+ title: 'Reaction Functions',
115
+ xAxis: { label: 'Firm 1 Output (q₁)', min: 0, max: 'chartMax' },
116
+ yAxis: { label: 'Firm 2 Output (q₂)', min: 0, max: 'chartMax' },
117
+ elements: [
118
+ // Firm 1 reaction function: q1 = (a-c1)/(2b) - q2/2
119
+ // Rearranged as q2 in terms of q1: q2 = (a-c1)/b - 2*q1
120
+ {
121
+ id: 'rf1',
122
+ type: 'line',
123
+ equation: '(a - c1) / b - 2 * x',
124
+ color: '#2563EB',
125
+ strokeWidth: 3,
126
+ label: 'RF₁ (Firm 1 BR)',
127
+ domain: { min: 0, max: 'rf1_intercept' },
128
+ },
129
+ // Firm 2 reaction function: q2 = (a-c2)/(2b) - q1/2
130
+ {
131
+ id: 'rf2',
132
+ type: 'line',
133
+ equation: '(a - c2) / (2 * b) - x / 2',
134
+ color: '#DC2626',
135
+ strokeWidth: 3,
136
+ label: 'RF₂ (Firm 2 BR)',
137
+ domain: { min: 0, max: '(a - c2) / b' },
138
+ },
139
+ // Nash equilibrium
140
+ {
141
+ id: 'nash',
142
+ type: 'point',
143
+ x: 'q1_star',
144
+ y: 'q2_star',
145
+ color: '#16A34A',
146
+ size: 12,
147
+ label: 'Nash Equilibrium',
148
+ droplines: { x: true, y: true },
149
+ },
150
+ // 45-degree line (equal outputs)
151
+ {
152
+ id: 'equal-line',
153
+ type: 'line',
154
+ equation: 'x',
155
+ color: '#9CA3AF',
156
+ strokeWidth: 1,
157
+ lineStyle: 'dotted',
158
+ },
159
+ ],
160
+ annotations: [
161
+ {
162
+ id: 'nash-label',
163
+ text: '(${q1_star:.1f}, ${q2_star:.1f})',
164
+ x: 'q1_star + chartMax * 0.05',
165
+ y: 'q2_star + chartMax * 0.05',
166
+ color: '#16A34A',
167
+ },
168
+ {
169
+ id: 'rf1-label',
170
+ text: 'q₁ = (a-c₁)/2b - q₂/2',
171
+ x: 'chartMax * 0.6',
172
+ y: 'chartMax * 0.9',
173
+ color: '#2563EB',
174
+ },
175
+ {
176
+ id: 'rf2-label',
177
+ text: 'q₂ = (a-c₂)/2b - q₁/2',
178
+ x: 'chartMax * 0.6',
179
+ y: 'chartMax * 0.8',
180
+ color: '#DC2626',
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ id: 'market',
186
+ title: 'Market Outcome',
187
+ xAxis: { label: 'Total Quantity (Q)', min: 0, max: 'q_comp * 1.2' },
188
+ yAxis: { label: 'Price', min: 0, max: 'a * 1.1' },
189
+ elements: [
190
+ // Demand curve
191
+ {
192
+ id: 'demand',
193
+ type: 'line',
194
+ equation: 'a - b * x',
195
+ color: '#2563EB',
196
+ strokeWidth: 3,
197
+ label: 'Market Demand',
198
+ domain: { min: 0, max: 'a / b' },
199
+ },
200
+ // MC (average of both)
201
+ {
202
+ id: 'mc',
203
+ type: 'horizontalLine',
204
+ y: '(c1 + c2) / 2',
205
+ color: '#DC2626',
206
+ strokeWidth: 2,
207
+ lineStyle: 'dashed',
208
+ label: 'Avg MC',
209
+ },
210
+ // Cournot equilibrium
211
+ {
212
+ id: 'cournot-point',
213
+ type: 'point',
214
+ x: 'Q_star',
215
+ y: 'P_star',
216
+ color: '#16A34A',
217
+ size: 10,
218
+ label: 'Cournot',
219
+ droplines: { x: true, y: true },
220
+ },
221
+ ],
222
+ annotations: [
223
+ {
224
+ id: 'outcome',
225
+ text: 'P* = $${P_star:.1f}, Q* = ${Q_star:.1f}',
226
+ x: 'q_comp * 0.5',
227
+ y: 'a * 0.9',
228
+ color: '#16A34A',
229
+ },
230
+ {
231
+ id: 'profits',
232
+ text: 'π₁ = $${profit1:.0f}, π₂ = $${profit2:.0f}',
233
+ x: 'q_comp * 0.5',
234
+ y: 'a * 0.8',
235
+ color: '#6B7280',
236
+ },
237
+ ],
238
+ },
239
+ ],
240
+ };
241
+ },
242
+ };
243
+ /**
244
+ * Bertrand Duopoly
245
+ *
246
+ * Simultaneous price competition with homogeneous products.
247
+ * Results in competitive (marginal cost) pricing.
248
+ */
249
+ const bertrandDuopoly = {
250
+ id: 'bertrand_duopoly',
251
+ name: 'Bertrand Duopoly',
252
+ description: 'Simultaneous price competition with identical products. In equilibrium, both firms price at marginal cost (competitive outcome). The "Bertrand paradox."',
253
+ category: 'market_structures',
254
+ level: 'undergraduate',
255
+ tags: ['Bertrand', 'oligopoly', 'price competition', 'Bertrand paradox'],
256
+ parameters: {
257
+ demandIntercept: {
258
+ type: 'number',
259
+ default: 100,
260
+ description: 'Market demand intercept',
261
+ },
262
+ demandSlope: {
263
+ type: 'number',
264
+ default: 1,
265
+ description: 'Market demand slope',
266
+ },
267
+ marginalCost: {
268
+ type: 'number',
269
+ default: 20,
270
+ description: 'Common marginal cost',
271
+ },
272
+ firm1Price: {
273
+ type: 'number',
274
+ default: 30,
275
+ description: 'Firm 1 price choice (for illustration)',
276
+ },
277
+ },
278
+ generate: (params) => {
279
+ const a = params.demandIntercept ?? 100;
280
+ const b = params.demandSlope ?? 1;
281
+ const MC = params.marginalCost ?? 20;
282
+ const p1 = params.firm1Price ?? 30;
283
+ return {
284
+ metadata: {
285
+ specVersion: '1.3',
286
+ title: 'Bertrand Duopoly',
287
+ description: 'Price competition with homogeneous products',
288
+ },
289
+ parameters: {
290
+ a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
291
+ b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
292
+ MC: { value: MC, label: 'Marginal Cost', min: 5, max: 50 },
293
+ p1: { value: p1, label: 'Firm 1 Price', min: 'MC', max: 'a' },
294
+ // Bertrand equilibrium: P1 = P2 = MC
295
+ P_star: { expression: 'MC', label: 'P* (equilibrium)', readonly: true },
296
+ // Total quantity at equilibrium
297
+ Q_star: { expression: '(a - MC) / b', label: 'Q*', readonly: true },
298
+ // Each firm gets half (symmetric)
299
+ q_each: { expression: 'Q_star / 2', label: 'q each', readonly: true },
300
+ // Zero profit in equilibrium
301
+ profit_eq: { value: 0, label: 'π* (each)', readonly: true },
302
+ // Compare to monopoly
303
+ P_monopoly: { expression: '(a + MC) / 2', label: 'P (monopoly)', readonly: true },
304
+ Q_monopoly: { expression: '(a - MC) / (2 * b)', hidden: true },
305
+ // What happens at p1 (illustration)
306
+ Q_at_p1: { expression: '(a - p1) / b', hidden: true },
307
+ profit_at_p1: {
308
+ expression: '(p1 - MC) * Q_at_p1',
309
+ label: 'π if monopolist',
310
+ readonly: true,
311
+ },
312
+ // Chart bounds
313
+ chartMaxQ: { expression: 'Q_star * 1.3', hidden: true },
314
+ chartMaxP: { expression: 'a * 1.1', hidden: true },
315
+ },
316
+ charts: [
317
+ {
318
+ id: 'undercutting',
319
+ title: 'Bertrand Price Competition',
320
+ xAxis: { label: 'Firm 1 Price (P₁)', min: 0, max: 'chartMaxP' },
321
+ yAxis: { label: 'Firm 2 Best Response (P₂)', min: 0, max: 'chartMaxP' },
322
+ elements: [
323
+ // Firm 2 best response: undercut P1 if P1 > MC
324
+ // At P1 = MC, indifferent between MC and not entering
325
+ // For P1 > MC: BR is P2 = P1 - ε (just below P1)
326
+ // This creates the "undercut" dynamic
327
+ // 45-degree line (match price)
328
+ {
329
+ id: 'match-line',
330
+ type: 'line',
331
+ equation: 'x',
332
+ color: '#2563EB',
333
+ strokeWidth: 3,
334
+ label: 'Match Price (P₂ = P₁)',
335
+ domain: { min: 'MC', max: 'chartMaxP' },
336
+ },
337
+ // MC horizontal
338
+ {
339
+ id: 'mc-line',
340
+ type: 'horizontalLine',
341
+ y: 'MC',
342
+ color: '#DC2626',
343
+ strokeWidth: 3,
344
+ label: 'MC',
345
+ },
346
+ // MC vertical
347
+ {
348
+ id: 'mc-vertical',
349
+ type: 'verticalLine',
350
+ x: 'MC',
351
+ color: '#DC2626',
352
+ strokeWidth: 3,
353
+ },
354
+ // Nash equilibrium
355
+ {
356
+ id: 'nash',
357
+ type: 'point',
358
+ x: 'MC',
359
+ y: 'MC',
360
+ color: '#16A34A',
361
+ size: 12,
362
+ label: 'Nash: P₁ = P₂ = MC',
363
+ },
364
+ // Current p1 illustration
365
+ {
366
+ id: 'p1-point',
367
+ type: 'point',
368
+ x: 'p1',
369
+ y: 'p1',
370
+ color: '#7C3AED',
371
+ label: 'If P₁ = ${p1}',
372
+ droplines: { x: true, y: true },
373
+ },
374
+ ],
375
+ annotations: [
376
+ {
377
+ id: 'undercutting-note',
378
+ text: 'Any P > MC invites undercutting',
379
+ x: 'chartMaxP * 0.5',
380
+ y: 'chartMaxP * 0.8',
381
+ color: '#6B7280',
382
+ },
383
+ {
384
+ id: 'paradox',
385
+ text: 'Bertrand Paradox: Only 2 firms → competitive price!',
386
+ x: 'chartMaxP * 0.4',
387
+ y: 'chartMaxP * 0.15',
388
+ color: '#DC2626',
389
+ },
390
+ ],
391
+ },
392
+ {
393
+ id: 'market',
394
+ title: 'Market Outcome',
395
+ xAxis: { label: 'Quantity (Q)', min: 0, max: 'chartMaxQ' },
396
+ yAxis: { label: 'Price', min: 0, max: 'chartMaxP' },
397
+ elements: [
398
+ // Demand
399
+ {
400
+ id: 'demand',
401
+ type: 'line',
402
+ equation: 'a - b * x',
403
+ color: '#2563EB',
404
+ strokeWidth: 3,
405
+ label: 'Demand',
406
+ domain: { min: 0, max: 'a / b' },
407
+ },
408
+ // MC
409
+ {
410
+ id: 'mc',
411
+ type: 'horizontalLine',
412
+ y: 'MC',
413
+ color: '#DC2626',
414
+ strokeWidth: 3,
415
+ label: 'MC = P*',
416
+ },
417
+ // Bertrand equilibrium
418
+ {
419
+ id: 'bertrand-eq',
420
+ type: 'point',
421
+ x: 'Q_star',
422
+ y: 'MC',
423
+ color: '#16A34A',
424
+ size: 10,
425
+ label: 'Bertrand Eq.',
426
+ droplines: { x: true, y: true },
427
+ },
428
+ // Monopoly point for comparison
429
+ {
430
+ id: 'monopoly-point',
431
+ type: 'point',
432
+ x: 'Q_monopoly',
433
+ y: 'P_monopoly',
434
+ color: '#9CA3AF',
435
+ size: 6,
436
+ label: 'Monopoly',
437
+ },
438
+ ],
439
+ annotations: [
440
+ {
441
+ id: 'bertrand-result',
442
+ text: 'P* = MC = $${MC}, Q* = ${Q_star:.0f}',
443
+ x: 'chartMaxQ * 0.5',
444
+ y: 'chartMaxP * 0.9',
445
+ color: '#16A34A',
446
+ },
447
+ {
448
+ id: 'zero-profit',
449
+ text: 'π₁ = π₂ = 0 (competitive outcome)',
450
+ x: 'chartMaxQ * 0.5',
451
+ y: 'chartMaxP * 0.8',
452
+ color: '#6B7280',
453
+ },
454
+ ],
455
+ },
456
+ ],
457
+ };
458
+ },
459
+ };
460
+ /**
461
+ * Stackelberg Duopoly
462
+ *
463
+ * Sequential quantity competition with a first-mover (leader)
464
+ * and a follower. Leader has first-mover advantage.
465
+ */
466
+ const stackelbergDuopoly = {
467
+ id: 'stackelberg_duopoly',
468
+ name: 'Stackelberg Duopoly',
469
+ description: 'Sequential quantity competition. Leader moves first, follower observes and responds. Leader has first-mover advantage and produces more than follower.',
470
+ category: 'market_structures',
471
+ level: 'undergraduate',
472
+ tags: ['Stackelberg', 'oligopoly', 'sequential game', 'first-mover advantage', 'leader-follower'],
473
+ parameters: {
474
+ demandIntercept: {
475
+ type: 'number',
476
+ default: 100,
477
+ description: 'Demand intercept',
478
+ },
479
+ demandSlope: {
480
+ type: 'number',
481
+ default: 1,
482
+ description: 'Demand slope',
483
+ },
484
+ marginalCost: {
485
+ type: 'number',
486
+ default: 10,
487
+ description: 'Common marginal cost',
488
+ },
489
+ },
490
+ generate: (params) => {
491
+ const a = params.demandIntercept ?? 100;
492
+ const b = params.demandSlope ?? 1;
493
+ const c = params.marginalCost ?? 10;
494
+ return {
495
+ metadata: {
496
+ specVersion: '1.3',
497
+ title: 'Stackelberg Duopoly',
498
+ description: 'Sequential quantity competition',
499
+ },
500
+ parameters: {
501
+ a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
502
+ b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
503
+ c: { value: c, label: 'Marginal Cost', min: 0, max: 40 },
504
+ // Follower's reaction function: q2 = (a - c)/(2b) - q1/2
505
+ // Leader maximizes: π1 = (a - b*(q1 + q2) - c) * q1
506
+ // Substituting follower's RF: q1* = (a - c) / (2b)
507
+ q_leader: {
508
+ expression: '(a - c) / (2 * b)',
509
+ label: 'q_L (leader)',
510
+ readonly: true,
511
+ },
512
+ // Follower's response: q2 = (a-c)/(2b) - q1/2 = (a-c)/(4b)
513
+ q_follower: {
514
+ expression: '(a - c) / (4 * b)',
515
+ label: 'q_F (follower)',
516
+ readonly: true,
517
+ },
518
+ // Total output
519
+ Q_stack: { expression: 'q_leader + q_follower', label: 'Q (total)', readonly: true },
520
+ // Price
521
+ P_stack: { expression: 'a - b * Q_stack', label: 'P*', readonly: true },
522
+ // Profits
523
+ profit_leader: {
524
+ expression: '(P_stack - c) * q_leader',
525
+ label: 'π_L',
526
+ readonly: true,
527
+ },
528
+ profit_follower: {
529
+ expression: '(P_stack - c) * q_follower',
530
+ label: 'π_F',
531
+ readonly: true,
532
+ },
533
+ // Cournot for comparison
534
+ q_cournot: { expression: '(a - c) / (3 * b)', hidden: true },
535
+ // Chart bounds
536
+ chartMax: { expression: '(a - c) / b * 0.7', hidden: true },
537
+ },
538
+ charts: [
539
+ {
540
+ id: 'reaction',
541
+ title: 'Stackelberg Equilibrium',
542
+ xAxis: { label: 'Leader Output (q_L)', min: 0, max: 'chartMax' },
543
+ yAxis: { label: 'Follower Output (q_F)', min: 0, max: 'chartMax' },
544
+ elements: [
545
+ // Follower's reaction function
546
+ {
547
+ id: 'rf-follower',
548
+ type: 'line',
549
+ equation: '(a - c) / (2 * b) - x / 2',
550
+ color: '#DC2626',
551
+ strokeWidth: 3,
552
+ label: 'Follower RF',
553
+ domain: { min: 0, max: '(a - c) / b' },
554
+ },
555
+ // Leader's isoprofit curves (higher profit toward origin)
556
+ // π_L = (a - b*(q1 + q2) - c) * q1
557
+ // For a given profit level π: q2 = (a-c)/b - q1 - π/(b*q1)
558
+ {
559
+ id: 'isoprofit-leader',
560
+ type: 'line',
561
+ equation: '(a - c) / b - x - profit_leader / (b * x)',
562
+ color: '#2563EB',
563
+ strokeWidth: 2,
564
+ lineStyle: 'dashed',
565
+ label: 'Leader Isoprofit',
566
+ domain: { min: 'q_leader * 0.3', max: 'q_leader * 1.5' },
567
+ },
568
+ // Stackelberg equilibrium
569
+ {
570
+ id: 'stackelberg-eq',
571
+ type: 'point',
572
+ x: 'q_leader',
573
+ y: 'q_follower',
574
+ color: '#16A34A',
575
+ size: 12,
576
+ label: 'Stackelberg',
577
+ droplines: { x: true, y: true },
578
+ },
579
+ // Cournot for comparison
580
+ {
581
+ id: 'cournot-eq',
582
+ type: 'point',
583
+ x: 'q_cournot',
584
+ y: 'q_cournot',
585
+ color: '#9CA3AF',
586
+ size: 8,
587
+ label: 'Cournot',
588
+ },
589
+ // 45-degree line
590
+ {
591
+ id: 'equal-line',
592
+ type: 'line',
593
+ equation: 'x',
594
+ color: '#E5E7EB',
595
+ strokeWidth: 1,
596
+ lineStyle: 'dotted',
597
+ },
598
+ ],
599
+ annotations: [
600
+ {
601
+ id: 'leader-advantage',
602
+ text: 'Leader: ${q_leader:.1f} > Follower: ${q_follower:.1f}',
603
+ x: 'chartMax * 0.5',
604
+ y: 'chartMax * 0.9',
605
+ color: '#16A34A',
606
+ },
607
+ {
608
+ id: 'tangency',
609
+ text: 'Tangency = optimal commitment',
610
+ x: 'q_leader * 1.1',
611
+ y: 'q_follower * 1.3',
612
+ color: '#2563EB',
613
+ },
614
+ ],
615
+ },
616
+ {
617
+ id: 'profit-compare',
618
+ title: 'Profit Comparison',
619
+ xAxis: { label: 'Firm', min: 0, max: 3 },
620
+ yAxis: { label: 'Profit', min: 0, max: 'profit_leader * 1.5' },
621
+ elements: [
622
+ // Leader profit bar
623
+ {
624
+ id: 'leader-bar',
625
+ type: 'rectangle',
626
+ x1: 0.5,
627
+ y1: 0,
628
+ x2: 1.5,
629
+ y2: 'profit_leader',
630
+ fill: '#2563EB',
631
+ opacity: 0.7,
632
+ },
633
+ // Follower profit bar
634
+ {
635
+ id: 'follower-bar',
636
+ type: 'rectangle',
637
+ x1: 1.5,
638
+ y1: 0,
639
+ x2: 2.5,
640
+ y2: 'profit_follower',
641
+ fill: '#DC2626',
642
+ opacity: 0.7,
643
+ },
644
+ ],
645
+ annotations: [
646
+ {
647
+ id: 'leader-label',
648
+ text: 'Leader: $${profit_leader:.0f}',
649
+ x: 1,
650
+ y: 'profit_leader * 1.1',
651
+ color: '#2563EB',
652
+ },
653
+ {
654
+ id: 'follower-label',
655
+ text: 'Follower: $${profit_follower:.0f}',
656
+ x: 2,
657
+ y: 'profit_follower * 1.1',
658
+ color: '#DC2626',
659
+ },
660
+ ],
661
+ },
662
+ ],
663
+ };
664
+ },
665
+ };
666
+ /**
667
+ * Kinked Demand Curve
668
+ *
669
+ * Oligopoly model explaining price rigidity through
670
+ * asymmetric responses to price changes.
671
+ */
672
+ const kinkedDemand = {
673
+ id: 'kinked_demand_oligopoly',
674
+ name: 'Kinked Demand Curve',
675
+ description: 'Model explaining price rigidity in oligopoly. Rivals match price cuts but not increases, creating a kink in demand and discontinuity in MR.',
676
+ category: 'market_structures',
677
+ level: 'undergraduate',
678
+ tags: ['kinked demand', 'oligopoly', 'price rigidity', 'price stickiness'],
679
+ parameters: {
680
+ currentPrice: {
681
+ type: 'number',
682
+ default: 50,
683
+ description: 'Current (kinked) price',
684
+ },
685
+ currentQuantity: {
686
+ type: 'number',
687
+ default: 50,
688
+ description: 'Current quantity at kink',
689
+ },
690
+ elasticSlope: {
691
+ type: 'number',
692
+ default: 0.5,
693
+ description: 'Slope for price increases (more elastic)',
694
+ },
695
+ inelasticSlope: {
696
+ type: 'number',
697
+ default: 2,
698
+ description: 'Slope for price decreases (less elastic)',
699
+ },
700
+ },
701
+ generate: (params) => {
702
+ const P_kink = params.currentPrice ?? 50;
703
+ const Q_kink = params.currentQuantity ?? 50;
704
+ const b_up = params.elasticSlope ?? 0.5;
705
+ const b_down = params.inelasticSlope ?? 2;
706
+ return {
707
+ metadata: {
708
+ specVersion: '1.3',
709
+ title: 'Kinked Demand Curve',
710
+ description: 'Price rigidity in oligopoly',
711
+ },
712
+ parameters: {
713
+ P_kink: { value: P_kink, label: 'Current Price', min: 20, max: 100 },
714
+ Q_kink: { value: Q_kink, label: 'Current Quantity', min: 20, max: 100 },
715
+ b_up: { value: b_up, label: 'Elastic Slope', min: 0.2, max: 1, step: 0.1 },
716
+ b_down: { value: b_down, label: 'Inelastic Slope', min: 1, max: 4, step: 0.1 },
717
+ // Demand intercepts (solving P = a - bQ at kink point)
718
+ a_up: { expression: 'P_kink + b_up * Q_kink', hidden: true },
719
+ a_down: { expression: 'P_kink + b_down * Q_kink', hidden: true },
720
+ // MR discontinuity at kink
721
+ MR_upper: { expression: 'a_up - 2 * b_up * Q_kink', hidden: true },
722
+ MR_lower: { expression: 'a_down - 2 * b_down * Q_kink', hidden: true },
723
+ MR_gap: {
724
+ expression: 'MR_upper - MR_lower',
725
+ label: 'MR Gap',
726
+ readonly: true,
727
+ },
728
+ // Chart bounds
729
+ chartMaxQ: { expression: 'Q_kink * 2', hidden: true },
730
+ chartMaxP: { expression: 'P_kink * 2', hidden: true },
731
+ },
732
+ charts: [
733
+ {
734
+ id: 'main',
735
+ title: 'Kinked Demand Curve',
736
+ xAxis: { label: 'Quantity (Q)', min: 0, max: 'chartMaxQ' },
737
+ yAxis: { label: 'Price', min: 0, max: 'chartMaxP' },
738
+ elements: [
739
+ // Upper demand segment (elastic - for price increases)
740
+ {
741
+ id: 'demand-upper',
742
+ type: 'line',
743
+ equation: 'a_up - b_up * x',
744
+ color: '#2563EB',
745
+ strokeWidth: 3,
746
+ label: 'Elastic (↑P → rivals don\'t follow)',
747
+ domain: { min: 0, max: 'Q_kink' },
748
+ },
749
+ // Lower demand segment (inelastic - for price cuts)
750
+ {
751
+ id: 'demand-lower',
752
+ type: 'line',
753
+ equation: 'a_down - b_down * x',
754
+ color: '#2563EB',
755
+ strokeWidth: 3,
756
+ label: 'Inelastic (↓P → rivals match)',
757
+ domain: { min: 'Q_kink', max: 'a_down / b_down' },
758
+ },
759
+ // Upper MR segment
760
+ {
761
+ id: 'mr-upper',
762
+ type: 'line',
763
+ equation: 'a_up - 2 * b_up * x',
764
+ color: '#7C3AED',
765
+ strokeWidth: 2,
766
+ label: 'MR (upper)',
767
+ domain: { min: 0, max: 'Q_kink' },
768
+ },
769
+ // Lower MR segment
770
+ {
771
+ id: 'mr-lower',
772
+ type: 'line',
773
+ equation: 'a_down - 2 * b_down * x',
774
+ color: '#7C3AED',
775
+ strokeWidth: 2,
776
+ label: 'MR (lower)',
777
+ domain: { min: 'Q_kink', max: 'a_down / (2 * b_down)' },
778
+ },
779
+ // MR gap (vertical discontinuity)
780
+ {
781
+ id: 'mr-gap',
782
+ type: 'verticalLine',
783
+ x: 'Q_kink',
784
+ color: '#7C3AED',
785
+ strokeWidth: 3,
786
+ domain: { min: 'MR_lower', max: 'MR_upper' },
787
+ },
788
+ // Kink point
789
+ {
790
+ id: 'kink',
791
+ type: 'point',
792
+ x: 'Q_kink',
793
+ y: 'P_kink',
794
+ color: '#DC2626',
795
+ size: 12,
796
+ label: 'Kink',
797
+ droplines: { x: true, y: true },
798
+ },
799
+ // Example MC in the gap (explaining price rigidity)
800
+ {
801
+ id: 'mc-example',
802
+ type: 'horizontalLine',
803
+ y: '(MR_upper + MR_lower) / 2',
804
+ color: '#DC2626',
805
+ strokeWidth: 2,
806
+ lineStyle: 'dashed',
807
+ label: 'MC (in gap)',
808
+ },
809
+ ],
810
+ annotations: [
811
+ {
812
+ id: 'rigidity-label',
813
+ text: 'MC can shift within gap',
814
+ x: 'Q_kink * 1.3',
815
+ y: '(MR_upper + MR_lower) / 2',
816
+ color: '#DC2626',
817
+ },
818
+ {
819
+ id: 'no-price-change',
820
+ text: '→ Price stays at $${P_kink}',
821
+ x: 'Q_kink * 1.3',
822
+ y: '(MR_upper + MR_lower) / 2 - chartMaxP * 0.1',
823
+ color: '#DC2626',
824
+ },
825
+ {
826
+ id: 'gap-size',
827
+ text: 'MR gap = $${MR_gap:.1f}',
828
+ x: 'Q_kink + chartMaxQ * 0.05',
829
+ y: 'MR_upper',
830
+ color: '#7C3AED',
831
+ },
832
+ ],
833
+ },
834
+ ],
835
+ };
836
+ },
837
+ };
838
+ /**
839
+ * Monopolistic Competition
840
+ *
841
+ * Market with many firms selling differentiated products.
842
+ * Shows short-run profit and long-run zero-profit tangency.
843
+ */
844
+ const monopolisticCompetition = {
845
+ id: 'monopolistic_competition',
846
+ name: 'Monopolistic Competition',
847
+ description: 'Many firms with differentiated products. Short-run profits attract entry until long-run equilibrium where demand is tangent to ATC (zero profit).',
848
+ category: 'market_structures',
849
+ level: 'undergraduate',
850
+ tags: ['monopolistic competition', 'product differentiation', 'free entry', 'tangency'],
851
+ parameters: {
852
+ demandInterceptSR: {
853
+ type: 'number',
854
+ default: 100,
855
+ description: 'Short-run demand intercept',
856
+ },
857
+ demandSlope: {
858
+ type: 'number',
859
+ default: 2,
860
+ description: 'Demand slope',
861
+ },
862
+ minATC: {
863
+ type: 'number',
864
+ default: 30,
865
+ description: 'Minimum average total cost',
866
+ },
867
+ efficientScale: {
868
+ type: 'number',
869
+ default: 20,
870
+ description: 'Output at minimum ATC',
871
+ },
872
+ atcCurvature: {
873
+ type: 'number',
874
+ default: 0.05,
875
+ description: 'ATC curvature parameter',
876
+ },
877
+ },
878
+ generate: (params) => {
879
+ const a_sr = params.demandInterceptSR ?? 100;
880
+ const b = params.demandSlope ?? 2;
881
+ const minATC = params.minATC ?? 30;
882
+ const Q_eff = params.efficientScale ?? 20;
883
+ const c = params.atcCurvature ?? 0.05;
884
+ return {
885
+ metadata: {
886
+ specVersion: '1.3',
887
+ title: 'Monopolistic Competition',
888
+ description: 'Short-run and long-run equilibrium',
889
+ },
890
+ parameters: {
891
+ a_sr: { value: a_sr, label: 'SR Demand Intercept', min: 60, max: 150 },
892
+ b: { value: b, label: 'Demand Slope', min: 1, max: 5, step: 0.1 },
893
+ minATC: { value: minATC, label: 'Min ATC', min: 10, max: 60 },
894
+ Q_eff: { value: Q_eff, label: 'Efficient Scale', min: 10, max: 40 },
895
+ c: { value: c, label: 'ATC Curvature', min: 0.01, max: 0.1, step: 0.01 },
896
+ // ATC: minATC + c*(Q - Q_eff)²
897
+ // MC: derivative of TC = minATC + 2c*(Q - Q_eff) + 2*c*Q_eff (approximately)
898
+ // Short-run: MR = MC at Q_sr, then P_sr from demand
899
+ // For simplicity, calculate SR equilibrium numerically
900
+ Q_sr: { expression: '(a_sr - minATC) / (2 * b + 2 * c)', label: 'Q_SR', readonly: true },
901
+ P_sr: { expression: 'a_sr - b * Q_sr', label: 'P_SR', readonly: true },
902
+ ATC_sr: {
903
+ expression: 'minATC + c * ((Q_sr - Q_eff) ^ 2)',
904
+ label: 'ATC at Q_SR',
905
+ readonly: true,
906
+ },
907
+ profit_sr: {
908
+ expression: '(P_sr - ATC_sr) * Q_sr',
909
+ label: 'SR Profit',
910
+ readonly: true,
911
+ },
912
+ // Long-run: demand tangent to ATC at zero profit
913
+ // At tangency: slope of demand = slope of ATC
914
+ // -b = 2c(Q - Q_eff), and P = ATC
915
+ // Solving: Q_lr and then a_lr (new demand after entry)
916
+ Q_lr: { expression: 'Q_eff - b / (2 * c)', label: 'Q_LR', readonly: true },
917
+ P_lr: {
918
+ expression: 'minATC + c * ((Q_lr - Q_eff) ^ 2)',
919
+ label: 'P_LR',
920
+ readonly: true,
921
+ },
922
+ a_lr: { expression: 'P_lr + b * Q_lr', hidden: true },
923
+ // Chart bounds
924
+ chartMaxQ: { expression: 'max(Q_sr, Q_eff) * 2', hidden: true },
925
+ chartMaxP: { expression: 'max(P_sr, a_sr) * 1.2', hidden: true },
926
+ },
927
+ charts: [
928
+ {
929
+ id: 'short-run',
930
+ title: 'Short-Run: Positive Profit',
931
+ xAxis: { label: 'Quantity (q)', min: 0, max: 'chartMaxQ' },
932
+ yAxis: { label: 'Price, Cost', min: 0, max: 'chartMaxP' },
933
+ elements: [
934
+ // Profit rectangle
935
+ {
936
+ id: 'profit-area',
937
+ type: 'rectangle',
938
+ x1: 0,
939
+ y1: 'ATC_sr',
940
+ x2: 'Q_sr',
941
+ y2: 'P_sr',
942
+ fill: '#22C55E',
943
+ opacity: 0.3,
944
+ label: 'Profit',
945
+ },
946
+ // Demand
947
+ {
948
+ id: 'demand-sr',
949
+ type: 'line',
950
+ equation: 'a_sr - b * x',
951
+ color: '#2563EB',
952
+ strokeWidth: 3,
953
+ label: 'Demand',
954
+ domain: { min: 0, max: 'a_sr / b' },
955
+ },
956
+ // MR
957
+ {
958
+ id: 'mr-sr',
959
+ type: 'line',
960
+ equation: 'a_sr - 2 * b * x',
961
+ color: '#7C3AED',
962
+ strokeWidth: 2,
963
+ label: 'MR',
964
+ domain: { min: 0, max: 'a_sr / (2 * b)' },
965
+ },
966
+ // ATC
967
+ {
968
+ id: 'atc',
969
+ type: 'line',
970
+ equation: 'minATC + c * ((x - Q_eff) ^ 2)',
971
+ color: '#16A34A',
972
+ strokeWidth: 3,
973
+ label: 'ATC',
974
+ domain: { min: 1, max: 'chartMaxQ' },
975
+ },
976
+ // MC (approximate)
977
+ {
978
+ id: 'mc',
979
+ type: 'line',
980
+ equation: 'minATC + 2 * c * (x - Q_eff)',
981
+ color: '#DC2626',
982
+ strokeWidth: 2,
983
+ label: 'MC',
984
+ domain: { min: 'Q_eff * 0.3', max: 'chartMaxQ' },
985
+ },
986
+ // Optimal point
987
+ {
988
+ id: 'optimum-sr',
989
+ type: 'point',
990
+ x: 'Q_sr',
991
+ y: 'P_sr',
992
+ color: '#2563EB',
993
+ size: 8,
994
+ droplines: { x: true, y: true },
995
+ },
996
+ ],
997
+ annotations: [
998
+ {
999
+ id: 'profit-label',
1000
+ text: 'π = $${profit_sr:.0f} → Entry',
1001
+ x: 'chartMaxQ * 0.6',
1002
+ y: 'chartMaxP * 0.9',
1003
+ color: '#16A34A',
1004
+ },
1005
+ ],
1006
+ },
1007
+ {
1008
+ id: 'long-run',
1009
+ title: 'Long-Run: Zero Profit (Tangency)',
1010
+ xAxis: { label: 'Quantity (q)', min: 0, max: 'chartMaxQ' },
1011
+ yAxis: { label: 'Price, Cost', min: 0, max: 'chartMaxP' },
1012
+ elements: [
1013
+ // LR Demand (shifted in from entry)
1014
+ {
1015
+ id: 'demand-lr',
1016
+ type: 'line',
1017
+ equation: 'a_lr - b * x',
1018
+ color: '#2563EB',
1019
+ strokeWidth: 3,
1020
+ label: 'Demand (after entry)',
1021
+ domain: { min: 0, max: 'a_lr / b' },
1022
+ },
1023
+ // LR MR
1024
+ {
1025
+ id: 'mr-lr',
1026
+ type: 'line',
1027
+ equation: 'a_lr - 2 * b * x',
1028
+ color: '#7C3AED',
1029
+ strokeWidth: 2,
1030
+ label: 'MR',
1031
+ domain: { min: 0, max: 'a_lr / (2 * b)' },
1032
+ },
1033
+ // ATC
1034
+ {
1035
+ id: 'atc-lr',
1036
+ type: 'line',
1037
+ equation: 'minATC + c * ((x - Q_eff) ^ 2)',
1038
+ color: '#16A34A',
1039
+ strokeWidth: 3,
1040
+ label: 'ATC',
1041
+ domain: { min: 1, max: 'chartMaxQ' },
1042
+ },
1043
+ // MC
1044
+ {
1045
+ id: 'mc-lr',
1046
+ type: 'line',
1047
+ equation: 'minATC + 2 * c * (x - Q_eff)',
1048
+ color: '#DC2626',
1049
+ strokeWidth: 2,
1050
+ label: 'MC',
1051
+ domain: { min: 'Q_eff * 0.3', max: 'chartMaxQ' },
1052
+ },
1053
+ // Tangency point
1054
+ {
1055
+ id: 'tangency',
1056
+ type: 'point',
1057
+ x: 'Q_lr',
1058
+ y: 'P_lr',
1059
+ color: '#16A34A',
1060
+ size: 12,
1061
+ label: 'Tangency (π = 0)',
1062
+ droplines: { x: true, y: true },
1063
+ },
1064
+ // Efficient scale marker
1065
+ {
1066
+ id: 'efficient-point',
1067
+ type: 'point',
1068
+ x: 'Q_eff',
1069
+ y: 'minATC',
1070
+ color: '#9CA3AF',
1071
+ size: 6,
1072
+ label: 'Min ATC',
1073
+ },
1074
+ ],
1075
+ annotations: [
1076
+ {
1077
+ id: 'zero-profit',
1078
+ text: 'P = ATC → π = 0',
1079
+ x: 'chartMaxQ * 0.6',
1080
+ y: 'chartMaxP * 0.9',
1081
+ color: '#16A34A',
1082
+ },
1083
+ {
1084
+ id: 'excess-capacity',
1085
+ text: 'Excess Capacity: Q_LR < Q_eff',
1086
+ x: 'chartMaxQ * 0.6',
1087
+ y: 'chartMaxP * 0.8',
1088
+ color: '#6B7280',
1089
+ },
1090
+ {
1091
+ id: 'markup',
1092
+ text: 'P > MC (markup over marginal cost)',
1093
+ x: 'chartMaxQ * 0.6',
1094
+ y: 'chartMaxP * 0.7',
1095
+ color: '#6B7280',
1096
+ },
1097
+ ],
1098
+ },
1099
+ ],
1100
+ };
1101
+ },
1102
+ };
1103
+ // ============================================================================
1104
+ // EXPORTS
1105
+ // ============================================================================
1106
+ export const oligopolyTemplates = [
1107
+ cournotDuopoly,
1108
+ bertrandDuopoly,
1109
+ stackelbergDuopoly,
1110
+ kinkedDemand,
1111
+ monopolisticCompetition,
1112
+ ];
1113
+ //# sourceMappingURL=oligopoly.js.map