Skip to content

Latest commit

 

History

History
291 lines (218 loc) · 8.65 KB

development.md

File metadata and controls

291 lines (218 loc) · 8.65 KB

Doing Development on Friendly Feud

Table of Contents

  1. Project Structure
  2. Dependencies
  3. Setup Instructions
  4. Quick Start
  5. Running Development
  6. End-to-End Testing
  7. Frontend Overview
  8. Backend Overview
  9. Troubleshooting
  10. Contributing Guidelines

Project Structure

├── backend/               # Golang backend
│   ├── api/               # Backend API and websocket logic
│   ├── Dockerfile         # Backend Dockerfile
│   ├── main.go            # Entry point for backend server
├── docker/                # Docker and nginx configuration files
│   ├── allinone/          # All-in-one Docker configuration
│   ├── nginx/             # Nginx configuration
│   └── docker-compose*.yaml # Docker compose files
├── doc/                   # Documentation and development guide
├── e2e/                   # End-to-end tests using Playwright
├── games/                 # Pre-built game files in JSON format
├── i18n/                  # Localization and translation files
├── public/                # Static assets (images, fonts, etc.)
├── scripts/               # Utility scripts for game creation
├── Dockerfile             # Frontend dockerfile
├── Dockerfile.allinone    # All-in-one dockerfile
├── src/                   # Next.js frontend
│   ├── components/        # React components
│   ├── lib/               # Utility functions
│   ├── pages/             # Next.js page components

Dependencies

System Requirements

Note: Required versions are not updated, but newest versions should work 😅

Setup Instructions

Windows Setup

For Windows users, we recommend using WSL.

You might need to configure Windows firewall to allow WSL network access:

# Add outbound rules
netsh advfirewall firewall add rule name="WSL2 HTTPS Out" dir=out action=allow protocol=TCP localport=443
netsh advfirewall firewall add rule name="WSL2 HTTP Out" dir=out action=allow protocol=TCP localport=80
# Add inbound rules
netsh advfirewall firewall add rule name="WSL2 HTTPS" dir=in action=allow protocol=TCP localport=443
netsh advfirewall firewall add rule name="WSL2 HTTP" dir=in action=allow protocol=TCP localport=80

Linux Setup

Install dependencies

Quick Start

  1. Install dependencies
  2. Clone the repository
  3. Start development environment:
    make dev
  4. Access the application at localhost

Running development

The stack consists of:

  • frontend: Next.js
  • backend: Golang
  • proxy: Nginx as the entry point

The development environment is managed through a Makefile. Key commands include:

  • make dev: Builds and starts the development stack
  • make dev-background: Same as make dev, but detaches
  • make dev-down: Stops/removes the development stack

Access the application at localhost/

The compose files should automatically be selected by the Makefile, but you can:

End-to-End Testing

make e2e-ui will launch playwright

The e2e tests are located in the e2e folder.

Tests are marked with their *.spec.js file name

Frontend Overview

The frontend is using Next.js as its way to serve pages.

Next.js Project Structure

Code is arranged in the ./src folder with ./src/pages/index.jsx being the entry point

From there you can expect the usual React functionality.

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.

We store a cookie to keep the user's session in the game as they refresh the page.

Working with styles

You can use anything from TailwindCSS as long as the colors you use match the colors found in tailwind.config.js

// ....
success: {
    900: "#14532D",
    700: "#15803D",
    500: "#22C55E",
    300: "#86EFAC",
    200: "#BBF7D0",
},
secondary: {
    900: "#A1A1AA",
    700: "#D4D4D8",
    500: "#E4E4E7",
    300: "#F4F4F5",
    200: "#FAFAFA",
},
// ....

This looks like

<div className="rounded bg-success-200 p-2">{t("Answer")} 1</div>
<div className="rounded bg-primary-200 p-2">{t("points")} 1</div>

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.

Backend Overview

The backend is a Golang application located in ./backend.

The entry point is main.go, where we start our WebSocket server:

http.HandleFunc("/api/ws", func(httpWriter http.ResponseWriter, httpRequest *http.Request) {
    api.ServeWs(httpWriter, httpRequest)
})

We also set up a "store" that backend functions interact with to store game data:

err := api.NewGameStore(cfg.store)

func NewGameStore(gameStore string) error {
	switch gameStore {
	case "memory":
		log.Println("Starting famf with memory store")
		store = NewMemoryStore()
		return nil
	case "sqlite":
		log.Println("Starting famf with sqlite store")
		store, _ = NewSQLiteStore()
	default:
		return fmt.Errorf("unknown store: %q", gameStore)
	}
	return nil
}

The variable store is used by functions to read and write game state:

var store gameStore

This setup creates a connection to the frontend and establishes two goroutines that asynchronously read and write to the client:

go client.writePump()
go client.readPump()

In readPump(), we receive messages and pass them to EventPipe().

EventPipe() is located in backend/api/pipe.go and serves as the next entry point for handling events from the frontend.

We parse messages like these in the parseEvent() function:

{ "action": "buzz", "room": "HL6T", "id": "fds-fds-21-fds-f-321"}
{ "action": "clearbuzzers", "room": "HL6T"}
func parseEvent(message []byte) (*Event, error) {
	var event *Event
	err := json.Unmarshal(message, &event)
	if err != nil {
		return nil, err
	}
	return event, nil
}

If the action exists in the receiveActions map, we call the corresponding function.

Backend functions typically follow this pattern:

  1. Retrieve data from the store for the specified room
  2. Perform actions on the data
  3. Send updated data to the player or the entire room
  4. Write changes back to the store

When you see code like this:

client.send <- message

or

if room.Hub.broadcast != nil {
    room.Hub.broadcast <- message
}
if room.Hub.stop != nil {
    room.Hub.stop <- true
}

We're sending data back to goroutines initialized when the player connects or when a Hub is created for the room.

Writing a new Store

Creating a new game store is straightforward.

The Go interface in backend/api/store.go defines the required functions:

type gameStore interface {}

For a simple example, see the memory store in backend/api/store-memory.go, which uses a map to store game data.

Note: Production deployments currently use the sqlite store.

When you see:

m.mu.RLock()
defer m.mu.RUnlock()

This lock prevents race conditions when accessing memory in asynchronous goroutines.

Troubleshooting

  1. If localhost doesn't work, try using 127.0.0.1 instead. On Windows with WSL, verify with curl localhost
  2. For WebSocket issues:
    • Verify the backend is running
  3. If node_modules aren't updating:
    make dev-down
    make dev

Contributing Guidelines

We welcome contributions! Please follow these guidelines:

  1. Fork the repository and create your branch from master
  2. Follow the existing code style and architecture
  3. Write commit messages using Conventional Commits
  4. Add tests for new features
  5. Update documentation when making changes
  6. Open a pull request with a detailed description