create-asciitorium 0.1.26 → 0.1.27

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.
package/README.md CHANGED
@@ -53,6 +53,18 @@ List available FIGlet fonts:
53
53
  npm run figlet:fonts
54
54
  ```
55
55
 
56
+ Generate ASCII maze files (placed in public/art/mazes):
57
+
58
+ ```bash
59
+ node scripts/maze-builder.js <width> <height> <filename> [--smooth]
60
+
61
+ # Examples:
62
+ node scripts/maze-builder.js 10 10 dungeon-level-1.txt
63
+ node scripts/maze-builder.js 15 20 castle-maze.txt --smooth
64
+ ```
65
+
66
+ The `--smooth` flag uses Unicode box drawing characters for improved visual appearance.
67
+
56
68
  ## Generated Project Structure
57
69
 
58
70
  A typical generated project will look like:
@@ -60,8 +72,12 @@ A typical generated project will look like:
60
72
  ```
61
73
  my-asciitorium-app/
62
74
  ├── public/
63
- │ ├── art/ # ASCII art assets
75
+ │ ├── art/
76
+ │ │ └── mazes/ # Generated maze files
64
77
  │ └── fonts/ # Custom fonts
78
+ ├── scripts/
79
+ │ ├── gen-figlet-art.js # FIGlet art generator
80
+ │ └── maze-builder.js # ASCII maze generator
65
81
  ├── src/
66
82
  │ ├── main.tsx # Main application entry
67
83
  │ └── vite-env.d.ts # TypeScript definitions
@@ -0,0 +1,294 @@
1
+ import fs from 'fs/promises'
2
+ import process from 'process'
3
+ import path from 'path'
4
+
5
+ function parseArgs() {
6
+ const args = process.argv.slice(2)
7
+
8
+ // Check for flags
9
+ let smooth = false
10
+ let filteredArgs = []
11
+
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i] === '--smooth' || args[i] === '--unicode') {
14
+ smooth = true
15
+ } else {
16
+ filteredArgs.push(args[i])
17
+ }
18
+ }
19
+
20
+ if (filteredArgs.length !== 3) {
21
+ console.error('Usage: node maze-builder.js <width> <height> <filename> [--smooth]')
22
+ console.error('Example: node maze-builder.js 10 10 dungeon-level-1.txt --smooth')
23
+ console.error('Mazes will be saved to public/art/mazes/ directory')
24
+ console.error('Use --smooth flag to enable Unicode box drawing characters')
25
+ process.exit(1)
26
+ }
27
+
28
+ const width = parseInt(filteredArgs[0])
29
+ const height = parseInt(filteredArgs[1])
30
+ const filename = filteredArgs[2]
31
+
32
+ if (isNaN(width) || width <= 0) {
33
+ console.error('Width must be a positive integer')
34
+ process.exit(1)
35
+ }
36
+
37
+ if (isNaN(height) || height <= 0) {
38
+ console.error('Height must be a positive integer')
39
+ process.exit(1)
40
+ }
41
+
42
+ // Construct the full path to the mazes directory
43
+ const mazesDir = path.join(process.cwd(), '../public/art/mazes')
44
+ const fullPath = path.join(mazesDir, filename)
45
+
46
+ return { width, height, filename: fullPath, mazesDir, smooth }
47
+ }
48
+
49
+ class MazeBuilder {
50
+ // Maze is a 2D array of chars.
51
+ // example 10x10 grid = [
52
+ // 0123456789012345678
53
+ // 0'+-0-+---+---+---+---+',
54
+ // 1'0 1 2 3 4 5 6 7 8 9 |',
55
+ // 2'+-2-+---+---+---+---+',
56
+ // 3'| 3 | | |',
57
+ // 4'+-4-+ + + + +',
58
+ // 5'| 5 | | | |',
59
+ // 6'+-6-+ +---+ + +',
60
+ // 7'| 7 | | | |',
61
+ // 8'+-8-+ + +---+ +',
62
+ // 9'| 9 | | |',
63
+ // '+---+---+---+---+---+',
64
+ // ]
65
+ // is actually a 11x19 2d array
66
+ // conversion: y = y, x = x*2
67
+ // only place items and creatures on odd values of y and x
68
+
69
+ constructor(width, height, fileName, mazesDir, smooth = false) {
70
+ this.fileName = fileName
71
+ this.mazesDir = mazesDir
72
+ this.width = width
73
+ this.height = height
74
+ this.smooth = smooth
75
+ this.map = []
76
+ }
77
+
78
+ async build() {
79
+ console.log(`--- building maze (${this.width}x${this.height}) ---`)
80
+
81
+ try {
82
+ this.map = this.createMaze()
83
+ console.log(' - created base maze')
84
+
85
+ // Add rooms
86
+ this.addRooms()
87
+ console.log(' - added rooms')
88
+
89
+ // Smooth the maze with Unicode box drawing characters (if requested)
90
+ if (this.smooth) {
91
+ this.mapUpdate(this.map)
92
+ console.log(' - smoothed maze appearance with Unicode characters')
93
+ }
94
+
95
+ // Save the maze to text file
96
+ await this.saveToText()
97
+ console.log('--- maze build complete ---')
98
+ } catch (error) {
99
+ console.error(`Error building maze: ${error.message}`)
100
+ console.error(error)
101
+ }
102
+ }
103
+
104
+ async saveToText() {
105
+ try {
106
+ // Ensure the mazes directory exists
107
+ await fs.mkdir(this.mazesDir, { recursive: true })
108
+
109
+ // Join the map array with newlines to create plain text
110
+ const textOutput = this.map.join('\n')
111
+
112
+ await fs.writeFile(this.fileName, textOutput, 'utf-8')
113
+ console.log(` - successfully wrote maze data to ${this.fileName}`)
114
+ } catch (error) {
115
+ console.error(`Error writing text to file: ${error.message}`)
116
+ }
117
+ }
118
+
119
+ xOffset(x) {
120
+ return x * 4 + 2
121
+ }
122
+
123
+ yOffset(y) {
124
+ return y * 2 + 1
125
+ }
126
+
127
+ // Doors are placed at all dead ends with openings to the north and south.
128
+ addRooms() {
129
+ for (let y = 0; y < this.height; y++) {
130
+ const yPos = this.yOffset(y)
131
+ for (let x = 0; x < this.width; x++) {
132
+ const xPos = this.xOffset(x)
133
+ this.replaceSpacesWithDoors(xPos, yPos)
134
+ }
135
+ }
136
+ }
137
+
138
+ // identify dead ends to the north or south.
139
+ // If there is a dead end to the north or south, then specify the x and y position of the opening.
140
+ replaceSpacesWithDoors(xPos, yPos) {
141
+ if (this.map[yPos][xPos] != ' ') return
142
+ const wallDirections = []
143
+ if (this.map[yPos][xPos - 2] != ' ' && this.map[yPos][xPos - 2] != 'o') wallDirections.push('west')
144
+ if (this.map[yPos][xPos + 2] != ' ' && this.map[yPos][xPos + 2] != 'o') wallDirections.push('east')
145
+ if (this.map[yPos - 1][xPos] != ' ' && this.map[yPos - 1][xPos] != 'o') wallDirections.push('north')
146
+ if (this.map[yPos + 1][xPos] != ' ' && this.map[yPos + 1][xPos] != 'o') wallDirections.push('south')
147
+ if (wallDirections.length == 3) {
148
+ if (wallDirections.includes('north') && !wallDirections.includes('south')) {
149
+ this.map[yPos + 1] = this.replaceAt(this.map[yPos + 1], xPos - 1, '-')
150
+ this.map[yPos + 1] = this.replaceAt(this.map[yPos + 1], xPos, 'o')
151
+ this.map[yPos + 1] = this.replaceAt(this.map[yPos + 1], xPos + 1, '-')
152
+ } else if (!wallDirections.includes('north') && wallDirections.includes('south')) {
153
+ this.map[yPos - 1] = this.replaceAt(this.map[yPos - 1], xPos - 1, '-')
154
+ this.map[yPos - 1] = this.replaceAt(this.map[yPos - 1], xPos, 'o')
155
+ this.map[yPos - 1] = this.replaceAt(this.map[yPos - 1], xPos + 1, '-')
156
+ }
157
+ }
158
+ }
159
+
160
+ replaceAt(string, index, replacement) {
161
+ return string.substr(0, index) + replacement + string.substr(index + replacement.length)
162
+ }
163
+
164
+ mapUpdate(map) {
165
+ const copy = [...map]
166
+ for (let y = 0; y < copy.length; y++) {
167
+ for (let x = 0; x < copy[y].length; x++) {
168
+ if (copy[y][x] === '-') copy[y] = copy[y].substring(0, x) + '─' + copy[y].substring(x + 1)
169
+ if (copy[y][x] === '|') copy[y] = copy[y].substring(0, x) + '│' + copy[y].substring(x + 1)
170
+ if (copy[y][x] === '+') {
171
+ let north = y > 0 && copy[y - 1][x] !== ' '
172
+ let south = y < copy.length - 1 && copy[y + 1][x] !== ' '
173
+ let west = x > 0 && copy[y][x - 1] !== ' '
174
+ let east = x < copy[y].length - 1 && copy[y][x + 1] !== ' '
175
+ if (north && south && west && east) copy[y] = copy[y].substring(0, x) + '┼' + copy[y].substring(x + 1)
176
+ else if (north && south && west) copy[y] = copy[y].substring(0, x) + '┤' + copy[y].substring(x + 1)
177
+ else if (north && south && east) copy[y] = copy[y].substring(0, x) + '├' + copy[y].substring(x + 1)
178
+ else if (north && east && west) copy[y] = copy[y].substring(0, x) + '┴' + copy[y].substring(x + 1)
179
+ else if (south && east && west) copy[y] = copy[y].substring(0, x) + '┬' + copy[y].substring(x + 1)
180
+ else if (north && south) copy[y] = copy[y].substring(0, x) + '│' + copy[y].substring(x + 1)
181
+ else if (west && east) copy[y] = copy[y].substring(0, x) + '─' + copy[y].substring(x + 1)
182
+ else if (north && east) copy[y] = copy[y].substring(0, x) + '╰' + copy[y].substring(x + 1)
183
+ else if (north && west) copy[y] = copy[y].substring(0, x) + '╯' + copy[y].substring(x + 1)
184
+ else if (south && east) copy[y] = copy[y].substring(0, x) + '╭' + copy[y].substring(x + 1)
185
+ else if (south && west) copy[y] = copy[y].substring(0, x) + '╮' + copy[y].substring(x + 1)
186
+ else if (east) copy[y] = copy[y].substring(0, x) + '╶' + copy[y].substring(x + 1)
187
+ else if (west) copy[y] = copy[y].substring(0, x) + '╴' + copy[y].substring(x + 1)
188
+ else if (north) copy[y] = copy[y].substring(0, x) + '╵' + copy[y].substring(x + 1)
189
+ else if (south) copy[y] = copy[y].substring(0, x) + '╷' + copy[y].substring(x + 1)
190
+ }
191
+ }
192
+ }
193
+ this.map = copy
194
+ }
195
+
196
+ createMaze() {
197
+ // Establish variables and starting grid
198
+ const totalCells = this.height * this.width
199
+ const unvisited = [] // cells that are unvisited.
200
+ const maze = []
201
+ for (let y = 0; y < this.height; y++) {
202
+ unvisited[y] = []
203
+ maze[y * 2] = ''
204
+ maze[y * 2 + 1] = ''
205
+ for (let x = 0; x < this.width; x++) {
206
+ maze[y * 2] += '+---'
207
+ maze[y * 2 + 1] += '| '
208
+ unvisited[y][x] = true
209
+ }
210
+ maze[y * 2] += '+'
211
+ maze[y * 2 + 1] += '|'
212
+ }
213
+ maze[this.height * 2] = ''
214
+ for (let x = 0; x < this.width; x++) {
215
+ maze[this.height * 2] += '+---'
216
+ }
217
+ maze[this.height * 2] += '+'
218
+
219
+ // Set a random position to start from
220
+ let current = [Math.floor(Math.random() * this.height), Math.floor(Math.random() * this.width)]
221
+ const path = [current]
222
+ unvisited[current[0]][current[1]] = false
223
+ let visited = 1
224
+
225
+ // // Loop through all available cell positions (given walls take up half the cells, we divide the total by two)
226
+ while (visited < totalCells) {
227
+ // Determine neighboring cells (0,1) and pathways to them (2,3)
228
+ const south = [current[0] - 1, current[1], 'north']
229
+ const north = [current[0] + 1, current[1], 'south']
230
+ const east = [current[0], current[1] - 1, 'west']
231
+ const west = [current[0], current[1] + 1, 'east']
232
+ const possibleNeighbors = [south, north, east, west]
233
+ const neighbors = []
234
+
235
+ // Determine if each neighboring cell is in game grid, and whether it has already been checked
236
+ for (let i = 0; i < possibleNeighbors.length; i++) {
237
+ if (
238
+ possibleNeighbors[i][0] > -1 &&
239
+ possibleNeighbors[i][0] < this.height &&
240
+ possibleNeighbors[i][1] > -1 &&
241
+ possibleNeighbors[i][1] < this.width &&
242
+ unvisited[possibleNeighbors[i][0]][possibleNeighbors[i][1]] == true
243
+ ) {
244
+ neighbors.push(possibleNeighbors[i])
245
+ }
246
+ }
247
+
248
+ // If at least one active neighboring cell has been found
249
+ if (neighbors.length > 0) {
250
+ // Choose one of the neighbors at random
251
+ const next = neighbors[Math.floor(Math.random() * neighbors.length)]
252
+
253
+ // Remove the wall between the current cell and the chosen neighboring cell in the maze view.
254
+ // split maze
255
+ let xBorderToRemove = 0
256
+ let yBorderToRemove = 0
257
+ if (next[2] == 'south') {
258
+ yBorderToRemove = this.yOffset(next[0]) - 1
259
+ xBorderToRemove = this.xOffset(next[1])
260
+ maze[yBorderToRemove] = this.replaceAt(maze[yBorderToRemove], xBorderToRemove - 1, ' ')
261
+ maze[yBorderToRemove] = this.replaceAt(maze[yBorderToRemove], xBorderToRemove + 1, ' ')
262
+ } else if (next[2] == 'north') {
263
+ yBorderToRemove = this.yOffset(next[0]) + 1
264
+ xBorderToRemove = this.xOffset(next[1])
265
+ maze[yBorderToRemove] = this.replaceAt(maze[yBorderToRemove], xBorderToRemove - 1, ' ')
266
+ maze[yBorderToRemove] = this.replaceAt(maze[yBorderToRemove], xBorderToRemove + 1, ' ')
267
+ } else if (next[2] == 'east') {
268
+ yBorderToRemove = this.yOffset(next[0])
269
+ xBorderToRemove = this.xOffset(next[1]) - 2
270
+ } else if (next[2] == 'west') {
271
+ yBorderToRemove = this.yOffset(next[0])
272
+ xBorderToRemove = this.xOffset(next[1]) + 2
273
+ }
274
+
275
+ maze[yBorderToRemove] = this.replaceAt(maze[yBorderToRemove], xBorderToRemove, ' ')
276
+
277
+ // Mark the neighbor as visited, and set it as the current cell
278
+ current = next
279
+ unvisited[current[0]][current[1]] = false
280
+ visited++
281
+ path.push(current)
282
+ }
283
+ // Otherwise go back up a step and keep going
284
+ else {
285
+ current = path.pop()
286
+ }
287
+ }
288
+ return maze
289
+ }
290
+ }
291
+
292
+ const { width, height, filename, mazesDir, smooth } = parseArgs()
293
+ const builder = new MazeBuilder(width, height, filename, mazesDir, smooth)
294
+ builder.build()
@@ -11,14 +11,12 @@ import {
11
11
  import {
12
12
  ButtonExample,
13
13
  CelticBorderExample,
14
- SelectExample,
15
14
  MultiSelectExample,
16
15
  RelativeSizingExample,
17
- TextInputExample,
18
16
  TabsExample,
19
- TextExample,
20
17
  AsciiArtExample,
21
18
  SlidersExample,
19
+ FormExample,
22
20
  AsciiMazeExample,
23
21
  } from 'asciitorium/examples';
24
22
 
@@ -37,13 +35,11 @@ const componentList = [
37
35
  'AsciiMaze',
38
36
  'Button',
39
37
  'CelticBorder',
38
+ 'Form',
40
39
  'MultiSelect',
41
40
  'RelativeSizing',
42
- 'Select',
43
41
  'Sliders',
44
42
  'Tabs',
45
- 'Text',
46
- 'TextInput',
47
43
  ];
48
44
 
49
45
  // Component mapping for dynamic content
@@ -54,11 +50,9 @@ const examples = {
54
50
  CelticBorder: CelticBorderExample,
55
51
  MultiSelect: MultiSelectExample,
56
52
  RelativeSizing: RelativeSizingExample,
57
- Select: SelectExample,
58
53
  Sliders: SlidersExample,
54
+ Form: FormExample,
59
55
  Tabs: TabsExample,
60
- Text: TextExample,
61
- TextInput: TextInputExample,
62
56
  };
63
57
 
64
58
  const app = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asciitorium",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "private": false,
5
5
  "description": "Scaffold a Vite + TypeScript project prewired for asciitorium (web + cli).",
6
6
  "bin": {
@@ -38,5 +38,12 @@
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/tgruby/asciitorium.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/tgruby/asciitorium/issues"
41
48
  }
42
49
  }