@jpetit/toolkit 3.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 (192) hide show
  1. package/.prettierignore +11 -0
  2. package/.prettierrc.json +9 -0
  3. package/.vscode/settings.json +26 -0
  4. package/README.md +22 -0
  5. package/assets/lua/fixCodeBlocks.lua +19 -0
  6. package/assets/lua/removeEnvs.lua +20 -0
  7. package/assets/lua/removeHtmlOnly.lua +11 -0
  8. package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
  9. package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
  10. package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
  11. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
  12. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
  13. package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
  14. package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
  15. package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
  16. package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
  17. package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
  18. package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
  19. package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
  20. package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
  21. package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
  22. package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
  23. package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
  24. package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
  25. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
  26. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
  27. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
  28. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
  29. package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
  30. package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
  31. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
  32. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
  33. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
  34. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
  35. package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
  36. package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
  37. package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
  38. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
  39. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
  40. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
  41. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
  42. package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
  43. package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
  44. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
  45. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
  46. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
  47. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
  48. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
  49. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
  50. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
  51. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
  52. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
  53. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
  54. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
  55. package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
  56. package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
  57. package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
  58. package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
  59. package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
  60. package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
  61. package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
  62. package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
  63. package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
  64. package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
  65. package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
  66. package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
  67. package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
  68. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
  69. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
  70. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
  71. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
  72. package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
  73. package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
  74. package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
  75. package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
  76. package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
  77. package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
  78. package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
  79. package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
  80. package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
  81. package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
  82. package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
  83. package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
  84. package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
  85. package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
  86. package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
  87. package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
  88. package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
  89. package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
  90. package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
  91. package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
  92. package/assets/sty/judgeit.ca.sty +54 -0
  93. package/assets/sty/judgeit.de.sty +61 -0
  94. package/assets/sty/judgeit.en.sty +60 -0
  95. package/assets/sty/judgeit.es.sty +54 -0
  96. package/assets/sty/judgeit.fr.sty +59 -0
  97. package/assets/sty/judgeit.sty +307 -0
  98. package/assets/sty/picins.sty +579 -0
  99. package/assets.zip +0 -0
  100. package/eslint.config.mjs +31 -0
  101. package/lib/ai.ts +138 -0
  102. package/lib/assets.ts +31 -0
  103. package/lib/cleaner.ts +58 -0
  104. package/lib/compilers/_frompython.ts +388 -0
  105. package/lib/compilers/base.ts +97 -0
  106. package/lib/compilers/gcc.ts +47 -0
  107. package/lib/compilers/gxx.ts +47 -0
  108. package/lib/compilers/index.ts +61 -0
  109. package/lib/compilers/python3.ts +67 -0
  110. package/lib/data.ts +19 -0
  111. package/lib/doctor.ts +104 -0
  112. package/lib/generate.ts +333 -0
  113. package/lib/maker.ts +535 -0
  114. package/lib/settings.ts +42 -0
  115. package/lib/tui.ts +69 -0
  116. package/lib/utils.ts +83 -0
  117. package/package.json +56 -0
  118. package/problems/graphic.pbm/README.md +14 -0
  119. package/problems/graphic.pbm/award.png +0 -0
  120. package/problems/graphic.pbm/handler.yml +2 -0
  121. package/problems/graphic.pbm/problem.ca.html +13 -0
  122. package/problems/graphic.pbm/problem.ca.md +20 -0
  123. package/problems/graphic.pbm/problem.ca.tex +21 -0
  124. package/problems/graphic.pbm/problem.ca.txt +20 -0
  125. package/problems/graphic.pbm/problem.ca.yml +3 -0
  126. package/problems/graphic.pbm/sample-1.inp +1 -0
  127. package/problems/graphic.pbm/sample-2.inp +1 -0
  128. package/problems/graphic.pbm/solution.py +11 -0
  129. package/problems/maxim2.pbm/Main.java +13 -0
  130. package/problems/maxim2.pbm/distillation.yml +7 -0
  131. package/problems/maxim2.pbm/distilled-01.inp +1 -0
  132. package/problems/maxim2.pbm/distilled-02.inp +1 -0
  133. package/problems/maxim2.pbm/distilled-03.inp +1 -0
  134. package/problems/maxim2.pbm/distiller.yml +2 -0
  135. package/problems/maxim2.pbm/generate-inputs.py +9 -0
  136. package/problems/maxim2.pbm/handler.yml +2 -0
  137. package/problems/maxim2.pbm/ma-1.inp +1 -0
  138. package/problems/maxim2.pbm/ma-2.inp +1 -0
  139. package/problems/maxim2.pbm/ma-3.inp +1 -0
  140. package/problems/maxim2.pbm/ma-4.inp +1 -0
  141. package/problems/maxim2.pbm/ma-5.inp +1 -0
  142. package/problems/maxim2.pbm/per-doubles.inp +1 -0
  143. package/problems/maxim2.pbm/problem.ca.html +11 -0
  144. package/problems/maxim2.pbm/problem.ca.md +19 -0
  145. package/problems/maxim2.pbm/problem.ca.tex +17 -0
  146. package/problems/maxim2.pbm/problem.ca.txt +19 -0
  147. package/problems/maxim2.pbm/problem.ca.yml +3 -0
  148. package/problems/maxim2.pbm/problem.en.html +11 -0
  149. package/problems/maxim2.pbm/problem.en.md +19 -0
  150. package/problems/maxim2.pbm/problem.en.tex +16 -0
  151. package/problems/maxim2.pbm/problem.en.txt +19 -0
  152. package/problems/maxim2.pbm/problem.en.yml +4 -0
  153. package/problems/maxim2.pbm/sample-1.inp +1 -0
  154. package/problems/maxim2.pbm/sample-2.inp +1 -0
  155. package/problems/maxim2.pbm/sample-3.inp +1 -0
  156. package/problems/maxim2.pbm/solution.c +12 -0
  157. package/problems/maxim2.pbm/solution.cc +13 -0
  158. package/problems/maxim2.pbm/solution.java +13 -0
  159. package/problems/maxim2.pbm/solution.pas +9 -0
  160. package/problems/maxim2.pbm/solution.py +5 -0
  161. package/problems/maxim2.pbm/tags.yml +2 -0
  162. package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
  163. package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
  164. package/problems/maxim2.pbm/test_-1_0.inp +1 -0
  165. package/problems/maxim2.pbm/test_-1_1.inp +1 -0
  166. package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
  167. package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
  168. package/problems/maxim2.pbm/test_-2_0.inp +1 -0
  169. package/problems/maxim2.pbm/test_-2_1.inp +1 -0
  170. package/problems/maxim2.pbm/test_0_-1.inp +1 -0
  171. package/problems/maxim2.pbm/test_0_-2.inp +1 -0
  172. package/problems/maxim2.pbm/test_0_0.inp +1 -0
  173. package/problems/maxim2.pbm/test_0_1.inp +1 -0
  174. package/problems/maxim2.pbm/test_1_-1.inp +1 -0
  175. package/problems/maxim2.pbm/test_1_-2.inp +1 -0
  176. package/problems/maxim2.pbm/test_1_0.inp +1 -0
  177. package/problems/maxim2.pbm/test_1_1.inp +1 -0
  178. package/test.ts +3 -0
  179. package/toolkit/ai.ts +30 -0
  180. package/toolkit/clean.ts +19 -0
  181. package/toolkit/compilers.ts +29 -0
  182. package/toolkit/create-jutge-ai.ts +101 -0
  183. package/toolkit/create-template.ts +51 -0
  184. package/toolkit/create-wizard.ts +4 -0
  185. package/toolkit/create.ts +75 -0
  186. package/toolkit/doctor.ts +17 -0
  187. package/toolkit/index.ts +28 -0
  188. package/toolkit/init.ts +66 -0
  189. package/toolkit/make.ts +60 -0
  190. package/toolkit/verify.ts +19 -0
  191. package/tsconfig.json +38 -0
  192. package/types/zip.d.ts +4 -0
@@ -0,0 +1,47 @@
1
+ import * as tui from '@/lib/tui.js'
2
+ import { Compiler } from './base'
3
+ import { rm, exists } from 'fs/promises'
4
+ import { join } from 'path'
5
+
6
+ export class Compiler_GCC extends Compiler {
7
+ id(): string {
8
+ return 'GCC'
9
+ }
10
+
11
+ name(): string {
12
+ return 'GNU C Compiler'
13
+ }
14
+
15
+ type(): string {
16
+ return 'compiler'
17
+ }
18
+
19
+ language(): string {
20
+ return 'C'
21
+ }
22
+
23
+ async version(): Promise<string> {
24
+ return await this.getVersion('gcc --version', 0)
25
+ }
26
+
27
+ flags1(): string {
28
+ return '-D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare'
29
+ }
30
+
31
+ flags2(): string {
32
+ return ''
33
+ }
34
+
35
+ extension(): string {
36
+ return 'c'
37
+ }
38
+
39
+ async compile(directory: string, sourcePath: string): Promise<void> {
40
+ const outputPath = sourcePath + '.exe'
41
+ const fullOutputPath = join(directory, outputPath)
42
+ await rm(fullOutputPath, { force: true })
43
+ tui.command(`gcc ${this.flags1()} ${sourcePath} -o ${outputPath}`)
44
+ await Bun.$`gcc ${this.flags1().split(' ')} ${sourcePath} -o ${outputPath}`.cwd(directory).nothrow()
45
+ if (!(await exists(fullOutputPath))) throw new Error(`Compilation failed for ${sourcePath}`)
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ import { rm, exists } from 'fs/promises'
2
+ import { Compiler } from './base'
3
+ import chalk from 'chalk'
4
+ import { join } from 'path'
5
+
6
+ export class Compiler_GXX extends Compiler {
7
+ id(): string {
8
+ return 'GXX'
9
+ }
10
+
11
+ name(): string {
12
+ return 'GNU C++ Compiler'
13
+ }
14
+
15
+ type(): string {
16
+ return 'compiler'
17
+ }
18
+
19
+ language(): string {
20
+ return 'C++'
21
+ }
22
+
23
+ async version(): Promise<string> {
24
+ return await this.getVersion('g++ --version', 0)
25
+ }
26
+
27
+ flags1(): string {
28
+ return '-std=c++11 -D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare -Wshadow'
29
+ }
30
+
31
+ flags2(): string {
32
+ return '-std=c++11 -D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare -Wshadow'
33
+ }
34
+
35
+ extension(): string {
36
+ return 'cc'
37
+ }
38
+
39
+ async compile(directory: string, sourcePath: string): Promise<void> {
40
+ const outputPath = sourcePath + '.exe'
41
+ const fullOutputPath = join(directory, outputPath)
42
+ await rm(fullOutputPath, { force: true })
43
+ console.log(chalk.magenta(`❯ g++ ${this.flags1()} ${sourcePath} -o ${outputPath}`))
44
+ await Bun.$`g++ ${this.flags1().split(' ')} ${sourcePath} -o ${outputPath}`.cwd(directory).nothrow()
45
+ if (!(await exists(fullOutputPath))) throw new Error(`Compilation failed for ${sourcePath}`)
46
+ }
47
+ }
@@ -0,0 +1,61 @@
1
+ import { Compiler, type CompilerInfo } from './base'
2
+ import { Compiler_GCC } from './gcc'
3
+ import { Compiler_GXX } from './gxx'
4
+ import { Compiler_Python3 } from './python3'
5
+
6
+ const compilersRegistryById: Record<string, new () => Compiler> = {
7
+ C: Compiler_GCC,
8
+ 'C++': Compiler_GXX,
9
+ Python3: Compiler_Python3,
10
+ }
11
+
12
+ const compilersRegistryByExtension: Record<string, new () => Compiler> = {
13
+ c: Compiler_GCC,
14
+ cc: Compiler_GXX,
15
+ py: Compiler_Python3,
16
+ }
17
+
18
+ export function getCompilerById(id: string): Compiler {
19
+ const CompilerClass = compilersRegistryById[id]
20
+ if (!CompilerClass) throw new Error(`Compiler '${id}' is not defined`)
21
+ const compilerInstance = new CompilerClass()
22
+ return compilerInstance
23
+ }
24
+
25
+ export function getCompilerByExtension(extension: string): Compiler {
26
+ const CompilerClass = compilersRegistryByExtension[extension]
27
+ if (!CompilerClass) throw new Error(`No compiler defined for extension '.${extension}'`)
28
+ const compilerInstance = new CompilerClass()
29
+ return compilerInstance
30
+ }
31
+
32
+ export async function getDefinedCompilerIds(): Promise<string[]> {
33
+ await Bun.sleep(0) // to make function async
34
+ return Object.keys(compilersRegistryById)
35
+ }
36
+
37
+ export async function getAvailableCompilers(): Promise<string[]> {
38
+ const available: string[] = []
39
+ for (const id of Object.keys(compilersRegistryById)) {
40
+ const CompilerClass = compilersRegistryById[id]!
41
+ const compilerInstance = new CompilerClass()
42
+ if (await compilerInstance.available()) {
43
+ available.push(id)
44
+ }
45
+ }
46
+ return available
47
+ }
48
+
49
+ export async function getCompilerInfo(id: string): Promise<CompilerInfo> {
50
+ const compilerInstance = getCompilerById(id)
51
+ if (!compilerInstance) throw new Error(`Compiler '${id}' is not defined`)
52
+ return await compilerInstance.info()
53
+ }
54
+
55
+ export async function getCompilersInfo(): Promise<Record<string, CompilerInfo>> {
56
+ const infos: Record<string, CompilerInfo> = {}
57
+ for (const id of Object.keys(compilersRegistryById)) {
58
+ infos[id] = await getCompilerInfo(id)
59
+ }
60
+ return infos
61
+ }
@@ -0,0 +1,67 @@
1
+ import * as tui from '@/lib/tui.js'
2
+ import { exists, rm } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { Compiler } from './base'
5
+
6
+ export class Compiler_Python3 extends Compiler {
7
+ id(): string {
8
+ return 'Python3'
9
+ }
10
+
11
+ name(): string {
12
+ return 'Python3'
13
+ }
14
+
15
+ type(): string {
16
+ return 'interpreter'
17
+ }
18
+
19
+ language(): string {
20
+ return 'Python'
21
+ }
22
+
23
+ async version(): Promise<string> {
24
+ return await this.getVersion('python3 --version', 0)
25
+ }
26
+
27
+ flags1(): string {
28
+ return ''
29
+ }
30
+
31
+ flags2(): string {
32
+ return ''
33
+ }
34
+
35
+ extension(): string {
36
+ return 'py'
37
+ }
38
+
39
+ async compile(directory: string, sourcePath: string): Promise<void> {
40
+ tui.command(`python3 -m py_compile ${sourcePath}`)
41
+ console.log('directory:', directory, 'sourcePath:', sourcePath)
42
+ const { exitCode } = await Bun.$`python3 -m py_compile ${sourcePath}`.cwd(directory).nothrow()
43
+ if (exitCode !== 0) throw new Error(`Compilation failed for ${sourcePath}`)
44
+ }
45
+
46
+ override async execute(directory: string, inputPath: string, outputPath: string): Promise<void> {
47
+ const sourcePath = 'solution.py'
48
+ if (!(await exists(join(directory, sourcePath)))) {
49
+ throw new Error(`Source file ${sourcePath} does not exist in directory ${directory}`)
50
+ }
51
+ const fullInputPath = join(directory, inputPath)
52
+ const fullOutputPath = join(directory, outputPath)
53
+ await rm(fullOutputPath, { force: true })
54
+ tui.command(`python3 ${sourcePath} < ${inputPath} > ${outputPath}`)
55
+
56
+ const proc = Bun.spawn(['python3', sourcePath], {
57
+ cwd: directory,
58
+ stdin: Bun.file(fullInputPath),
59
+ stdout: Bun.file(fullOutputPath),
60
+ stderr: 'inherit',
61
+ })
62
+ await proc.exited
63
+ const exitCode = proc.exitCode
64
+
65
+ if (exitCode !== 0) throw new Error(`Execution failed for ${sourcePath}`)
66
+ }
67
+ }
package/lib/data.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { invert } from 'radash'
2
+
3
+ export const languageNames: Record<string, string> = {
4
+ en: 'English',
5
+ ca: 'Catalan',
6
+ es: 'Spanish',
7
+ fr: 'French',
8
+ de: 'German',
9
+ }
10
+
11
+ export const proglangNames: Record<string, string> = {
12
+ c: 'C',
13
+ cc: 'C++',
14
+ py: 'Python3',
15
+ hs: 'Haskell',
16
+ clj: 'Clojure',
17
+ }
18
+
19
+ export const proglangExtensions: Record<string, string> = invert(proglangNames)
package/lib/doctor.ts ADDED
@@ -0,0 +1,104 @@
1
+ import chalk from 'chalk'
2
+ import * as tui from '@/lib/tui.js'
3
+
4
+ export async function probePython3(showInfo: boolean = false): Promise<boolean> {
5
+ if (showInfo) tui.command('python3 --version')
6
+ const output = await Bun.$`python3 --version`.nothrow().text()
7
+ const version = output.trim()
8
+ if (showInfo) console.log(version)
9
+ return version.startsWith('Python 3')
10
+ }
11
+
12
+ export async function probeGCC(showInfo: boolean = false): Promise<boolean> {
13
+ if (showInfo) tui.command('g++ --version')
14
+ const output = await Bun.$`g++ --version`.nothrow().text()
15
+ const version = output.split('\n')[0]!.trim()
16
+ if (showInfo) console.log(version)
17
+ return output.startsWith('Apple clang') || output.startsWith('g++')
18
+ }
19
+
20
+ export async function probeLaTeX(showInfo: boolean = false): Promise<boolean> {
21
+ if (showInfo) tui.command('pdflatex --version')
22
+ const output = await Bun.$`pdflatex --version`.nothrow().text()
23
+ const version = output.split('\n')[0]!.trim()
24
+ if (showInfo) console.log(version)
25
+ return output.startsWith('pdfTeX')
26
+ }
27
+
28
+ export async function probePandoc(showInfo: boolean = false): Promise<boolean> {
29
+ if (showInfo) tui.command('pandoc --version')
30
+ const output = await Bun.$`pandoc --version`.nothrow().text()
31
+ const version = output.split('\n')[0]!.trim()
32
+ if (showInfo) console.log(version)
33
+ return output.startsWith('pandoc') && output.includes('+lua')
34
+ }
35
+
36
+ export async function probeImageMagick(showInfo: boolean = false): Promise<boolean> {
37
+ if (showInfo) tui.command('magick --version')
38
+ const output = await Bun.$`magick --version`.nothrow().text()
39
+ const version = output.split('\n')[0]!.trim()
40
+ if (showInfo) console.log(version)
41
+ return output.startsWith('Version: ImageMagick')
42
+ }
43
+
44
+ export async function checkPython3(): Promise<void> {
45
+ if (await probePython3(true)) {
46
+ tui.success('Python3 seems installed')
47
+ } else {
48
+ tui.error('Python3 does not appear to be installed')
49
+ tui.warning('This is not a problem if you do not plan to use Python solutions.')
50
+ tui.warning('See https://www.python.org/downloads/')
51
+ }
52
+ }
53
+
54
+ export async function checkGCC(): Promise<void> {
55
+ if (await probeGCC(true)) {
56
+ tui.success('C/C++ seems installed')
57
+ } else {
58
+ tui.error('C/C++ does not appear to be installed')
59
+ tui.warning('This is not a problem if you do not plan to use C or C++ solutions.')
60
+ tui.warning('Please install GCC or Clang.')
61
+ }
62
+ }
63
+
64
+ export async function checkLaTeX(): Promise<void> {
65
+ if (await probeLaTeX(true)) {
66
+ tui.success('LaTeX seems installed')
67
+ } else {
68
+ tui.error('LaTeX does not appear to be installed')
69
+ tui.warning('You will not be able to generate PDF statements.')
70
+ tui.warning('TODO: Provide instructions for installing LaTeX')
71
+ }
72
+ }
73
+
74
+ export async function checkPandoc(): Promise<void> {
75
+ if (await probePandoc(true)) {
76
+ tui.success('Pandoc with Lua support seems installed')
77
+ } else {
78
+ tui.error('Pandoc with Lua support does not appear to be installed')
79
+ tui.warning('You will not be able to generate text statements (HTML, TXT, MD).')
80
+ tui.warning('See https://pandoc.org/installing.html')
81
+ }
82
+ }
83
+
84
+ export async function checkImageMagick(): Promise<void> {
85
+ if (await probeImageMagick(true)) {
86
+ tui.success('ImageMagick seems installed')
87
+ } else {
88
+ tui.error('ImageMagick does not appear to be installed')
89
+ tui.warning('You will not be able to convert images for text statements.')
90
+ tui.warning('See https://imagemagick.org/script/download.php')
91
+ }
92
+ }
93
+
94
+ export async function checkEnvVars(): Promise<void> {
95
+ await Bun.sleep(0) // to prevent "async function has no await" warning
96
+ const vars = ['OPENAI_API_KEY', 'GEMINI_API_KEY']
97
+ for (const v of vars) {
98
+ if (process.env[v]) {
99
+ tui.success(`${v} is set`)
100
+ } else {
101
+ tui.error(`${v} is not set`)
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,333 @@
1
+ import { ChatBot, estimatePowerConsumption } from '@/lib/ai'
2
+ import * as tui from '@/lib/tui.js'
3
+ import chalk from 'chalk'
4
+ import { appendFile, mkdir } from 'fs/promises'
5
+ import { oraPromise } from 'ora'
6
+ import { join } from 'path'
7
+ import { z } from 'zod'
8
+ import { paths } from './settings'
9
+ import { proglangNames, languageNames } from './data'
10
+
11
+ const showPrompt = true
12
+ const showAnswer = true
13
+
14
+ export const ZProblemData = z.object({
15
+ title: z.string(),
16
+ description: z.string(),
17
+ author: z.string(),
18
+ email: z.string().email(),
19
+ type: z.string(),
20
+ proglangs: z.array(z.string()),
21
+ languages: z.array(z.string()),
22
+ model: z.string(),
23
+ outputDir: z.string(),
24
+ })
25
+
26
+ export type ProblemData = z.infer<typeof ZProblemData>
27
+
28
+ export async function generateProblemWithJutgeAI(info: ProblemData): Promise<void> {
29
+ const generator = new ProblemGenerator(info)
30
+ await generator.run()
31
+ await generator.save(info.outputDir)
32
+ }
33
+
34
+ class ProblemGenerator {
35
+ private bot: ChatBot
36
+ private info: ProblemData
37
+
38
+ // generated problem parts
39
+ private problemStatement: string = ''
40
+ private problemSampleTests: string = ''
41
+ private problemPrivateTests: string = ''
42
+ private problemSolutions: Record<string, string> = {}
43
+ private problemTranslations: Record<string, string> = {}
44
+ private problemReadme: string = ''
45
+
46
+ private latexExample = `
47
+ \\Problem{Which one is missing?}
48
+
49
+ \\Statement
50
+
51
+ Johny has a list of objects, labeled between 1 and $n$, but he lost one of them. He wants to know which one is missing.
52
+
53
+ \\medskip
54
+
55
+ Write a program that reads sequences with all the numbers between 1 and $n$ but one, and tells which one is missing.
56
+
57
+ \\Input
58
+
59
+ Input consists of several sequences.
60
+ Every sequence begins with a number $n$ between~1 and~$10^4$ followed by $n - 1$ natural numbers.
61
+ Every number between 1 and $n$ appears exactly once, except one of them, which is missing.
62
+
63
+ \\Output
64
+
65
+ For every sequence, print the missing number.
66
+ `
67
+
68
+ private systemPrompt = `
69
+ You are an expert in coding and writing programmming problems.
70
+ You provide accurate information and write following the instructed format.
71
+ `
72
+
73
+ private statementCoda = `
74
+
75
+ \\Observation
76
+
77
+ This problem has been generated by Jutge$^{\\text{AI}}$. Remove this observation after reviewing it carefully.
78
+
79
+ \\Sample
80
+
81
+ `
82
+ constructor(info: ProblemData) {
83
+ this.info = info
84
+ this.bot = new ChatBot(info.model, this.systemPrompt)
85
+ }
86
+
87
+ async generateStatement(): Promise<string> {
88
+ return await tui.section('Generating problem statement', async () => {
89
+ const statementPrompt = `
90
+ You are to write the statement of a programming problem
91
+
92
+ The statement must be written in LaTeX, using a few predefined macros.
93
+ Use LaTeX math syntax for formulas and variables.
94
+ Use dollars for inline maths and use \\[ and \\] for display maths.
95
+ Do not add input/output test cases in the statement.
96
+ Separate paragraphs by a blank line and \\medskip macro.
97
+
98
+ Write in the style of Salvador Roura, the famous problem setter from Jutge.org.
99
+
100
+ Here is an example for an unrelated problem, follow its structure and macros:
101
+
102
+ ${this.latexExample}
103
+
104
+ The title for this problem is:
105
+
106
+ ${this.info.title}
107
+
108
+ Here is the description of this problem and some additional instructions:
109
+
110
+ ${this.info.description}
111
+ `
112
+ const answer = cleanMardownCodeString(await this.complete(statementPrompt)) + this.statementCoda
113
+ return answer
114
+ })
115
+ }
116
+
117
+ async generateSampleTests(): Promise<string> {
118
+ return await tui.section('Generating sample test cases', async () => {
119
+ const sampleTestCasesPrompt = `
120
+ Now, write a sample test case file to illustrate the input that the program must read according to the problem statement.
121
+
122
+ Only the input should be given, the output will be computed from it.
123
+ Sample test cases should be relatively small but cover interesting cases but not all edge cases.
124
+ `
125
+ const answer = cleanMardownCodeString(await this.complete(sampleTestCasesPrompt))
126
+ return answer
127
+ })
128
+ }
129
+
130
+ async generatePrivateTests(): Promise<string> {
131
+ return await tui.section('Generating private test cases', async () => {
132
+ const privateTestCasesPrompt = `
133
+ Now, write a private test case file to check the correctness and efficiency of the program.
134
+
135
+ Only the input should be given, the output will be computed from it.
136
+ The private test cases should cover edge cases.
137
+ Limit the number of test cases to 20.
138
+ `
139
+ const answer = cleanMardownCodeString(await this.complete(privateTestCasesPrompt))
140
+ return answer
141
+ })
142
+ }
143
+
144
+ async generateSolutions(): Promise<Record<string, string>> {
145
+ return await tui.section('Generating solutions', async () => {
146
+ const solutions: Record<string, string> = {}
147
+ for (const proglang of this.info.proglangs) {
148
+ await tui.section(`Generating solution in ${proglangNames[proglang]}`, async () => {
149
+ const solutionPrompt = `
150
+ Now, write a solution in ${proglangNames[proglang]} to solve this problem
151
+
152
+ It must be written in an idiomatic way and only include relevant comments.
153
+ The code must be efficient and handle all edge cases.
154
+ The code must read from standard input and write to standard output.
155
+ The code must be well written and easy to understand to novices.
156
+ The code does not have to check the preconditions stated in the probleM
157
+ Do not use any non-standard libraries.
158
+ ${proglang === 'cpp' ? 'Do not use fast input/output methods.' : ''}
159
+ ${proglang === 'cpp' ? 'Add a using namespace std; declaration after the includes and do not use std:: prefixes.' : ''}
160
+ ${proglang === 'py' ? 'Use type annotations.' : ''}
161
+ `
162
+ const answer = cleanMardownCodeString(await this.complete(solutionPrompt))
163
+ this.bot.forgetLastInteraction()
164
+ solutions[proglang] = answer
165
+ })
166
+ }
167
+ return solutions
168
+ })
169
+ }
170
+
171
+ async translateStatements(): Promise<Record<string, string>> {
172
+ return await tui.section('Translating problem statements', async () => {
173
+ const translations: Record<string, string> = {}
174
+ for (const language of this.info.languages) {
175
+ await tui.section(`Translating to ${languageNames[language]}`, async () => {
176
+ const translationPrompt = `
177
+ Now, translate the problem statement to ${languageNames[language]}.
178
+
179
+ The translation must be accurate and use proper technical terminology.
180
+ Maintain the LaTeX formatting and macros.
181
+ The texts that the program must read and write should not be translated.
182
+ `
183
+ const answer = cleanMardownCodeString(await this.complete(translationPrompt)) + this.statementCoda
184
+ this.bot.forgetLastInteraction()
185
+ translations[language] = answer
186
+ })
187
+ }
188
+ return translations
189
+ })
190
+ }
191
+
192
+ async generateReadme(): Promise<string> {
193
+ return tui.section('Generating README.md', async () => {
194
+ await Bun.sleep(0)
195
+ const readme = `
196
+ # ${this.info.title}
197
+
198
+ This programming problem for Jutge.org was generated by Jutge<sup>AI</sup> through the Jutge.org API using ${this.info.model} and a prompt by ${this.info.author}.
199
+
200
+ **Warning**: This problem may contain inaccuracies or errors. Review the problem statements, test cases, and solutions carefully before using them in a real setting. Output tests and statement PDFs have not been generated, use \`jutge-make-problem\` to generate them.
201
+
202
+ ## Author
203
+
204
+ ${this.info.author}
205
+
206
+ ## Original prompt
207
+
208
+ ${this.info.description}
209
+
210
+ ## Generated solutions
211
+
212
+ - ${this.info.proglangs.map((proglang) => proglangNames[proglang]).join('\n- ')}
213
+
214
+ ## Generated languages
215
+
216
+ - English
217
+ - ${this.info.languages.map((language) => languageNames[language]).join('\n- ')}
218
+
219
+ ## Model information
220
+
221
+ \`\`\`yaml
222
+ ${Bun.YAML.stringify(this.bot.modelInformation(), null, 2)}
223
+ \`\`\`
224
+
225
+ ## Estimated cost
226
+
227
+ The following information is based on estimations from token counts and do not reflect the actual costs incurred. Using GPT-5 pricing as reference.
228
+
229
+ - Total input tokens: ${this.bot.totalInputTokens}
230
+ - Total output tokens: ${this.bot.totalOutputTokens}
231
+ - Total input cost: ${this.bot.totalInputCost.toFixed(6)} USD
232
+ - Total output cost: ${this.bot.totalOutputCost.toFixed(6)} USD
233
+ - Total estimated cost: ${(this.bot.totalInputCost + this.bot.totalOutputCost).toFixed(6)} USD
234
+ - Energy: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).wattHours.toFixed(6)} Wh
235
+ - CO₂ emissions: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).co2Grams.toFixed(6)} g CO₂
236
+
237
+ `
238
+ return readme
239
+ })
240
+ }
241
+
242
+ async run() {
243
+ tui.title('Generating problem with JutgeAI')
244
+ await this.bot.init()
245
+ this.problemStatement = await this.generateStatement()
246
+ this.problemSampleTests = await this.generateSampleTests()
247
+ this.problemPrivateTests = await this.generatePrivateTests()
248
+ this.problemSolutions = await this.generateSolutions() // these are forgotten inside
249
+ this.bot.forgetLastInteraction() // forget private tests
250
+ this.bot.forgetLastInteraction() // forget sample tests
251
+ this.problemTranslations = await this.translateStatements()
252
+ this.problemReadme = await this.generateReadme()
253
+ }
254
+
255
+ async save(path: string) {
256
+ await tui.section(`Saving problem to ${path}`, async () => {
257
+ await mkdir(path, { recursive: true })
258
+ await Bun.write(join(path, 'problem.en.tex'), this.problemStatement)
259
+
260
+ const yml = {
261
+ title: this.info.title,
262
+ author: this.info.author,
263
+ email: this.info.email,
264
+ model: this.info.model,
265
+ }
266
+ await Bun.write(`${path}/problem.en.yml`, Bun.YAML.stringify(yml, null, 2) + '\n')
267
+
268
+ for (const [lang, translation] of Object.entries(this.problemTranslations)) {
269
+ await Bun.write(join(path, `problem.${lang}.tex`), translation)
270
+ const yml = {
271
+ title: getTitleFromTranslation(translation) || this.info.title,
272
+ translator: this.info.author,
273
+ translator_email: this.info.email,
274
+ original_language: 'en',
275
+ model: this.info.model,
276
+ }
277
+ await Bun.write(join(path, `problem.${lang}.yml`), Bun.YAML.stringify(yml, null, 2) + '\n')
278
+ }
279
+
280
+ await Bun.write(join(path, 'sample.inp'), this.problemSampleTests)
281
+
282
+ await Bun.write(join(path, 'test.inp'), this.problemPrivateTests)
283
+
284
+ for (const [proglang, solution] of Object.entries(this.problemSolutions)) {
285
+ const ext = proglang
286
+ await Bun.write(join(path, `solution.${ext}`), solution)
287
+ }
288
+
289
+ const handlerYml: any = {
290
+ handler: 'std',
291
+ }
292
+ await Bun.write(join(path, 'handler.yml'), Bun.YAML.stringify(handlerYml, null, 2) + '\n')
293
+
294
+ await Bun.write(join(path, 'README.md'), this.problemReadme)
295
+ })
296
+ }
297
+
298
+ async complete(prompt: string): Promise<string> {
299
+ if (showPrompt) console.log(chalk.gray(prompt))
300
+ await appendFile(join(paths.log, 'jutge-ai.log'), '------------------- PROMPT -------------------\n\n')
301
+ await appendFile(join(paths.log, 'jutge-ai.log'), prompt + '\n\n')
302
+
303
+ const answer = await oraPromise(
304
+ async () => {
305
+ return await this.bot.complete(prompt)
306
+ },
307
+ {
308
+ text: `JutgeAI is thinking`,
309
+ },
310
+ )
311
+
312
+ if (showAnswer) console.log(chalk.gray(answer))
313
+ await appendFile(join(paths.log, 'jutge-ai.log'), '------------------- ANSWER -------------------\n\n')
314
+ await appendFile(join(paths.log, 'jutge-ai.log'), answer + '\n\n')
315
+
316
+ return answer
317
+ }
318
+ }
319
+
320
+ function getTitleFromTranslation(translation: string): string | null {
321
+ const pattern = /\\Problem\{(.*?)\}/
322
+ const match = translation.match(pattern)
323
+ if (match) {
324
+ return match[1]!.trim()
325
+ }
326
+ return null
327
+ }
328
+
329
+ function cleanMardownCodeString(s: string): string {
330
+ const pattern = /^\n*```\w*\s*(.*?)\s*```\n*$/s
331
+ const clean = s.replace(pattern, '$1')
332
+ return clean
333
+ }