Skip to content

Commit

Permalink
Docs updates.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: a84a8644914ee93a60cc50f2ea130245287bfaba
  • Loading branch information
cpojer committed May 14, 2024
1 parent b0dc628 commit 6b4c883
Show file tree
Hide file tree
Showing 28 changed files with 824 additions and 203 deletions.
53 changes: 27 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[Athena Crisis](https://athenacrisis.com) is an Open Core video game developed by [Nakazawa Tech](https://nkzw.tech) and published by [Null](https://null.com). The source code in this repository is licensed under the [MIT License](./LICENSE.md) and can be used to improve Athena Crisis, build additional tools, study game development with JavaScript or create entirely new turn-based strategy games.

The single-player campaign, multiplayer, art, music, and content are not open source. You can try a demo at [athenacrisis.com](https://athenacrisis.com) and you can [wishlist or purchase Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy Athena Crisis directly](https://app.athenacrisis.com/checkout) to play the full game.
The single-player campaign, multiplayer, art, music, and content are not open source. You can try a demo at [athenacrisis.com](https://athenacrisis.com) and you can [wishlist or purchase Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy Athena Crisis directly](https://app.athenacrisis.com/checkout) to experience the full game.

If you like Athena Crisis, [please consider a sponsorship to support its development](https://github.com/sponsors/cpojer).

Expand Down Expand Up @@ -38,7 +38,11 @@ pnpm dev
Visit [localhost:3003](http://localhost:3003/) to see the docs page.

## Packages
## Documentation & Playground

Check out our [Athena Crisis Open Source Docs & Playground](https://athenacrisis.com/open-source) site.

### Packages

The codebase is split into multiple packages to enforce the separation of concerns. We suggest starting with these packages to get an end-to-end overview:

Expand All @@ -59,17 +63,33 @@ These are secondary packages focused on specific domains:
- `offline` → Offline splash screen for app (_client_).
- `tests` → e2e tests.

## Documentation & Playground
# Contributing

Check out our [Athena Crisis Open Source Docs & Playground](https://athenacrisis.com/open-source) site.
We welcome contributions to Athena Crisis. Some feature development is funded via [Polar](https://polar.sh): [`nkzw-tech/athena-crisis` on Polar](https://polar.sh/nkzw-tech/athena-crisis). Here are some guidelines to get you started:

- The style guide is enforced through tests and linting. Please run `pnpm test` to run all checks. If they pass, you are good to send a Pull Request.
- We suggest adding tests to Pull Requests. You can find many examples in the [`tests` folder](https://github.com/nkzw-tech/athena-crisis/tree/main/tests).
- Check out [The Perfect Development Environment](https://cpojer.net/posts/the-perfect-development-environment) and [Fastest Frontend Tooling](https://cpojer.net/posts/fastest-frontend-tooling-in-2022) for tips on how to optimize your environment setup.
- [Join the #tech channel on Discord](https://discord.gg/2VBCCep7Fk) if you run into issues.

We greatly appreciate contributions in the following areas:

- Bug fixes.
- AI improvements.
- New game features.
- Balancing improvements.
- Experimental technical explorations.
- Tests to cover untested functionality.
- Performance Improvements to core data structures.
- Separation of concerns into smaller libraries that can be published on npm and consumed by other projects.

# Q&A

## What is open source and what isn't?

About 75% of all non-content related Athena Crisis code – **almost 100,000 lines** – is open source, including the core data structures, algorithms, game engine, rendering, AI, and the map editor. Backend implementations such as user management, databases, APIs, realtime spectating, server configuration, and app wrappers for Steam or app stores are not open source. We aim to open source more of the game over time, but the content will remain the intellectual property of Nakazawa Tech KK and therefore not be open source. You can buy and enjoy [Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy it on athenacrisis.com](https://app.athenacrisis.com/checkout).
About 75% of all non-content related Athena Crisis code – **almost 100,000 lines** – is open source, including the core data structures, algorithms, game engine, rendering, AI, and the map editor. Backend implementations such as user management, databases, APIs, realtime spectating, server configuration, and app wrappers for Steam or app stores are not open source. We aim to open source more of the game over time, but the content will remain the intellectual property of Nakazawa Tech KK and therefore not be open source. You can buy and experience [Athena Crisis on Steam Early Access](https://store.steampowered.com/app/2456430/Athena_Crisis/) or [buy it on athenacrisis.com](https://app.athenacrisis.com/checkout).

## Why did you open source Athena Crisis?
## Why is Athena Crisis open source?

[Nakazawa Tech](https://nkzw.tech) is an Open Core company. See [the "Athena Crisis is now Open Source" blog post](https://cpojer.net/posts/athena-crisis-open-source) for more information.

Expand All @@ -85,7 +105,7 @@ To simplify dependency management with [`pnpm`](https://pnpm.io/), most of the i

Why not!? At some point it became necessary to split the codebase into multiple packages to share code between the client and server. The first package was named `athena`, and it was hard to come up with meaningful names for the other packages. We decided to name them after Greek gods because it seemed cute.

Over time, many pieces are expected to be extracted into separate packages and published on npm under the `@nkzw` organization. Please send a Pull Request if you find code that you think should be extracted into a separate package.
Over time, many pieces will be extracted into separate packages and published on npm under the `@nkzw` organization. Please move it to a `packages` folder and send a Pull Request if you find code that should be extracted into a separate package.

## How do assets work in this codebase?

Expand All @@ -97,25 +117,6 @@ Yes, you can. However, any content such as art, music, story, characters and the

If you'd like to use content from Athena Crisis for commercial or non-commercial purposes, you must obtain a license from Nakazawa Tech KK by emailing [email protected].

# Contributing

We welcome contributions to Athena Crisis. Some feature development is funded via [Polar](https://polar.sh): [`nkzw-tech/athena-crisis` on Polar](https://polar.sh/nkzw-tech/athena-crisis). Here are some guidelines to get you started:

- The style guide is enforced through tests and linting. Please run `pnpm test` to run all checks. If they pass, you are good to send a Pull Request.
- Check out [The Perfect Development Environment](https://cpojer.net/posts/the-perfect-development-environment) and [Fastest Frontend Tooling](https://cpojer.net/posts/fastest-frontend-tooling-in-2022) for tips on how to optimize your environment setup.
- We suggest adding tests to Pull Requests. You can find many examples in the [`tests` folder](https://github.com/nkzw-tech/athena-crisis/tree/main/tests).

We greatly appreciate contributions in the following areas:

- Bug fixes.
- AI improvements.
- New game features.
- Balancing improvements.
- Experimental technical explorations.
- Tests to cover untested functionality.
- Performance Improvements to core data structures.
- Separation of concerns into smaller libraries that can be published on npm and consumed by other projects.

# More information

Check out these links to learn more about the tech behind Athena Crisis:
Expand Down
2 changes: 1 addition & 1 deletion dionysus/BaseAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default abstract class BaseAI {
protected executeMove(
map: MapData,
action: MoveAction,
): [MapData | null, boolean] {
): [map: MapData | null, blocked: boolean] {
const currentMap = this.execute(map, action);
const state = this.gameState.at(-1);

Expand Down
5 changes: 5 additions & 0 deletions docs/content/examples/entities-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import UnitPreviews from '@deities/hera/ui/demo/UnitPreviews.tsx';

export default function EntitiesExample() {
return <UnitPreviews />;
}
4 changes: 2 additions & 2 deletions docs/content/examples/map-data-examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Flamethrower, Infantry } from '@deities/athena/info/Unit.tsx';
import withModifiers from '@deities/athena/lib/withModifiers.tsx';
import vec from '@deities/athena/map/vec.tsx';
import MapData from '@deities/athena/MapData.tsx';
import Button from '@deities/ui/Button.tsx';
import InlineLink from '@deities/ui/InlineLink.tsx';
import { Fragment, useState } from 'react';
import PlaygroundGame from '../playground/PlaygroundGame.tsx';

Expand Down Expand Up @@ -41,7 +41,7 @@ export default function ExampleMap() {
return (
<Fragment key={render}>
<PlaygroundGame map={mapB} />
<Button onClick={() => rerender(render + 1)}>Reset</Button>
<InlineLink onClick={() => rerender(render + 1)}>Reset State</InlineLink>
</Fragment>
);
}
22 changes: 12 additions & 10 deletions docs/content/examples/map-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ const viewer = {

export default function MapEditorExample() {
return (
<MapEditor
animationSpeed={null}
confirmActionStyle="touch"
createMap={() => {}}
fogStyle="soft"
setHasChanges={() => {}}
tiltStyle="on"
updateMap={() => {}}
user={viewer}
/>
<div style={{ width: '150%' }}>
<MapEditor
animationSpeed={null}
confirmActionStyle="touch"
createMap={() => {}}
fogStyle="soft"
setHasChanges={() => {}}
tiltStyle="on"
updateMap={() => {}}
user={viewer}
/>
</div>
);
}
26 changes: 26 additions & 0 deletions docs/content/examples/portraits-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { preparePortraits } from '@deities/art/Sprites.tsx';
import {
BazookaBear,
Flamethrower,
Jetpack,
Sniper,
} from '@deities/athena/info/Unit.tsx';
import Portrait from '@deities/hera/character/Portrait.tsx';
import Stack from '@deities/ui/Stack.tsx';

preparePortraits();

// [!region portraits]
const portraits = (
<Stack gap>
<Portrait animate player={1} unit={Sniper} variant={0} />
<Portrait animate player={2} unit={Flamethrower} variant={2} />
<Portrait animate player={6} unit={BazookaBear} variant={0} />
<Portrait animate player={5} unit={Jetpack} variant={2} />
</Stack>
);
// [!endregion portraits]

export default function PortraitsExample() {
return portraits;
}
166 changes: 166 additions & 0 deletions docs/content/pages/core-concepts/actions.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,167 @@
import Image from '../../playground/Image.tsx';

# Actions

In the [Map Data](/core-concepts/map-data) section we learned about the core map data structure of Athena Crisis and how to query and update game state. In this section we'll discuss the formalized approach to update game state via _Actions_.

Actions are the primary way to update game state. While game state might be mutated before a game starts or after it ends via code, players can only interact with the game via actions. Actions can be a move, attack, create unit or other game events. You can find the full list of actions that can be executed by users or scripts in [`Action.tsx`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/Action.tsx). Each Action produces an ActionResponse, which can be found in [`ActionResponse.tsx`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/ActionResponse.tsx). ActionResponses will then be applied to the game state to update it, and animated if they are shown to a player.

**Here is a diagram of the action flow:**

<Image alt="Actions" src="/actions.png" />

## Defining a new Action

Adding new actions is straightforward and TypeScript guides you through the process. First, add a new Action type to `Action.tsx`:

```tsx
type SleepAction = Readonly<{
type: 'Sleep';
from: Vector;
}>;
```

You'll also need to add it to the `Action` type in the same file:

```tsx
export type Action =
| ActivatePowerAction
| ……
| SleepAction;
```

If you are adding a new type of `ActionResponse`, you'll need to do the same in `ActionResponse.tsx`:

```tsx
export type SleepActionResponse = Readonly<{
type: 'Sleep';
from: Vector;
}>;
```

You'll also need to add it to the `Action` type in the same file:

```tsx
export type ActionResponse =
| ActivatePowerActionResponse
| ……
| SleepActionResponse;
```

After this, run `pnpm codegen` to generate all the encoded actions (for storage or network transmission), and the corresponding formatters for use in snapshot tests.

## Implementing an Action

After a new Action is defined, you can run TypeScript via `pnpm tsc` and it will guide you through each call site where the new action needs to be handled. This is a great way to get an overview of the whole system. Let's build a new "Sleep" action that puts a unit to sleep. In our case, it won't have any functionality, but could be made visible to the player with an animation. First, add the new Action in [`Action.tsx`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/Action.tsx):

```tsx
function sleep(map: MapData, { from }: SleepAction) {
const unit = map.units.get(from);
return unit && map.isCurrentPlayer(unit) && !unit.isSleeping()
? ({ from, type: 'Sleep' } as const)
: null;
}
```

Actions only return an `ActionResponse` if the action is valid. They do not mutate the game state, which happens via [`applyActionResponse`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/actions/applyActionResponse.tsx) by processing the `ActionResponse` and returning a new `MapData` object:

```tsx
switch (type) {
case 'Sleep': {
const { from } = actionResponse;
const unit = map.units.get(from);
return unit
? map.copy({ units: map.units.set(from, unit.sleep().complete()) })
: map;
}
}
```

Next, TypeScript will tell us that we need to handle the visibility of the new ActionResponse in fog. Fog in Athena Crisis works by removing all information from each player that is not visible to them. When an Action is executed, it calls [`computeVisibleActions`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/lib/computeVisibleActions.tsx) on the ActionResponse once for each player. The Action we created is fairly minimal, so we only need to handle one case: Show the action if the source field (`from`) is visible to the player, or drop it if it isn't:

```tsx
const VisibleActionModifiers = {
Sleep: { Source: true },
}
```

There are more complex cases where it is harder to know if an action should be visible or hidden from that player, such as when a unit is moving or attacking, and the action affects more than just one field. `computeVisibleActions` can handle each case individually, and it can either return the same ActionResponse, drop it, return a modified version or even return multiple new ActionResponses. For example, a unit can be created from one Building but deployed on another field. The process looks like this:

```tsx
CreateUnit: {
Both: true,
Source: true,
Target: (
{ from, to, unit }: CreateUnitActionResponse,
_: MapData,
activeMap: MapData,
): HiddenMoveActionResponse => ({
path: [from, to],
type: 'HiddenMove',
unit,
}),
}
```

If both fields or just the source are visible, the action is shown to the player unmodified. However, if only the target field is visible, the response is replaced with a `HiddenMove` ActionResponse. The player who is viewing the game won't be able to tell if the unit was just created or moved from another field in fog.

For convenience, we'll also add an [`ActionMutator`](https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/action-mutators/ActionMutators.tsx). These are simple functions to avoid repetition when executing actions against game state, like is often the case in tests.

```
export const SleepAction = (from: Vector) =>
({
from,
type: 'Sleep',
}) as const;
```

Now, if you are writing a test to simulate some game actions, you can use the mutator like this:

```tsx
const response = executeGameActions(map, [
MoveAction(from, to),
SleepAction(to),
EndTurnAction(),
]);
```

TypeScript may point you to a few more utility functions that need handling for your action, but once you are done we can move on to the UI layer.

## Actions in the UI

We are now ready to make our first change to the Athena Crisis game client. Most of the client code can be found in [`hera`](https://github.com/nkzw-tech/athena-crisis/tree/main/hera).

For our new action, we need to first implement the handler for what happens when another player or the AI execute this action. This code lives in [`processActionResponse`](https://github.com/nkzw-tech/athena-crisis/blob/main/hera/action-response/processActionResponse.tsx) which is a wrapper around `applyActionResponse` to animate game state and apply the ActionResponse at the right time. The client side Sleep Action could look something like this:

```tsx
export function sleepAction(
{ optimisticAction, update }: Actions,
state: State,
): Promise<State> {
const { map, selectedPosition, vision } = state;
if (selectedPosition) {
return update({
map: applyActionResponse(
map,
vision,
optimisticAction(state, SleepUnitAction(selectedPosition)),
),
position: selectedPosition,
...resetBehavior(),
});
}
return null;
}
```

Finally, we need to allow the player to execute the Sleep Action for a unit in the game. We could consider adding a button to the [`Menu`](https://github.com/nkzw-tech/athena-crisis/blob/main/hera/behavior/Menu.tsx) behavior that works similarly to other buttons and executes the action against the game state when clicked. We can reuse the same `sleepAction` that we defined above for the user initiated action as well.

After following the above steps, Athena Crisis should now have a new "Sleep" feature for units!

## Optimistic Updates

In the above example we called `optimisticAction(state, SleepUnitAction(from))` to update the game state on the client optimistically. Due to the elegance of immutable data structures and the Athena Crisis architecture, we can apply an Action on the client while sending the same action to the server at the same time, executing it, and sending the (visible) ActionResponse to each other player.

The architecture of the game ensures that the server always has the final say on the game state, and the client will be updated with the server's response. This is a powerful feature that allows the game to feel responsive and smooth, even on slow connections, and it also allows hiding secrets like hidden win conditions from players.
Loading

0 comments on commit 6b4c883

Please sign in to comment.