Skip to content

Commit

Permalink
feat: add dice hints
Browse files Browse the repository at this point in the history
  • Loading branch information
jziggas committed Dec 6, 2024
1 parent 8c0d6e8 commit 8c822df
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 113 deletions.
4 changes: 2 additions & 2 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ coverage:
status:
project:
default:
target: 75% # Require 75% total coverage for the project
target: 50% # Require total coverage for the project
threshold: 0% # No allowable drop in total coverage
informational: false # Make it blocking (default)
informational: true # Make it blocking if false
patch:
default:
enabled: false # Disable patch (diff) coverage check
122 changes: 60 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,71 @@

A TypeScript library that creates dice rolls using the [narrative dice system](https://star-wars-rpg-ffg.fandom.com/wiki/Narrative_Dice) for the Star Wars Role-Playing Game by [Fantasy Flight Games](https://www.fantasyflightgames.com/en/starwarsrpg/) and [Edge Studio](https://www.edge-studio.net/categories-games/starwarsrpg/).

## Features

- Complete narrative dice system implementation
- Detailed roll breakdown for each die
- Action hints to suggest possible uses for advantages, triumphs, etc.
- Roll results include:
- Successes / Failures
- Advantages / Threats
- Triumphs / Despairs
- Light / Dark Side Points (Force dice)
- Comprehensive Test Coverage
- The safety of TypeScript
- CLI Support

## Installation

### As a CLI Tool

To use the dice roller from the command line:

```bash
npm i -g @swrpg-online/dice
```

### As a project dependency

```bash
npm i @swrpg-online/dice
```

or optionally to use as a CLI command you can install globally with `npm i -g @swrpg-online/dice`
## CLI Usage

## Features
```
swrpg-dice <dice-notation> [options]
```

- Complete narrative dice system implementation
- Detailed roll breakdown for each die
- Comprehensive test coverage
- TypeScript type safety
- roll from a CLI
Example:

## Usage
```
swrpg-dice 2y 1g 1p 1b 1sb --hints
```

Output:

via code:
```
1 Success(es), 4 Advantage(s), 1 Threat(s)
Possible actions:
• 1 Advantage or 1 Triumph - Recover one strain (may be applied more than once).
• 1 Advantage or 1 Triumph - Add a boost die to the next allied active character's check.
• 1 Advantage or 1 Triumph - Notice a single important point in the ongoing conflict, such as the location of a blast door's control panel or
...
```

Dice Options:

- y/pro = Yellow / Proficiency
- g/a = Green / Ability
- b/boo = Blue / Boost
- r/c = Red / Challenge
- p/diff = Purple / Difficulty
- blk/k/sb/s = Black / Setback
- w/f = White / Force

## Programmatic Usage

```typescript
import { roll, DicePool } from '@swrpg-online/dice';
Expand All @@ -39,9 +85,8 @@ const pool: DicePool = {

const result = roll(pool);

// Access detailed results
console.log(result.results); // Array of individual die results
console.log(result.summary); // Summary of total successes, advantages, etc.
console.log(result.results);
console.log(result.summary);

=> {
"results": [
Expand All @@ -57,18 +102,6 @@ console.log(result.summary); // Summary of total successes, advantages, etc.
"despair": 0
}
},
{
"type": "ability",
"roll": 3,
"result": {
"successes": 1,
"failures": 0,
"advantages": 0,
"threats": 0,
"triumphs": 0,
"despair": 0
}
},
{
"type": "proficiency",
"roll": 10,
Expand All @@ -81,30 +114,7 @@ console.log(result.summary); // Summary of total successes, advantages, etc.
"despair": 0
}
},
{
"type": "difficulty",
"roll": 2,
"result": {
"successes": 0,
"failures": 1,
"advantages": 0,
"threats": 0,
"triumphs": 0,
"despair": 0
}
},
{
"type": "challenge",
"roll": 11,
"result": {
"successes": 0,
"failures": 0,
"advantages": 0,
"threats": 2,
"triumphs": 0,
"despair": 0
}
}
...
],
"summary": {
"successes": 0,
Expand All @@ -117,21 +127,9 @@ console.log(result.summary); // Summary of total successes, advantages, etc.
}
```

Each roll result includes:

- Detailed breakdown of each die roll
- Die type identification
- Individual results per die
- Overall summary of the roll

# Roadmap or Under Review
# License

- implement the Force die
- implement ability to add success, failure, and so on to dice pools
- ship combat?
- crits?
- polyhedral dice for convenience?
- anything else?
This project is licensed under the MIT License.

# Contribution

Expand Down
74 changes: 33 additions & 41 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { roll } from "./dice";
import { DicePool } from "./types";
import { DicePool, RollResult } from "./types";
// import * as path from 'path';

export function parseDiceNotation(input: string): DicePool {
Expand All @@ -15,26 +15,6 @@ export function parseDiceNotation(input: string): DicePool {
forceDice: 0,
};

// function getImagePath(type: string): string {
// const basePath = path.join(__dirname, 'images');
// switch (type) {
// case 'successes':
// return path.join(basePath, 'success.svg'); // Adjust path and extension as needed
// case 'failures':
// return path.join(basePath, 'failure.svg');
// case 'advantages':
// return path.join(basePath, 'advantage.svg');
// case 'threats':
// return path.join(basePath, 'threat.svg');
// case 'triumphs':
// return path.join(basePath, 'triumph.svg');
// case 'despair':
// return path.join(basePath, 'despair.svg');
// default:
// return '';
// }
// }

const parts = input.toLowerCase().trim().split(" ");

for (const part of parts) {
Expand Down Expand Up @@ -103,45 +83,57 @@ export function parseDiceNotation(input: string): DicePool {
return pool;
}

export function formatResult(result: any): string {
const parts = [];
export const formatResult = (result: RollResult): string => {
const effects: string[] = [];

if (result.summary.successes > 0)
parts.push(`${result.summary.successes} Success(es)`);
effects.push(`${result.summary.successes} Success(es)`);
if (result.summary.failures > 0)
parts.push(`${result.summary.failures} Failure(s)`);
effects.push(`${result.summary.failures} Failure(s)`);
if (result.summary.advantages > 0)
parts.push(`${result.summary.advantages} Advantage(s)`);
effects.push(`${result.summary.advantages} Advantage(s)`);
if (result.summary.threats > 0)
parts.push(`${result.summary.threats} Threat(s)`);
effects.push(`${result.summary.threats} Threat(s)`);
if (result.summary.triumphs > 0)
parts.push(`${result.summary.triumphs} Triumph(s)`);
effects.push(`${result.summary.triumphs} Triumph(s)`);
if (result.summary.despair > 0)
parts.push(`${result.summary.despair} Despair(s)`);
if (result.summary.lightSide > 0)
parts.push(`${result.summary.lightSide} Light Side(s)`);
if (result.summary.darkSide > 0)
parts.push(`${result.summary.darkSide} Dark Side(s)`);
effects.push(`${result.summary.despair} Despair(s)`);

return parts.join(", ") || "No effects";
}
const resultText = effects.length > 0 ? effects.join(", ") : "No effects";

if (result.summary.hints && result.summary.hints.length > 0) {
return `${resultText}\n\nPossible actions:\n${result.summary.hints.map((hint) => " • " + hint).join("\n")}`;
}
return resultText;
};

export const main = () => {
const input = process.argv.slice(2).join(" ");
if (!input) {
console.log(`Usage: > swrpg-dice 2y 1g 2p 1r
const args = process.argv.slice(2);
const hintsIndex = args.indexOf("--hints");
const showHints = hintsIndex !== -1;
const diceNotation =
hintsIndex !== -1
? args.filter((_, index) => index !== hintsIndex).join(" ")
: args.join(" ");

if (!diceNotation.trim()) {
console.log(`Usage: swrpg-dice <dice-notation> <dice-options>
Example: swrpg-dice 2y 1g 1p 1b 1sb --hints
Dice Options:
- y/pro = Yellow / Proficiency
- g/a = Green / Ability
- b/boo = Blue / Boost
- r/c = Red / Challenge
- p/diff = Purple / Difficulty
- blk/k/sb/s = Black / Setback
- w/f = White/Force
`);
Options:
--hints Show possible actions based on roll results`);
process.exit(1);
}

const pool = parseDiceNotation(input);
const result = roll(pool);
const pool = parseDiceNotation(diceNotation);
const result = roll(pool, { hints: showHints });
console.log(formatResult(result));
};

Expand Down
41 changes: 34 additions & 7 deletions src/dice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { DicePool, RollResult, DiceResult, DetailedDieResult } from "./types";
import { hintCostDisplayText, hints } from "./hints";
import {
DicePool,
RollResult,
DiceResult,
DetailedDieResult,
RollOptions,
} from "./types";

const rollDie = (sides: number): number =>
Math.floor(Math.random() * sides) + 1;
Expand Down Expand Up @@ -491,7 +498,10 @@ const forceDieResult = (roll: number): DiceResult => {
}
};

const sumResults = (results: DiceResult[]): DiceResult => {
const sumResults = (
results: DiceResult[],
options?: RollOptions,
): DiceResult => {
const sums = results.reduce(
(acc, curr) => ({
successes: acc.successes + curr.successes,
Expand All @@ -515,7 +525,6 @@ const sumResults = (results: DiceResult[]): DiceResult => {
},
);

// Calculate net successes/failures
let netSuccesses = 0;
let netFailures = 0;

Expand All @@ -528,7 +537,7 @@ const sumResults = (results: DiceResult[]): DiceResult => {
netFailures = sums.failures - sums.successes;
}

return {
const result: DiceResult = {
successes: netSuccesses,
failures: netFailures,
advantages: sums.advantages,
Expand All @@ -538,10 +547,11 @@ const sumResults = (results: DiceResult[]): DiceResult => {
lightSide: sums.lightSide,
darkSide: sums.darkSide,
};

return result;
};

export const roll = (pool: DicePool): RollResult => {
// Initialize all dice counts to 0 if undefined
export const roll = (pool: DicePool, options?: RollOptions): RollResult => {
const boostCount = pool.boostDice ?? 0;
const abilityCount = pool.abilityDice ?? 0;
const proficiencyCount = pool.proficiencyDice ?? 0;
Expand Down Expand Up @@ -633,8 +643,25 @@ export const roll = (pool: DicePool): RollResult => {
});
}

const summary = sumResults(detailedResults.map((r) => r.result));

if (options?.hints) {
const applicableHints = hints.filter((hint) => {
const { cost } = hint;
return Object.entries(cost).some(([symbol, required]) => {
const summaryKey = (symbol.toLowerCase() + "s") as keyof typeof summary;
const value = summary[summaryKey];
if (typeof value !== "number") return false;
return required <= value;
});
});
summary.hints = applicableHints.map(
(hint) => `${hintCostDisplayText(hint)} - ${hint.description}`,
);
}

return {
results: detailedResults,
summary: sumResults(detailedResults.map((r) => r.result)),
summary: summary,
};
};
Loading

0 comments on commit 8c822df

Please sign in to comment.