move-by-move 1.0.5 → 2.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.
- package/README.md +123 -0
- package/lib/index.d.ts +3 -4
- package/lib/index.js +4 -4
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# move-by-move
|
|
2
|
+
|
|
3
|
+
A simple, dependency-free game engine for turn-based games.
|
|
4
|
+
|
|
5
|
+
`move-by-move` gives you a tiny state container in the spirit of Redux, but built around **rules** instead of reducers. Every move runs through a pipeline of rules, each of which sees both the action and the current state and can transform either. This makes it natural to express turn-based game logic — validation, the move itself, win checks, turn switching — as a sequence of small, composable steps.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install move-by-move
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import createGame from 'move-by-move'
|
|
17
|
+
|
|
18
|
+
type State = { count: number }
|
|
19
|
+
|
|
20
|
+
const game = createGame<State>(
|
|
21
|
+
[
|
|
22
|
+
// each rule receives [action, state] and returns [action, state]
|
|
23
|
+
([action, state]) => {
|
|
24
|
+
if (action.type === 'INCREMENT') {
|
|
25
|
+
return [action, { count: state.count + action.by }]
|
|
26
|
+
}
|
|
27
|
+
return [action, state]
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
{ count: 0 }, // initial state
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const unsubscribe = game.subscribe(() => console.log('state:', game.getState()))
|
|
34
|
+
|
|
35
|
+
game.move({ type: 'INCREMENT', by: 1 }) // logs: state: { count: 1 }
|
|
36
|
+
game.move({ type: 'INCREMENT', by: 5 }) // logs: state: { count: 6 }
|
|
37
|
+
|
|
38
|
+
unsubscribe() // stop listening
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Core concept: rules
|
|
42
|
+
|
|
43
|
+
A **rule** is a function over a `[action, state]` tuple:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
type RuleIO<State> = [action: GameAction, state: State]
|
|
47
|
+
type Rule<State> = (input: RuleIO<State>) => RuleIO<State>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When you call `game.move(action)`, the engine pipes `[action, state]` through every rule **in order** and uses the resulting state as the new state:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
[action, state] → rule1 → rule2 → rule3 → [action, newState]
|
|
54
|
+
└── becomes the new state
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Because each rule receives both the action and the (possibly already-transformed) state, rules can:
|
|
58
|
+
|
|
59
|
+
- **transform state** based on the action,
|
|
60
|
+
- **ignore** actions they don't care about (just return the input unchanged),
|
|
61
|
+
- **rewrite the action** for downstream rules,
|
|
62
|
+
- **run unconditionally**, e.g. a rule that switches the active player or checks for a winner after every move.
|
|
63
|
+
|
|
64
|
+
Order matters — a validation rule should run before the rule that applies the move, and a "check for win" rule should run after it.
|
|
65
|
+
|
|
66
|
+
### Example: composing rules
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import createGame from 'move-by-move'
|
|
70
|
+
|
|
71
|
+
type State = {
|
|
72
|
+
board: (string | null)[]
|
|
73
|
+
turn: 'X' | 'O'
|
|
74
|
+
winner: string | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const placeMark = ([action, state]: RuleIO<State>): RuleIO<State> => {
|
|
78
|
+
if (action.type !== 'PLACE' || state.winner) return [action, state]
|
|
79
|
+
if (state.board[action.index] !== null) return [action, state] // square taken
|
|
80
|
+
const board = [...state.board]
|
|
81
|
+
board[action.index] = state.turn
|
|
82
|
+
return [action, { ...state, board }]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const switchTurn = ([action, state]: RuleIO<State>): RuleIO<State> => {
|
|
86
|
+
if (action.type !== 'PLACE' || state.winner) return [action, state]
|
|
87
|
+
return [action, { ...state, turn: state.turn === 'X' ? 'O' : 'X' }]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const game = createGame<State>(
|
|
91
|
+
[placeMark, switchTurn],
|
|
92
|
+
{ board: Array(9).fill(null), turn: 'X', winner: null },
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### `createGame<State>(rules, initialState): Game<State>`
|
|
99
|
+
|
|
100
|
+
Creates a new game instance.
|
|
101
|
+
|
|
102
|
+
- `rules` — an array of rule functions, applied in order on every move.
|
|
103
|
+
- `initialState` — the starting state.
|
|
104
|
+
|
|
105
|
+
### `Game<State>`
|
|
106
|
+
|
|
107
|
+
| Method | Description |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `getState(): State` | Returns the current state. |
|
|
110
|
+
| `move(action: GameAction): void` | Runs `action` through the rule pipeline, updates state, and notifies subscribers. |
|
|
111
|
+
| `subscribe(listener: Function): () => void` | Registers a listener called (with no arguments) after every move. Use `getState()` inside it to read the new state. Returns an unsubscribe function — call it to remove the listener. |
|
|
112
|
+
|
|
113
|
+
### `GameAction<Payload>`
|
|
114
|
+
|
|
115
|
+
An action is any payload object with a required `type` string:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
type GameAction<Payload = any> = Payload & { type: string }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT © Andreas Riedmüller
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare type GameAction<Payload = any> = Payload & {
|
|
2
2
|
type: string;
|
|
3
|
-
}
|
|
3
|
+
};
|
|
4
4
|
export interface Game<Type> {
|
|
5
|
-
subscribe(listener: Function): void;
|
|
6
|
-
unsubscribe(listener: Function): void;
|
|
5
|
+
subscribe(listener: Function): () => void;
|
|
7
6
|
getState(): Type;
|
|
8
7
|
move(action: GameAction): void;
|
|
9
8
|
}
|
package/lib/index.js
CHANGED
|
@@ -4,12 +4,13 @@ const helpers_1 = require("./helpers");
|
|
|
4
4
|
function createGame(rules, initialGameState) {
|
|
5
5
|
let listeners = [];
|
|
6
6
|
let state = initialGameState;
|
|
7
|
-
const subscribe = (listener) => {
|
|
8
|
-
listeners = [...listeners, listener];
|
|
9
|
-
};
|
|
10
7
|
const unsubscribe = (listener) => {
|
|
11
8
|
listeners = listeners.filter(item => item !== listener);
|
|
12
9
|
};
|
|
10
|
+
const subscribe = (listener) => {
|
|
11
|
+
listeners = [...listeners, listener];
|
|
12
|
+
return () => unsubscribe(listener);
|
|
13
|
+
};
|
|
13
14
|
const getState = () => {
|
|
14
15
|
return state;
|
|
15
16
|
};
|
|
@@ -22,7 +23,6 @@ function createGame(rules, initialGameState) {
|
|
|
22
23
|
};
|
|
23
24
|
return {
|
|
24
25
|
subscribe,
|
|
25
|
-
unsubscribe,
|
|
26
26
|
getState,
|
|
27
27
|
move,
|
|
28
28
|
};
|