Repository: boardgameio/boardgame.io (ts)

Issue:
Feature Request - Allow to specify next phase using a function
**Problem:** Currently the `next` field in the phase configuration only accepts a string.

 I'm trying to write a phase using the `TurnOrder.ONCE` turn order, however the next phase needs to be determined dynamically. The only way to do this as far as I know is to specify an `endIf` function, but this clashes with automatic phase-ending `TurnOrder.ONCE`. I could write such a function, but especially since `endIf` is called before the phase is even begun, in my case the logic to accomplish this gets unnecesarily convoluted. It would be convenient if the `next` field accepted a function to allow to determine the next phase dynamically, without interfering with phase-ending logic.

Pull request description:
feat(phases): Support setting next phase with a function
Closes #954 

I don't typically write Typescript nor have I contributed to this project in many moons so if I've missed something please let me know!

#### Checklist

- [x] Use a separate branch in your local repo (not `main`).
- [x] Test coverage is 100% (or you have a story for why it's ok).


Patch:
diff --git a/src/core/flow.ts b/src/core/flow.ts
index a3e3333..f06d5b8 100644
--- a/src/core/flow.ts
+++ b/src/core/flow.ts
@@ -163,6 +163,12 @@ export function Flow({
       onEnd: HookWrapper(phaseConfig.turn.onEnd),
       endIf: TriggerWrapper(phaseConfig.turn.endIf),
     };
+
+    if (typeof phaseConfig.next !== 'function') {
+      const { next } = phaseConfig;
+      phaseConfig.next = () => next || null;
+    }
+    phaseConfig.wrapped.next = HookWrapper(phaseConfig.next);
   }
 
   function GetPhase(ctx: { phase: string }): PhaseConfig {
@@ -314,10 +320,8 @@ export function Flow({
         logging.error('invalid phase: ' + arg.next);
         return state;
       }
-    } else if (phaseConfig.next !== undefined) {
-      ctx = { ...ctx, phase: phaseConfig.next };
     } else {
-      ctx = { ...ctx, phase: null };
+      ctx = { ...ctx, phase: phaseConfig.wrapped.next(state) || null };
     }
 
     state = { ...state, ctx };
diff --git a/src/types.ts b/src/types.ts
index 2b7db8e..fdf4dbc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -207,7 +207,7 @@ export interface PhaseConfig<
   CtxWithPlugins extends Ctx = Ctx
 > {
   start?: boolean;
-  next?: string;
+  next?: ((G: G, ctx: CtxWithPlugins) => string | void) | string;
   onBegin?: (G: G, ctx: CtxWithPlugins) => any;
   onEnd?: (G: G, ctx: CtxWithPlugins) => any;
   endIf?: (G: G, ctx: CtxWithPlugins) => boolean | void | { next: string };
@@ -219,6 +219,7 @@ export interface PhaseConfig<
     ) => boolean | void | { next: string };
     onBegin?: (state: State<G, CtxWithPlugins>) => any;
     onEnd?: (state: State<G, CtxWithPlugins>) => any;
+    next?: (state: State<G, CtxWithPlugins>) => string | void;
   };
 }
 


Test patch:
diff --git a/src/core/flow.test.ts b/src/core/flow.test.ts
index d5a1e42..2eb8b7f 100644
--- a/src/core/flow.test.ts
+++ b/src/core/flow.test.ts
@@ -730,6 +730,21 @@ test('init', () => {
   expect(state.G).toMatchObject({ done: true });
 });
 
+test('next', () => {
+  const flow = Flow({
+    phases: {
+      A: { start: true, next: () => 'C' },
+      B: {},
+      C: {},
+    },
+  });
+
+  let state = { ctx: flow.ctx(3) } as State;
+  state = flow.processEvent(state, gameEvent('endPhase'));
+
+  expect(state.ctx.phase).toEqual('C');
+});
+
 describe('endIf', () => {
   test('basic', () => {
     const flow = Flow({ endIf: (G) => G.win });