console-fun
Version:
Some stuff in the console: utils, printing, games and other fun
1,681 lines (1,640 loc) • 78.3 kB
JavaScript
#!/usr/bin/env node
import meow from 'meow';
import * as R from 'rambda';
import { join, repeat, findIndex, propEq, addIndex, filter, adjust, set, lensProp, map, times, compose, sum, prop, isNil, zipObj, range, values, equals, keys, reduce, difference, all } from 'rambda';
import { shuffle } from 'fast-shuffle';
import ansiEscapes from 'ansi-escapes';
import stringWidth from 'string-width';
import chalk from 'chalk';
import delay from 'delay';
import readline from 'readline';
import figureSet from 'figures';
import { coloreme } from 'coloreme';
import fs from 'fs';
import logUpdate from 'log-update';
import terminalSize from 'terminal-size';
const randPos$2 = (yMin = 0) => {
const randY = Math.floor(Math.random() * process.stdout.rows);
return [
Math.floor(Math.random() * process.stdout.columns),
randY > yMin ? randY : Math.floor(yMin + Math.random() * process.stdout.rows)
];
};
const fallingStarsGame = () => {
let interval;
process.stdout.write(ansiEscapes.eraseScreen);
const [x, y] = randPos$2();
process.stdout.write(ansiEscapes.cursorTo(x, y));
let userScore = 0;
let starCount = 0;
let paused = true;
let finished = false;
let firstIter = true;
const howManyPrint = Math.floor(
process.stdout.rows * process.stdout.columns / 70
);
const takenNumbers = [];
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", (_chunk, key) => {
if (key && key.name == "s") {
if (interval != null) {
clearInterval(interval);
}
fallingStarsGame();
} else if (key && key.name == "c") {
process.exit(0);
}
if (finished) {
return;
}
if (key && key.name == "y") {
paused = false;
} else if (key && key.name == "p") {
paused = !paused;
} else if (key.name === "space") {
if (!paused && !takenNumbers.includes(starCount) && starCount % 9 === 0) {
userScore++;
const scoreText2 = chalk.bgYellow.black(
`SCORE: ${userScore.toString()}`
);
const startingLeftPosition2 = process.stdout.columns - stringWidth(scoreText2);
process.stdout.write(ansiEscapes.cursorTo(startingLeftPosition2, 0));
process.stdout.write(scoreText2);
takenNumbers.push(starCount);
}
}
});
const drawnPositions = [];
process.stdout.write(ansiEscapes.cursorTo(0, 0));
const redYellow = coloreme.redYellow.inverse;
process.stdout.write(chalk.bgGreen.hex(redYellow.c)("stars-watcher"));
const scoreText = chalk.bgYellow.black(`SCORE: ${userScore.toString()}`);
const startingLeftPosition = process.stdout.columns - stringWidth(scoreText);
process.stdout.write(ansiEscapes.cursorTo(startingLeftPosition, 0));
process.stdout.write(scoreText);
interval = setInterval(async () => {
if (paused) {
const halfColumns = process.stdout.columns / 2;
let y0 = process.stdout.rows / 2 - 6;
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
const instructions = [
`Watch appearing stars`,
`Press ${chalk.bold("[space]")} on every 9th, 18th... star's appearance`
];
let cnt = 0;
instructions.map((instruction, i) => {
process.stdout.write(ansiEscapes.cursorNextLine);
const instructionWidth = stringWidth(instruction);
process.stdout.write(
ansiEscapes.cursorTo(
halfColumns - Math.floor(instructionWidth / 2),
y0 + i
)
);
process.stdout.write(instruction);
process.stdout.write(ansiEscapes.cursorNextLine);
cnt++;
});
const prompt = `Are you ready? Press 'y' to start a round`;
const promptWidth = stringWidth(prompt);
process.stdout.write(
ansiEscapes.cursorTo(
halfColumns - Math.floor(promptWidth / 2),
y0 + cnt + 1
)
);
process.stdout.write(prompt);
firstIter = true;
return;
}
if (firstIter) {
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
firstIter = false;
} else {
for (let i in drawnPositions) {
process.stdout.write(ansiEscapes.cursorTo(...drawnPositions[i]));
process.stdout.write(figureSet.star);
}
}
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
for (let i in drawnPositions) {
process.stdout.write(ansiEscapes.cursorTo(...drawnPositions[i]));
process.stdout.write(figureSet.star);
}
const [x2, y2] = randPos$2();
drawnPositions.push([x2, y2]);
process.stdout.write(ansiEscapes.cursorTo(x2, y2));
process.stdout.write(figureSet.star);
starCount++;
if (starCount >= howManyPrint) {
clearInterval(interval);
finished = true;
}
if (finished) {
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
const status = `GAME OVER`;
let x0 = Math.floor(process.stdout.columns / 2 - stringWidth(status) / 2);
let y0 = process.stdout.rows / 2 - 4;
process.stdout.write(
ansiEscapes.cursorTo(
process.stdout.columns / 2 - 4,
process.stdout.rows / 2 - 3
)
);
process.stdout.write(status);
process.stdout.write(ansiEscapes.cursorNextLine);
const scoreStr = `SCORE: ${userScore.toString()}`;
x0 = process.stdout.columns / 2 - stringWidth(scoreStr) / 2;
process.stdout.write(ansiEscapes.cursorTo(x0, y0 + 2));
process.stdout.write(scoreStr);
const startGameStr = `Press ${chalk.bold("s")} to start a new round`;
x0 = process.stdout.columns / 2 - stringWidth(startGameStr) / 2;
process.stdout.write(ansiEscapes.cursorTo(x0, y0 + 5));
process.stdout.write(startGameStr);
}
}, 1e3);
};
const getLines = (text) => {
const lines = [];
let lineIndex = 0;
let acc = "";
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (stringWidth(acc + char) <= process.stdout.columns) {
if (char === "\n") {
lines[lineIndex] ||= "";
lines[lineIndex] += acc;
lineIndex++;
lines[lineIndex] ||= "";
lines[lineIndex] += char;
lineIndex++;
acc = "";
} else {
acc += char;
if (i === text.length - 1) {
lines[lineIndex] ||= "";
lines[lineIndex] += acc;
}
}
} else {
lines[lineIndex] ||= "";
let isAccModified = false;
if (i < text.length) {
if (![" ", ".", ",", "!", "?"].includes(text[i])) {
const lastSpaceIndex = acc.lastIndexOf(" ");
lines[lineIndex] += acc.substring(0, lastSpaceIndex + 1);
acc = acc.substring(lastSpaceIndex + 1) + char;
isAccModified = true;
}
}
if (!isAccModified) {
lines[lineIndex] += acc;
acc = char;
}
lineIndex++;
}
}
return lines;
};
const repeatString = (source, count) => join("", repeat(source, count));
const randPos$1 = (yMin = 0) => {
const randY = Math.floor(Math.random() * process.stdout.rows);
return [
Math.floor(Math.random() * process.stdout.columns),
randY > yMin ? randY : Math.floor(yMin + Math.random() * process.stdout.rows)
];
};
const lineByLine = (text, options = {}) => {
const intervalMs = options.delay || 1e3;
const lines = getLines(text);
let paused = false;
process.stdout.write(ansiEscapes.clearScreen);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", (_chunk, key) => {
if (key && key.name == "s") {
paused = false;
}
if (key && key.name == "p") {
paused = !paused;
}
if (key && key.name == "c") {
process.exit(0);
}
if (key.ctrl && key.name === "w") {
process.exit(0);
}
});
let m = 0;
let line = "";
let newTopCounter = 0;
const workingInterval = setInterval(() => {
if (paused) {
return;
}
line = lines[m];
if (line !== "\n") {
if (options.color) {
try {
const color = chalk[options.color];
process.stdout.write(color(line));
} catch (e) {
process.stdout.write(line);
}
} else {
process.stdout.write(line);
}
}
if (newTopCounter >= process.stdout.rows) {
process.stdout.write(ansiEscapes.eraseScreen);
process.stdout.write(ansiEscapes.cursorTo(0, 0));
newTopCounter = 0;
} else {
if (line !== "\n") {
process.stdout.write(ansiEscapes.cursorNextLine);
}
}
m++;
if (line !== "\n") newTopCounter++;
if (m === lines.length) {
clearInterval(workingInterval);
process.exit();
}
}, intervalMs);
};
const readFileLineByLine = (file, options = {}) => {
const text = fs.readFileSync(file, "utf-8");
lineByLine(text, options);
};
const randPos = (yMin = 0) => {
const randY = Math.floor(Math.random() * process.stdout.rows);
return [
Math.floor(Math.random() * process.stdout.columns),
randY > yMin ? randY : Math.floor(yMin + Math.random() * process.stdout.rows)
];
};
const coloredStarsGame = () => {
let interval;
process.stdout.write(ansiEscapes.eraseScreen);
const [x, y] = randPos();
process.stdout.write(ansiEscapes.cursorTo(x, y));
let userScore = 0;
let starCount = 0;
let coloredStarCount = 0;
let paused = true;
let finished = false;
let firstIter = true;
const howManyPrint = Math.floor(
process.stdout.rows * process.stdout.columns / 40
);
const takenNumbers = [];
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", (_chunk, key) => {
if (key && key.name == "s") {
if (interval != null) {
clearInterval(interval);
}
coloredStarsGame();
} else if (key && key.name == "c") {
process.exit(0);
}
if (finished) {
return;
}
if (key && key.name == "y") {
paused = false;
} else if (key && key.name == "p") {
paused = !paused;
} else if (key.name === "space") {
if (!paused && !takenNumbers.includes(coloredStarCount) && coloredStarCount % 9 === 0) {
userScore++;
const scoreText2 = chalk.bgYellow(`SCORE: ${userScore.toString()}`);
const startingLeftPosition2 = process.stdout.columns - stringWidth(scoreText2);
process.stdout.write(ansiEscapes.cursorTo(startingLeftPosition2, 0));
process.stdout.write(scoreText2);
takenNumbers.push(coloredStarCount);
}
}
});
const drawnPositions = [];
process.stdout.write(ansiEscapes.cursorTo(0, 0));
process.stdout.write(chalk.bgGreen("colored-stars-watcher"));
coloreme.yellowBlack.inverse;
const scoreText = chalk.bgYellow.red(`SCORE: ${userScore.toString()}`);
const startingLeftPosition = process.stdout.columns - stringWidth(scoreText);
process.stdout.write(ansiEscapes.cursorTo(startingLeftPosition, 0));
process.stdout.write(scoreText);
interval = setInterval(async () => {
if (paused) {
const halfColumns = process.stdout.columns / 2;
let y0 = process.stdout.rows / 2 - 6;
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
const instructions = [
`Watch appearing stars`,
`Press ${chalk.bold("[space]")} on every 9th, 18th... red colored star's appearance`
];
let cnt = 0;
instructions.map((instruction, i) => {
process.stdout.write(ansiEscapes.cursorNextLine);
const instructionWidth = stringWidth(instruction);
process.stdout.write(
ansiEscapes.cursorTo(
halfColumns - Math.floor(instructionWidth / 2),
y0 + i
)
);
process.stdout.write(instruction);
process.stdout.write(ansiEscapes.cursorNextLine);
cnt++;
});
const prompt = `Are you ready? Press 'y' to start a round`;
const promptWidth = stringWidth(prompt);
process.stdout.write(
ansiEscapes.cursorTo(
halfColumns - Math.floor(promptWidth / 2),
y0 + cnt + 1
)
);
process.stdout.write(prompt);
firstIter = true;
return;
}
if (firstIter) {
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
firstIter = false;
} else {
for (let i in drawnPositions) {
process.stdout.write(ansiEscapes.cursorTo(...drawnPositions[i]));
process.stdout.write(figureSet.star);
}
}
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
for (let i in drawnPositions) {
process.stdout.write(ansiEscapes.cursorTo(...drawnPositions[i]));
const colorFn = drawnPositions[i][2];
process.stdout.write(colorFn(figureSet.star));
}
let newRandPos = randPos();
while (R.find(
(el) => el[0] === newRandPos[0] && el[1] === newRandPos[1],
drawnPositions
)) {
newRandPos = randPos();
}
const [x2, y2] = newRandPos;
const colors = ["red", "green", "cyan", "magenta", "white", "blue"].map(
(col) => chalk[col]
);
let starColor = R.compose(R.head, shuffle)(colors);
if (starColor !== chalk.red && starCount > 25 && coloredStarCount < 19 && starCount % 6 === 0) {
starColor = chalk.red;
}
drawnPositions.push([x2, y2, starColor]);
process.stdout.write(ansiEscapes.cursorTo(x2, y2));
process.stdout.write(starColor(figureSet.star));
if (starColor === chalk.red) coloredStarCount++;
starCount++;
if (starCount >= howManyPrint) {
clearInterval(interval);
finished = true;
}
if (finished) {
process.stdout.write(ansiEscapes.cursorTo(0, 1));
process.stdout.write(ansiEscapes.eraseDown);
const status = `GAME OVER`;
let x0 = Math.floor(process.stdout.columns / 2 - stringWidth(status) / 2);
let y0 = process.stdout.rows / 2 - 4;
process.stdout.write(
ansiEscapes.cursorTo(
process.stdout.columns / 2 - 4,
process.stdout.rows / 2 - 3
)
);
process.stdout.write(status);
process.stdout.write(ansiEscapes.cursorNextLine);
const scoreStr = `SCORE: ${userScore.toString()}`;
x0 = process.stdout.columns / 2 - stringWidth(scoreStr) / 2;
process.stdout.write(ansiEscapes.cursorTo(x0, y0 + 2));
process.stdout.write(scoreStr);
process.stdout.write(ansiEscapes.cursorNextLine);
const startGameStr = `Press ${chalk.bold("s")} to start a new round`;
x0 = process.stdout.columns / 2 - stringWidth(startGameStr) / 2;
process.stdout.write(ansiEscapes.cursorTo(x0, y0 + 5));
process.stdout.write(startGameStr);
}
}, 1e3);
};
const snippet = `
import joinPath from '../Components/Functions/path/joinPath';
import dirname from '../Components/Functions/path/dirname';
import FileMetaData from '../Typings/fileMetaData';
import isTauri from '../Util/is-tauri';
import { CALCULATE_DIRS_SIZE_ENDPOINT, CHECK_EXIST_ENDPOINT, CHECK_ISDIR_ENDPOINT, OPEN_FILE_ENDPOINT } from '../Util/constants';
/** Invoke Rust command to handle files */
class FileAPI {
readonly fileName: string | string[];
readonly parentDir: string;
/**
* Construct FileAPI Class
* @param {string} fileName - Your file path
* @param {string} parentDir - Parent directory of the file
*/
constructor(fileName: string | string[], parentDir?: string) {
if (parentDir && typeof fileName === 'string') {
this.parentDir = parentDir;
this.fileName = joinPath(parentDir, fileName);
} else this.fileName = fileName;
}
/**
* Read text file
* @returns {Promise<any>}
*/
readFile(): Promise<string> {
return new Promise((resolve, reject) => {
if (typeof this.fileName === 'string') {
if (isTauri) {
const { fs } = require('@tauri-apps/api');
fs.readTextFile(this.fileName).then((fileContent: string) => resolve(fileContent));
} else {
reject('Read file is currently not supported on web version');
}
} else {
reject('File name is not a string');
}
});
}
async readBuffer(): Promise<Buffer> {
const Buffer = require('buffer/').Buffer;
return new Promise((resolve, reject) => {
if (typeof this.fileName === 'string') {
if (isTauri) {
const { fs } = require('@tauri-apps/api');
resolve(Buffer.from(fs.readBinaryFile(this.fileName).then((fileContent: string) => fileContent)));
} else {
reject('Read file is currently not supported on web version');
}
}
});
}
/**
* Open file on default app
* @returns {Promise<void>}
*/
async openFile(): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('open_file', { filePath: this.fileName });
} else {
await fetch(OPEN_FILE_ENDPOINT + this.fileName, { method: 'GET' });
return;
}
}
/**
* Get tauri url of local assets
* @returns {string}
*/
readAsset(): string {
if (isTauri) {
const { tauri } = require('@tauri-apps/api');
return typeof this.fileName === 'string' ? tauri.convertFileSrc(this.fileName) : '';
}
}
/**
* Read file and return as JSON
* @returns {Promise<JSON>}
*/
async readJSONFile(): Promise<JSON> {
const content = await this.readFile();
return JSON.parse(content);
}
/**
* Return true if file exist
* @returns {boolean}
*/
async exists(): Promise<boolean> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('file_exist', { filePath: this.fileName });
} else {
const exists = await (await fetch(CHECK_EXIST_ENDPOINT + this.fileName, { method: 'GET' })).json();
return exists;
}
}
/**
* Create file if it doesn't exist
* @returns {Promise<void>}
*/
async createFile(): Promise<void> {
if (typeof this.fileName === 'string') {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
await invoke('create_dir_recursive', {
dirPath: dirname(this.fileName),
});
return await invoke('create_file', { filePath: this.fileName });
} else {
return;
}
}
}
/**
* Read properties of a file
* @returns {Promise<FileMetaData>}
*/
async properties(): Promise<FileMetaData> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('get_file_properties', { filePath: this.fileName });
}
}
/**
* Check if given path is directory
* @returns {Promise<boolean>}
*/
async isDir(): Promise<boolean> {
return new Promise((resolve) => {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
invoke('is_dir', { path: this.fileName }).then((result: boolean) => resolve(result));
} else {
fetch(CHECK_ISDIR_ENDPOINT + this.fileName, { method: 'GET' })
.then((response) => response.json())
.then((result: boolean) => resolve(result));
}
});
}
/**
* Extract icon of executable file
* @returns {Promise<string>}
*/
async extractIcon(): Promise<string> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('extract_icon', { filePath: this.fileName });
}
}
/**
* Calculate total size of given file paths
* @returns {number} - Size in bytes
*/
async calculateFilesSize(): Promise<number> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('calculate_files_total_size', { files: this.fileName });
} else {
const paths = Array.isArray(this.fileName) ? this.fileName.join('%2c-%2c') : this.fileName;
console.log(paths);
const size = await (await fetch(CALCULATE_DIRS_SIZE_ENDPOINT + paths, { method: 'GET' })).json();
return size;
}
}
/**
* Compress file(s) to zip file
* @returns {Promise<void>}
*/
async zip(): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('compress_to_zip', { files: typeof this.fileName === 'string' ? [this.fileName] : this.fileName });
}
return;
}
/**
* Extract zip file
* @param {string} target_dir - Target directory to extract files
* @returns {any}
*/
async unzip(target_dir: string): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('decompress_from_zip', { zipPath: this.fileName, targetDir: target_dir });
}
return;
}
}
export default FileAPI;`;
const snippet2 = `
import isTauri from '../Util/is-tauri';
import DirectoryAPI from './directory';
/**
* Invoke Rust command to operate files/dirs
*/
class OperationAPI {
private src: string;
private dest: string;
constructor(src: string, dest?: string) {
this.src = src;
this.dest = dest;
}
/**
* Copy files/dirs
* @returns {Promise<void>}
*/
async copyFile(): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('copy', { src: this.src, dest: this.dest });
}
}
/**
* Rename file/dir
* @returns {any}
*/
async rename(): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
return await invoke('rename', { path: this.src, newPath: this.dest });
}
}
async cut(): Promise<void> {
await this.copyFile();
await this.unlink();
return;
}
/**
* Unlink files/dirs
* @returns {Promise<void>}
*/
async unlink(): Promise<void> {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
if (await new DirectoryAPI(this.src).isDir()) {
return await invoke('remove_dir', { path: this.src });
} else {
return await invoke('remove_file', { path: this.src });
}
}
}
async duplicate(): Promise<void> {
this.dest =
this.src.split('.').length > 1 ? this.src.split('.').slice(0, -1) + ' - COPY.' + this.src.split('.').slice(-1) : this.src + ' - COPY';
return await this.copyFile();
}
}
export default OperationAPI;
`;
const snippet3 = `
import isTauri from '../Util/is-tauri';
/**
* Write text into clipboard
* @param {string} text - Text you want to write to clipboard
* @returns {void}
*/
const writeTextToClipboard = async (text: string): Promise<void> => {
if (isTauri) {
const { clipboard } = require('@tauri-apps/api');
return await clipboard.writeText(text);
} else {
return await navigator.clipboard.writeText(text);
}
};
/**
* Read clipboard text
* @returns {Promise<string>}
*/
const readTextFromClipboard = async (): Promise<string> => {
if (isTauri) {
const { clipboard } = require('@tauri-apps/api');
return await clipboard.readText();
} else {
return await navigator.clipboard.readText();
}
};
export { writeTextToClipboard, readTextFromClipboard };
`;
const snippet4 = `
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import isTauri from '../Util/is-tauri';
import IStorageData from '../Typings/storageData';
// Store fetched data into variable
const data: IStorageData = {};
/**
* Set information to local storage
* @param {string} key - Information key
* @param {any} data - Your data
* @returns {Promise<void>}
*/
const set = async (key: string, value: any): Promise<void> => {
if (isTauri) {
data[key] = value;
const { invoke } = require('@tauri-apps/api');
return await invoke('write_data', { key, value });
} else {
data[key] = value;
localStorage.setItem(key, JSON.stringify(value));
}
};
/**
* Get information from local storage
* @param {string} key - Information key
* @param {boolean} force - Force fetcing the latest data
* @returns {Promise<any>} - Your data
*/
const get = async (key: string, force?: boolean): Promise<any> => {
interface returnedType {
status: boolean;
data: JSON;
}
if (Object.keys(data).includes(key) && !force) {
return data[key];
} else {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
const returnedData = (await invoke('read_data', { key })) as returnedType;
data[key] = returnedData.data;
return returnedData.status ? returnedData.data : {};
} else {
const storedData = localStorage.getItem(key);
if (storedData) {
data[key] = JSON.parse(storedData);
return data[key];
} else {
return {};
}
}
}
};
/**
* Remove a data
* @param {string} key
* @returns {any}
*/
const remove = async (key: string): Promise<void> => {
if (isTauri) {
const { invoke } = require('@tauri-apps/api');
await invoke('delete_storage_data', { key });
} else {
localStorage.removeItem(key);
}
};
export default { get, set, remove };
`;
const snippets = [snippet, snippet2, snippet3, snippet4];
const hackerTypes = () => {
let offset = 0;
process.stdout.write(ansiEscapes.clearScreen);
readline.emitKeypressEvents(process.stdin);
const snippet = shuffle(snippets)[0];
process.stdin.setRawMode(true);
process.stdin.on("keypress", (_chunk, key) => {
if (key && key.name == "s") ; else if (key && key.name == "p") ; else if (key.ctrl && key.name === "c") {
process.exit(0);
} else {
if (offset < snippet.length) {
process.stdout.write(snippet.slice(offset, offset + 1));
offset++;
}
}
});
};
const start = async (roundFigure, { size }) => {
let score = 0;
let currentFigure = "";
let currentColor = chalk.green;
let currentCorrectIndex = 0;
const correctIndexes = [];
const answeredIndexes = [];
const gridSize = size;
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", (_chunk, key) => {
if (key && key.name == "q") {
process.exit();
} else if (key && key.name == "n") {
if (currentFigure === roundFigure) {
const lastIndex = correctIndexes[correctIndexes.length - 1];
if (!answeredIndexes.includes(lastIndex)) {
score++;
answeredIndexes.push(lastIndex);
logUpdate(getString());
}
}
} else if (key.name === "space") {
if (currentFigure === roundFigure) {
const lastIndex = correctIndexes[correctIndexes.length - 1];
if (!answeredIndexes.includes(lastIndex)) {
score++;
answeredIndexes.push(lastIndex);
logUpdate(getString());
}
}
} else if (key.name === "down") ; else if (key.name === "up") {
process.exit();
} else if (key.name === "s" || key.name === "return") ; else if ("12345678a9".includes(key.name)) {
score++;
}
});
const items = [];
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
let rndIndex = Math.floor(Math.random() * gridSize);
if (j === rndIndex) {
items.push([i, j, figureSet.circle, true]);
} else {
const figureToInsert = shuffle([
figureSet.triangleLeft,
figureSet.triangleRight,
figureSet.triangleDown,
figureSet.triangleUp
])[0];
items.push([i, j, figureToInsert]);
}
}
process.stdout.write(ansiEscapes.cursorTo(0, 0));
process.stdout.write(ansiEscapes.eraseScreen);
process.stdout.write(ansiEscapes.cursorNextLine);
process.stdout.write(figureSet.home);
}
let lineOffset = 10;
let displayable = join("", repeat(" ", lineOffset));
const getString = () => {
return `
Press ${chalk.bold("[space]")} on '${roundFigure}' appearance.
Score: ${score}
${displayable}
`;
};
let columnCounter = 0;
for (let y = 0; y < items.length; y++) {
currentColor = y % 2 === 0 ? chalk.cyan : chalk.red;
currentFigure = `${items[y][2]}`;
if (currentFigure === roundFigure) {
correctIndexes.push(currentCorrectIndex);
}
if (displayable === "") {
displayable = "";
}
displayable += ` ${currentColor(currentFigure)} `;
if (columnCounter + 1 >= gridSize) {
displayable += `
${join("", repeat(" ", lineOffset))}`;
columnCounter = -1;
}
logUpdate(getString());
await delay(items[y][2] === figureSet.heart ? 600 : 450);
columnCounter++;
currentCorrectIndex++;
}
logUpdate(getString());
};
const watchFigureGame = async ({ size }) => {
const roundFigure = figureSet.circle;
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key && key.name == "q") {
process.exit();
} else if (key && key.name == "y") {
{
await start(roundFigure, { size });
}
}
});
process.stdout.write(ansiEscapes.clearScreen);
process.stdout.write(repeatString(" ", 26) + chalk.green("watch-figure"));
process.stdout.write(repeatString(ansiEscapes.cursorNextLine, 4));
process.stdout.write(`
There are different figures are appearing during a game round.
Your task is to watch the ${roundFigure} figure appearance
and press ${chalk.bold("[space]")} before the next figure has been shown.
Start ${chalk.bold.green("y")} if you are ready.
`);
};
let biteX = 0;
let biteLength = 10;
let score = 0;
let gameStatus = "RUNNING";
const previousDrawnFigures = [];
let roundFigures = [];
let nextIndex = 0;
const drawFigs = () => {
const { rows } = process.stdout;
if (previousDrawnFigures.length > 0) {
findIndex(propEq(false, "done"), roundFigures);
if (nextIndex > -1 && nextIndex < roundFigures.length) {
const [x0, y0] = previousDrawnFigures[nextIndex];
process.stdout.write(ansiEscapes.cursorTo(x0, y0));
process.stdout.write(" ");
process.stdout.write(ansiEscapes.cursorTo(x0 + 1, y0 + 1));
process.stdout.write(figureSet.hamburger);
const remain = addIndex(filter)((_a, i) => {
return i !== nextIndex && !roundFigures[i].done;
}, previousDrawnFigures);
previousDrawnFigures[nextIndex] = [x0 + 1, y0 + 1];
if (y0 + 1 > rows - 2) {
if (biteX < x0 && biteX + biteLength >= x0) {
score++;
roundFigures = adjust(
nextIndex,
(a) => set(lensProp("done"), true, set(lensProp("win"), true, a)),
roundFigures
);
nextIndex = findIndex(propEq(false, "done"), roundFigures);
} else {
roundFigures = adjust(
nextIndex,
(a) => set(lensProp("done"), true, set(lensProp("win"), false, a)),
roundFigures
);
nextIndex = findIndex(propEq(false, "done"), roundFigures);
}
}
map((item) => {
const [x, y] = item;
process.stdout.write(ansiEscapes.cursorTo(x, y));
process.stdout.write(figureSet.hamburger);
}, remain);
} else {
gameStatus = "FINISHED";
nextIndex = -1;
}
} else {
times(() => {
const [x, y] = randPos$1(1);
process.stdout.write(ansiEscapes.cursorTo(x, y));
previousDrawnFigures.push([x, y]);
process.stdout.write(figureSet.hamburger);
}, 10);
addIndex(map)((item, i) => {
roundFigures.push({ index: i, done: false, win: false });
}, previousDrawnFigures);
}
};
const drawBite = (x) => {
const { rows } = process.stdout;
biteX = x;
process.stdout.write(ansiEscapes.cursorTo(0, rows - 1));
process.stdout.write(ansiEscapes.eraseEndLine);
process.stdout.write(ansiEscapes.cursorTo(x, rows - 1));
process.stdout.write(join("", repeat(figureSet.squareBottom, 10)));
};
const drawScore = () => {
process.stdout.write(ansiEscapes.cursorTo(3, 1));
process.stdout.write(`SCORE: ${score}`);
};
const drawStatus = () => {
process.stdout.write(ansiEscapes.cursorTo(19, 1));
process.stdout.write(`STATUS: ${gameStatus}`);
};
const drawGameOver = () => {
const { rows, columns } = process.stdout;
process.stdout.write(ansiEscapes.cursorTo(0));
process.stdout.write(ansiEscapes.eraseScreen);
process.stdout.write(ansiEscapes.clearScreen);
const scoreStr = chalk.green(`SCORE: ${score}`);
const scoreStrWidth = stringWidth(scoreStr);
process.stdout.write(
ansiEscapes.cursorTo(
columns / 2 - Math.floor(scoreStrWidth / 2),
rows / 2 - 4
)
);
process.stdout.write(scoreStr);
process.stdout.write(ansiEscapes.cursorNextLine);
const instruction = `Press ${chalk.bold("n")} to start a new game`;
const instructionWidth = stringWidth(instruction);
process.stdout.write(
ansiEscapes.cursorTo(
columns / 2 - Math.floor(instructionWidth / 2),
rows / 2 - 2
)
);
process.stdout.write(instruction);
};
const kardo = async () => {
const { rows } = process.stdout;
const columns = process.stdout.columns - 12;
let secondIter = 0;
let x = 15;
process.stdout.write(ansiEscapes.cursorForward(7));
process.stdout.write(ansiEscapes.clearScreen);
process.stdout.write(ansiEscapes.cursorHide);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (gameStatus === "FINISHED") {
if (key.name === "n") {
gameStatus = "RUNNING";
nextIndex = 0;
await kardo();
}
}
if (key && key.name == "s") ; else if (key && key.name == "c") {
process.exit(0);
}
if (key && key.name == "right") {
if (x - 1 < columns) {
x++;
drawBite(x);
}
} else if (key && key.name == "p") ; else if (key.name === "left") {
if (x > 0) {
x--;
drawBite(x);
}
}
});
process.stdout.write(ansiEscapes.clearScreen);
await delay(1200);
let currentIndex = 0;
let figures = [
figureSet.circle,
figureSet.squareSmall,
figureSet.squareSmallFilled,
figureSet.heart
];
let interval = setInterval(() => {
let curr = 1;
process.stdout.write(ansiEscapes.cursorTo(0, 0));
process.stdout.write(ansiEscapes.clearScreen);
curr++;
if (curr < columns) {
drawBite(x);
drawFigs();
drawScore();
drawStatus();
if (gameStatus === "FINISHED" || nextIndex >= roundFigures.length) {
clearInterval(interval);
drawGameOver();
return;
}
secondIter++;
figures[currentIndex];
if (secondIter >= rows - 1) {
secondIter = 0;
if (currentIndex >= process.stdout.columns - 30 - biteLength * 2) {
currentIndex = 1;
clearInterval(interval);
}
}
}
}, 150);
};
const drawHamburgers$1 = () => {
process.stdout.write(ansiEscapes.clearScreen);
const taken = [];
const interval = setInterval(() => {
let tries = 0;
let x;
let y;
while (tries < 7) {
[x, y] = randPos$1();
if (taken.some((a) => a[0] === x && a[1] === y)) {
tries++;
} else {
taken.push([x, y]);
tries = 0;
break;
}
}
if (tries > 5) {
clearInterval(interval);
process.stdout.write(ansiEscapes.cursorTo(0, process.stdout.rows - 1));
process.stdout.write(ansiEscapes.eraseEndLine);
process.stdout.write(
ansiEscapes.cursorTo(
process.stdout.columns / 2,
process.stdout.rows - 1
)
);
process.stdout.write(
chalk.cyan(`${chalk.bold("r")} - run again ${chalk.bold("q")} - exit`)
);
}
process.stdout.write(ansiEscapes.cursorTo(x, y));
process.stdout.write(figureSet.hamburger);
});
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawHamburgers$1();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const diag = async () => {
const sentence = "This comprehensive collection offers the latest trends, ideas, and innovations in both residential and commercial spaces.";
process.stdout.write(ansiEscapes.clearScreen);
const size = terminalSize();
for (let i = 0; i < sentence.length; i++) {
process.stdout.write(sentence[i]);
await delay(500);
process.stdout.write(ansiEscapes.cursorDown());
process.stdout.write(ansiEscapes.cursorForward());
if (i !== 0 && i % size.rows === 0) {
process.stdout.write(ansiEscapes.clearScreen);
}
}
};
const drawMixFigures = ({ colored }) => {
process.stdout.write(ansiEscapes.clearScreen);
const taken = [];
const interval = setInterval(() => {
let tries = 0;
let x;
let y;
while (tries < 7) {
[x, y] = randPos$1();
if (taken.some((a) => a[0] === x && a[1] === y)) {
tries++;
} else {
taken.push([x, y]);
tries = 0;
break;
}
}
if (tries > 5) {
clearInterval(interval);
process.stdout.write(ansiEscapes.cursorTo(0, process.stdout.rows - 1));
process.stdout.write(ansiEscapes.eraseEndLine);
process.stdout.write(
ansiEscapes.cursorTo(
process.stdout.columns / 2,
process.stdout.rows - 1
)
);
process.stdout.write(
chalk.cyan(`${chalk.bold("r")} - run again ${chalk.bold("q")} - exit`)
);
}
process.stdout.write(ansiEscapes.cursorTo(x, y));
const figure = shuffle([
figureSet.hamburger,
figureSet.squareCenter,
figureSet.circle,
figureSet.checkboxOff
])[0];
if (colored) {
const color = shuffle([
chalk.cyan,
chalk.blue,
chalk.gray,
chalk.green,
chalk.yellow,
chalk.magenta
])[0];
process.stdout.write(color(figure));
} else {
process.stdout.write(figure);
}
});
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawHamburgers();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const drawY = ({ colored }) => {
process.stdout.write(ansiEscapes.clearScreen);
let pointIndex = 0;
let points = [
[
[15, 16],
[17, 16],
[19, 16],
[21, 17],
[23, 19],
[25, 21]
],
[
[16, 18],
[17, 20],
[18, 21],
[20, 22],
[20, 24],
[22, 26],
[20, 22]
],
[
[22, 18],
[22, 20],
[22, 21],
[24, 21],
[26, 21],
[28, 21],
[24, 20]
],
[
[15, 18],
[12, 10],
[12, 11],
[14, 12],
[15, 14],
[16, 13],
[14, 15]
]
];
const interval = setInterval(() => {
process.stdout.write(ansiEscapes.clearScreen);
let currPoints = points[pointIndex];
pointIndex++;
addIndex(map)((a, i) => {
process.stdout.write(ansiEscapes.cursorTo(a[0], a[1]));
const clr = shuffle([
chalk.green,
chalk.cyan,
chalk.magenta,
chalk.black,
chalk.gray
])[0];
process.stdout.write(
clr(i % 2 === 0 ? figureSet.triangleUp : figureSet.triangleDown)
);
}, currPoints);
process.stdout.write(currPoints.length.toString());
if (pointIndex >= points.length) {
clearInterval(interval);
}
}, 500);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawY();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const indexedMap$4 = addIndex(map);
const sumLengths = compose(sum, map(prop("length")));
const drawTriangles = () => {
process.stdout.write(ansiEscapes.clearScreen);
let step = 0;
let pointIndex = 0;
const verticalCount = 9;
const horizontalCount = 15;
let upToDown = times((i) => [10, i + 5], verticalCount);
let leftToRight = filter(
(el) => !isNil(el[0]),
times(
(i) => [i % 2 !== 0 ? null : 11 + i + 1, verticalCount + 4],
horizontalCount
)
);
let downUp = times(
(i) => [11 + horizontalCount, verticalCount + 3 - i],
verticalCount - 1
);
let rightLeft = filter(
(el) => !isNil(el[0]),
times(
(i) => [i % 2 !== 0 ? null : 9 + horizontalCount - i, 5],
horizontalCount
)
);
const directions = ["upDown", "leftRight", "downUp", "rightLeft"];
const directionToFigure = zipObj(directions, [
figureSet.triangleDown,
figureSet.triangleRight,
figureSet.triangleUp,
figureSet.triangleLeft
]);
zipObj(directions, range(0, 4));
let all = zipObj(directions, [upToDown, leftToRight, downUp, rightLeft]);
const interval = setInterval(() => {
sumLengths(values(all));
process.stdout.write(ansiEscapes.clearScreen);
indexedMap$4((key, i) => {
const items = all[key];
indexedMap$4((item, j) => {
process.stdout.write(ansiEscapes.cursorTo(item[0], item[1]));
const clr = equals(key === "upDown" ? step + 1 : step, j) ? chalk.green : chalk.gray;
const na = j === items.length - 1 ? figureSet.squareCenter : key === "leftRight" ? directionToFigure[key] : directionToFigure[key];
process.stdout.write(
key === "leftRight" && j % 2 !== 0 ? clr(na) : clr(na)
);
}, items);
}, keys(all));
let centerFigure;
let currDir = directions[step % directions.length];
let curCol;
if (currDir === "upDown") {
centerFigure = figureSet.triangleDown;
curCol = chalk.cyan;
} else if (currDir === "leftRight") {
centerFigure = figureSet.triangleRight;
curCol = chalk.green;
} else if (currDir === "downUp") {
centerFigure = figureSet.triangleUp;
curCol = chalk.yellow;
} else {
centerFigure = figureSet.triangleLeft;
curCol = chalk.magenta;
}
process.stdout.write(
ansiEscapes.cursorTo(
Math.floor((22 + horizontalCount) / 2),
Math.floor((10 + verticalCount) / 2)
)
);
process.stdout.write(curCol(centerFigure));
step++;
pointIndex++;
const allValues = values(all);
const maxLength = reduce(
(prev, current) => prev < current.length ? current.length : prev,
allValues[0].length,
allValues.slice(1)
);
if (pointIndex >= maxLength) {
process.stdout.write(
ansiEscapes.cursorTo(Math.floor(process.stdout.rows / 2) - 6, 1)
);
process.stdout.write(
`${chalk.blue.bold("r")} - run again ${chalk.blue.bold("q")} - exit`
);
clearInterval(interval);
}
}, 1e3);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawTriangles();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const drawX$1 = ({ colored }) => {
process.stdout.write(ansiEscapes.clearScreen);
let step = 0;
let pointIndex = 0;
let points = [
[
[9, 9],
[10, 10],
[11, 9],
[12, 10],
[13, 11],
[11, 11]
],
[
[11, 9],
[12, 10],
[8, 7],
[8, 10],
[8, 8],
[11, 11]
]
];
const interval = setInterval(() => {
process.stdout.write(ansiEscapes.clearScreen);
let currPoints = points[pointIndex];
if (step > 0) {
points = addIndex(map)(
(a, i) => [
i % 2 === 0 ? a[0] + i : a[0] - i,
i % 2 === 0 ? a[1] + i : a[1] - i
],
points
);
}
step++;
pointIndex++;
map((a) => {
process.stdout.write(ansiEscapes.cursorTo(a[0], a[1]));
process.stdout.write(figureSet.bullet);
}, currPoints);
if (pointIndex >= points.length) {
clearInterval(interval);
}
}, 500);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawX$1();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const drawIt = ({ colored }) => {
const fncs = shuffle([drawX$1, drawY])[0];
fncs({ colored });
};
const drawBar = () => {
const fncs = shuffle([drawTriangles])[0];
fncs();
};
const indexedMap$3 = addIndex(map);
compose(sum, map(prop("length")));
const drawCircles = () => {
process.stdout.write(ansiEscapes.clearScreen);
let step = 0;
let pointIndex = 0;
let positions = [
[6, 9],
[9, 7],
[14, 6],
[19, 7],
[23, 9],
[19, 11],
[14, 13],
[9, 11]
];
const interval = setInterval(() => {
process.stdout.write(ansiEscapes.clearScreen);
indexedMap$3((item, i) => {
step === i ? chalk.green : chalk.cyan;
if (step === i) {
process.stdout.write(ansiEscapes.cursorTo(item[0], item[1] - 1));
const triColor = step % 2 === 0 ? chalk.green : chalk.blue;
process.stdout.write(triColor(figureSet.triangleDown));
}
const currentFigure = step === i ? figureSet.circle : figureSet.circleFilled;
process.stdout.write(ansiEscapes.cursorTo(item[0], item[1]));
process.stdout.write(chalk.green(currentFigure));
}, positions);
if (step === positions.length - 1) {
step = -1;
}
step++;
pointIndex++;
if (pointIndex >= Math.floor(Math.random() * positions.length * 53 + 23)) {
process.stdout.write(ansiEscapes.cursorTo(process.stdout.rows / 2, 1));
process.stdout.write(
`${chalk.blue.bold("r")} - run again ${chalk.blue.bold("q")} - exit`
);
clearInterval(interval);
}
}, 1e3);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawCircles();
}
if (key && key.name == "q") {
process.exit();
}
});
};
addIndex(map);
compose(sum, map(prop("length")));
const drawHisa = () => {
process.stdout.write(ansiEscapes.clearScreen);
let step = 0;
let stepY = 0;
let pointIndex = 0;
const interval = setInterval(() => {
const rows = 12;
const cols = 12;
const total = rows * cols;
process.stdout.write(ansiEscapes.clearScreen);
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
let currentFigure;
if (step === i && stepY === j) {
currentFigure = figureSet.lineDownDoubleLeftDoubleRightDouble;
}
if (isNil(currentFigure)) {
currentFigure = figureSet.circleDotted;
}
if (i % 2 !== 0) {
process.stdout.write(ansiEscapes.cursorTo(i + 9, j + 2));
process.stdout.write(chalk.green(currentFigure));
} else {
process.stdout.write(ansiEscapes.cursorTo(i + 9, j + 2));
process.stdout.write(chalk.green(currentFigure));
}
}
}
if (step === rows - 1) {
step = -1;
}
if (stepY === cols - 1) {
stepY = -1;
}
{
stepY = -1;
}
step++;
stepY++;
pointIndex++;
if (pointIndex >= Math.floor(Math.random() * total * 2 + 23)) {
process.stdout.write(ansiEscapes.cursorTo(process.stdout.rows / 2, 1));
process.stdout.write(
`${chalk.blue.bold("r")} - run again ${chalk.blue.bold("q")} - exit`
);
clearInterval(interval);
}
}, 400);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawHisa();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const indexedMap$2 = addIndex(map);
compose(sum, map(prop("length")));
const drawMisc = () => {
process.stdout.write(ansiEscapes.clearScreen);
let step = 0;
let pointIndex = 0;
let taken = [];
let positions = [];
let firstSet = [
figureSet.tick,
figureSet.info,
figureSet.warning,
figureSet.cross,
figureSet.square,
figureSet.squareDarkShade,
figureSet.circle,
figureSet.circleCross,
figureSet.circlePipe,
figureSet.checkboxOff,
figureSet.checkboxOn,
figureSet.bullet,
figureSet.home
];
for (let y = 8; y < 23; y++) {
for (let u = 9; u < 24; u++) {
positions.push([y, u]);
}
}
const interval = setInterval(() => {
process.stdout.write(ansiEscapes.clearScreen);
indexedMap$2((item, i) => {
if (i % 2 === 0) {
step === i ? chalk.green : chalk.cyan;
if (step === i) {
process.stdout.write(ansiEscapes.cursorTo(item[0] - 1, item[1]));
const triColor = step % 2 === 0 ? chalk.green : chalk.blue;
process.stdout.write(triColor(figureSet.pointer));
}
if (taken.length === firstSet.length) {
taken = [];
}
const remained = difference(firstSet, taken);
const currentFigure = shuffle(remained)[0];
taken.push(currentFigure);
process.stdout.write(ansiEscapes.cursorTo(item[0], item[1]));
process.stdout.write(chalk.green(currentFigure));
}
}, positions);
if (step === positions.length - 1) {
step = -1;
}
step++;
pointIndex++;
if (pointIndex >= Math.floor(Math.random() * positions.length * 53 + 23)) {
process.stdout.write(ansiEscapes.cursorTo(process.stdout.rows / 2, 1));
process.stdout.write(
`${chalk.blue.bold("r")} - run again ${chalk.blue.bold("q")} - exit`
);
clearInterval(interval);
}
}, 1e3);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on("keypress", async (_chunk, key) => {
if (key.name == "r") {
drawMisc();
}
if (key && key.name == "q") {
process.exit();
}
});
};
const circledWords = () => {
process.stdout.write(ansiEscapes.clearScreen);
let offs = 0;
let arr = [
{ item: "a", coords: [5, 5] },
{ item: "b", coords: [8, 6] },
{ item: "c", coords: [11, 7] },
{ item: "d", coords: [8, 8] },
{ item: "e", coords: [5, 9] },
{ item: "f", coords: [2, 8] },
{ item: "g", coords: [0, 7] },
{ item: "h", coords: [