|
| 1 | +# Doing Development on Friendly Feud |
| 2 | + |
| 3 | +## Table of Contents |
| 4 | +1. [Project Structure](#project-structure) |
| 5 | +2. [Dependencies](#dependencies) |
| 6 | +3. [Setup Instructions](#setup-instructions) |
| 7 | +4. [Quick Start](#quick-start) |
| 8 | +5. [Running Development](#running-development) |
| 9 | +6. [End-to-End Testing](#end-to-end-testing) |
| 10 | +7. [Frontend Overview](#frontend-overview) |
| 11 | +8. [Backend Overview](#backend-overview) |
| 12 | +9. [Troubleshooting](#troubleshooting) |
| 13 | +10. [Contributing Guidelines](#contributing-guidelines) |
| 14 | + |
| 15 | +## Project Structure |
| 16 | +```plaintext |
| 17 | +├── backend/ # Golang backend |
| 18 | +│ ├── api/ # Backend API and websocket logic |
| 19 | +│ ├── Dockerfile # Backend Dockerfile |
| 20 | +│ ├── main.go # Entry point for backend server |
| 21 | +├── docker/ # Docker and nginx configuration files |
| 22 | +│ ├── allinone/ # All-in-one Docker configuration |
| 23 | +│ ├── nginx/ # Nginx configuration |
| 24 | +│ └── docker-compose*.yaml # Docker compose files |
| 25 | +├── doc/ # Documentation and development guide |
| 26 | +├── e2e/ # End-to-end tests using Playwright |
| 27 | +├── games/ # Pre-built game files in JSON format |
| 28 | +├── i18n/ # Localization and translation files |
| 29 | +├── public/ # Static assets (images, fonts, etc.) |
| 30 | +├── scripts/ # Utility scripts for game creation |
| 31 | +├── Dockerfile # Frontend dockerfile |
| 32 | +├── Dockerfile.allinone # All-in-one dockerfile |
| 33 | +├── src/ # Next.js frontend |
| 34 | +│ ├── components/ # React components |
| 35 | +│ ├── lib/ # Utility functions |
| 36 | +│ ├── pages/ # Next.js page components |
| 37 | +``` |
| 38 | + |
| 39 | + |
| 40 | +## Dependencies |
| 41 | + |
| 42 | +### System Requirements |
| 43 | + |
| 44 | +- [Docker](https://docs.docker.com/engine/install/) |
| 45 | +- [Docker Compose](https://docs.docker.com/compose/install/) |
| 46 | +- [Node.js](https://nodejs.org/) (version specified in `.nvmrc`) |
| 47 | +- [Go](https://go.dev/doc/install) |
| 48 | +- [Make](https://www.gnu.org/software/make/) |
| 49 | + |
| 50 | +> Note: Required versions are not updated, but newest versions should work 😅 |
| 51 | +
|
| 52 | +## Setup Instructions |
| 53 | + |
| 54 | +### Windows Setup |
| 55 | + |
| 56 | +For Windows users, we recommend using WSL. |
| 57 | + |
| 58 | +You might need to configure Windows firewall to allow WSL network access: |
| 59 | +```powershell |
| 60 | +# Add outbound rules |
| 61 | +netsh advfirewall firewall add rule name="WSL2 HTTPS Out" dir=out action=allow protocol=TCP localport=443 |
| 62 | +netsh advfirewall firewall add rule name="WSL2 HTTP Out" dir=out action=allow protocol=TCP localport=80 |
| 63 | +# Add inbound rules |
| 64 | +netsh advfirewall firewall add rule name="WSL2 HTTPS" dir=in action=allow protocol=TCP localport=443 |
| 65 | +netsh advfirewall firewall add rule name="WSL2 HTTP" dir=in action=allow protocol=TCP localport=80 |
| 66 | +``` |
| 67 | + |
| 68 | +### Linux Setup |
| 69 | + |
| 70 | +Install dependencies |
| 71 | + |
| 72 | +## Quick Start |
| 73 | +1. Install dependencies |
| 74 | +2. Clone the repository |
| 75 | +3. Start development environment: |
| 76 | + ```bash |
| 77 | + make dev |
| 78 | + ``` |
| 79 | +4. Access the application at [localhost](https://localhost/) |
| 80 | + |
| 81 | +## Running development |
| 82 | +The stack consists of: |
| 83 | + |
| 84 | +- `frontend`: Next.js |
| 85 | +- `backend`: Golang |
| 86 | +- `proxy`: Nginx as the entry point |
| 87 | + |
| 88 | +The development environment is managed through a Makefile. Key commands include: |
| 89 | + |
| 90 | +- `make dev`: Builds and starts the development stack |
| 91 | +- `make dev-background`: Same as `make dev`, but detaches |
| 92 | +- `make dev-down`: Stops/removes the development stack |
| 93 | + |
| 94 | +Access the application at [localhost/](https://localhost/) |
| 95 | + |
| 96 | +The compose files should automatically be selected by the Makefile, but you can: |
| 97 | +- check out the [Linux version](../docker/docker-compose.yaml) if on Linux or Macos |
| 98 | +- check out the [WSL version](../docker/docker-compose-dev-wsl.yaml) if on Windows |
| 99 | + |
| 100 | +## End-to-End Testing |
| 101 | + |
| 102 | +`make e2e-ui` will launch [playwright](https://playwright.dev/) |
| 103 | + |
| 104 | +The e2e tests are located in the [e2e](../e2e/) folder. |
| 105 | + |
| 106 | +Tests are marked with their `*.spec.js` file name |
| 107 | + |
| 108 | +## Frontend Overview |
| 109 | +The frontend is using `Next.js` as its way to serve pages. |
| 110 | + |
| 111 | +[Next.js Project Structure](https://nextjs.org/docs/app/getting-started/project-structure) |
| 112 | + |
| 113 | +Code is arranged in the [./src](../src/) folder with [./src/pages/index.jsx](../src/pages/index.jsx) being the entry point |
| 114 | + |
| 115 | +From there you can expect the usual React functionality. |
| 116 | + |
| 117 | +The `frontend` connects back to the `backend` via the `nginx` proxy to setup a WebSocket connection that will control its behavior when data comes in. |
| 118 | + |
| 119 | +We store a cookie to keep the user's session in the game as they refresh the page. |
| 120 | + |
| 121 | +### Working with styles |
| 122 | + |
| 123 | +You can use anything from [TailwindCSS](https://tailwindcss.com/) as long as the colors you use match the colors found in [tailwind.config.js](../tailwind.config.js) |
| 124 | + |
| 125 | +```js |
| 126 | +// .... |
| 127 | +success: { |
| 128 | + 900: "#14532D", |
| 129 | + 700: "#15803D", |
| 130 | + 500: "#22C55E", |
| 131 | + 300: "#86EFAC", |
| 132 | + 200: "#BBF7D0", |
| 133 | +}, |
| 134 | +secondary: { |
| 135 | + 900: "#A1A1AA", |
| 136 | + 700: "#D4D4D8", |
| 137 | + 500: "#E4E4E7", |
| 138 | + 300: "#F4F4F5", |
| 139 | + 200: "#FAFAFA", |
| 140 | +}, |
| 141 | +// .... |
| 142 | +``` |
| 143 | + |
| 144 | +This looks like |
| 145 | + |
| 146 | +```html |
| 147 | +<div className="rounded bg-success-200 p-2">{t("Answer")} 1</div> |
| 148 | +<div className="rounded bg-primary-200 p-2">{t("points")} 1</div> |
| 149 | +``` |
| 150 | + |
| 151 | +What this does is setup a "Theme" we use for the theme picker for the game, so make sure you use the colors named in that configuration file. |
| 152 | + |
| 153 | +## Backend Overview |
| 154 | + |
| 155 | +The backend is a `Golang` application located in [./backend](../backend/). |
| 156 | + |
| 157 | +The entry point is `main.go`, where we start our WebSocket server: |
| 158 | + |
| 159 | +```go |
| 160 | +http.HandleFunc("/api/ws", func(httpWriter http.ResponseWriter, httpRequest *http.Request) { |
| 161 | + api.ServeWs(httpWriter, httpRequest) |
| 162 | +}) |
| 163 | +``` |
| 164 | + |
| 165 | +We also set up a "store" that backend functions interact with to store game data: |
| 166 | + |
| 167 | +```go |
| 168 | +err := api.NewGameStore(cfg.store) |
| 169 | + |
| 170 | +func NewGameStore(gameStore string) error { |
| 171 | + switch gameStore { |
| 172 | + case "memory": |
| 173 | + log.Println("Starting famf with memory store") |
| 174 | + store = NewMemoryStore() |
| 175 | + return nil |
| 176 | + case "sqlite": |
| 177 | + log.Println("Starting famf with sqlite store") |
| 178 | + store, _ = NewSQLiteStore() |
| 179 | + default: |
| 180 | + return fmt.Errorf("unknown store: %q", gameStore) |
| 181 | + } |
| 182 | + return nil |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +The variable `store` is used by functions to read and write game state: |
| 187 | + |
| 188 | +```go |
| 189 | +var store gameStore |
| 190 | +``` |
| 191 | + |
| 192 | +This setup creates a connection to the frontend and establishes two [goroutines](https://go.dev/tour/concurrency/1) that asynchronously read and write to the client: |
| 193 | + |
| 194 | +```go |
| 195 | +go client.writePump() |
| 196 | +go client.readPump() |
| 197 | +``` |
| 198 | + |
| 199 | +In `readPump()`, we receive messages and pass them to `EventPipe()`. |
| 200 | + |
| 201 | +`EventPipe()` is located in [backend/api/pipe.go](../backend/api/pipe.go) and serves as the next entry point for handling events from the frontend. |
| 202 | + |
| 203 | +We parse messages like these in the `parseEvent()` function: |
| 204 | + |
| 205 | +```json |
| 206 | +{ "action": "buzz", "room": "HL6T", "id": "fds-fds-21-fds-f-321"} |
| 207 | +{ "action": "clearbuzzers", "room": "HL6T"} |
| 208 | +``` |
| 209 | + |
| 210 | +```go |
| 211 | +func parseEvent(message []byte) (*Event, error) { |
| 212 | + var event *Event |
| 213 | + err := json.Unmarshal(message, &event) |
| 214 | + if err != nil { |
| 215 | + return nil, err |
| 216 | + } |
| 217 | + return event, nil |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +If the action exists in the `receiveActions` map, we call the corresponding function. |
| 222 | + |
| 223 | +Backend functions typically follow this pattern: |
| 224 | + |
| 225 | +1. Retrieve data from the store for the specified room |
| 226 | +2. Perform actions on the data |
| 227 | +3. Send updated data to the player or the entire room |
| 228 | +4. Write changes back to the store |
| 229 | + |
| 230 | +When you see code like this: |
| 231 | + |
| 232 | +```go |
| 233 | +client.send <- message |
| 234 | +``` |
| 235 | + |
| 236 | +or |
| 237 | + |
| 238 | +```go |
| 239 | +if room.Hub.broadcast != nil { |
| 240 | + room.Hub.broadcast <- message |
| 241 | +} |
| 242 | +if room.Hub.stop != nil { |
| 243 | + room.Hub.stop <- true |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +We're sending data back to goroutines initialized when the player connects or when a [Hub](../backend/api/hub.go) is created for the room. |
| 248 | + |
| 249 | +### Writing a new Store |
| 250 | + |
| 251 | +Creating a new game store is straightforward. |
| 252 | + |
| 253 | +The Go interface in [backend/api/store.go](../backend/api/store.go) defines the required functions: |
| 254 | + |
| 255 | +```go |
| 256 | +type gameStore interface {} |
| 257 | +``` |
| 258 | + |
| 259 | +For a simple example, see the memory store in [backend/api/store-memory.go](../backend/api/store-memory.go), which uses a `map` to store game data. |
| 260 | + |
| 261 | +> Note: Production deployments currently use the `sqlite` store. |
| 262 | +
|
| 263 | +When you see: |
| 264 | + |
| 265 | +```go |
| 266 | +m.mu.RLock() |
| 267 | +defer m.mu.RUnlock() |
| 268 | +``` |
| 269 | + |
| 270 | +This lock prevents race conditions when accessing memory in asynchronous goroutines. |
| 271 | + |
| 272 | + |
| 273 | +## Troubleshooting |
| 274 | + |
| 275 | +1. If localhost doesn't work, try using `127.0.0.1` instead. On Windows with WSL, verify with `curl localhost` |
| 276 | +2. For WebSocket issues: |
| 277 | + - Verify the backend is running |
| 278 | +3. If node_modules aren't updating: |
| 279 | + ```sh |
| 280 | + make dev-down |
| 281 | + make dev |
| 282 | + ``` |
| 283 | + |
| 284 | +## Contributing Guidelines |
| 285 | +We welcome contributions! Please follow these guidelines: |
| 286 | +1. Fork the repository and create your branch from `master` |
| 287 | +2. Follow the existing code style and architecture |
| 288 | +3. Write commit messages using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) |
| 289 | +4. Add tests for new features |
| 290 | +5. Update documentation when making changes |
| 291 | +6. Open a pull request with a detailed description |
0 commit comments