diff --git a/tools/coin-flip/banner.png b/tools/coin-flip/banner.png new file mode 100644 index 00000000..7113e8a7 Binary files /dev/null and b/tools/coin-flip/banner.png differ diff --git a/tools/coin-flip/icon.png b/tools/coin-flip/icon.png new file mode 100644 index 00000000..a02d1c0d Binary files /dev/null and b/tools/coin-flip/icon.png differ diff --git a/tools/coin-flip/index.test.ts b/tools/coin-flip/index.test.ts new file mode 100644 index 00000000..e02d52ed --- /dev/null +++ b/tools/coin-flip/index.test.ts @@ -0,0 +1,54 @@ +import { expect } from '@jest/globals'; +import { getToolTestClient } from '../../src/test/utils'; +import * as path from 'path'; + +describe('Coin Flip Tool', () => { + const toolPath = path.join(__dirname, 'tool.ts'); + const client = getToolTestClient(); + + it('flips a coin with default parameters (3 sides)', async () => { + const response = await client.executeToolFromFile(toolPath, {}); + + expect(response).toHaveProperty('result'); + expect(['-', '0', '+']).toContain(response.result); + }); + + it('flips a coin with custom sides', async () => { + const response = await client.executeToolFromFile(toolPath, { + sides: 2 + }); + + expect(response).toHaveProperty('result'); + expect(['heads', 'tails']).toContain(response.result); + }); + + it('flips a coin with custom side names', async () => { + const sideNames = ['past', 'present', 'future']; + const response = await client.executeToolFromFile(toolPath, { + sides: 3, + sideNames + }); + + expect(response).toHaveProperty('result'); + expect(sideNames).toContain(response.result); + }); + + it('handles invalid side names length', async () => { + const response = await client.executeToolFromFile(toolPath, { + sides: 2, + sideNames: ['one', 'two', 'three'] + }); + + expect(response).toHaveProperty('error'); + expect(response.error).toContain('Number of side names'); + }); + + it('handles negative sides', async () => { + const response = await client.executeToolFromFile(toolPath, { + sides: -1 + }); + + expect(response).toHaveProperty('error'); + expect(response.error).toContain('negative sides'); + }); +}); diff --git a/tools/coin-flip/metadata.json b/tools/coin-flip/metadata.json new file mode 100644 index 00000000..9bc149fa --- /dev/null +++ b/tools/coin-flip/metadata.json @@ -0,0 +1,45 @@ +{ + "id": "coin-flip", + "version": "1.0.0", + "name": "Coin Flip Tool", + "description": "Flip a coin with n sides using true randomness from random.org. Supports custom side names and various meta-usage patterns.", + "author": "Shinkai", + "keywords": ["random", "coin-flip", "decision-making", "shinkai"], + "tool_type": "typescript", + "configurations": { + "type": "object", + "properties": {}, + "required": [] + }, + "parameters": { + "type": "object", + "properties": { + "sides": { + "type": "number", + "description": "Number of sides (default: 3)" + }, + "sideNames": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional custom names for sides (must match number of sides)" + } + }, + "required": [] + }, + "result": { + "type": "object", + "properties": { + "result": { + "type": "string", + "description": "The result of the coin flip" + }, + "error": { + "type": "string", + "description": "Error message if the flip failed" + } + }, + "required": ["result"] + } +} diff --git a/tools/coin-flip/store.json b/tools/coin-flip/store.json new file mode 100644 index 00000000..c3ac9901 --- /dev/null +++ b/tools/coin-flip/store.json @@ -0,0 +1,3 @@ +{ + "categoryId": "5f10d0b4-6acd-477a-96e1-be35634465b2" +} diff --git a/tools/coin-flip/tool.ts b/tools/coin-flip/tool.ts new file mode 100644 index 00000000..e0b9f1f7 --- /dev/null +++ b/tools/coin-flip/tool.ts @@ -0,0 +1,79 @@ +import axios from 'npm:axios@1.7.7'; + +type Configurations = Record; + +type Parameters = { + sides?: number; + sideNames?: string[]; +}; + +type Result = { + result: string; + error?: string; +}; + +export type Run, I extends Record, R extends Record> = ( + config: C, + inputs: I +) => Promise; + +export const run: Run = async ( + _configurations: Configurations, + params: Parameters +): Promise => { + try { + const sides = params.sides ?? 3; + const sideNames = params.sideNames; + + if (sideNames && sideNames.length !== sides) { + return { + result: '', + error: `Number of side names (${sideNames.length}) must match number of sides (${sides})` + }; + } + + if (sides === 0) { + return { result: 'The coin vanished into another dimension! 🌀' }; + } + + if (sides === 1) { + return { result: '_' }; + } + + if (sides < 0) { + return { result: '', error: 'Cannot flip a coin with negative sides!' }; + } + + const response = await axios.get('https://www.random.org/integers/', { + params: { + num: 1, + min: 1, + max: sides, + col: 1, + base: 10, + format: 'plain', + rnd: 'new' + } + }); + + const result = parseInt(response.data); + let output: string; + + if (sideNames) { + output = sideNames[result - 1].toLowerCase(); + } else if (sides === 2) { + output = result === 1 ? 'heads' : 'tails'; + } else if (sides === 3) { + output = result === 1 ? '-' : result === 2 ? '0' : '+'; + } else { + output = `side ${result}`; + } + + return { result: output }; + } catch (error) { + return { + result: '', + error: `Error flipping coin: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +};