<file_sep>import {Graph} from '@src/app/models/graph';
import {Square} from '@src/app/models/square';
import {Player} from '@src/app/models/player';
import {Edge} from '@src/app/models/edge';

export interface Game {
  id: string;
  graph: Graph;
  squares: Square[];
  players: Player[];
  currentPlayerIdx: number;
  moves: Edge[] | undefined;
}
<file_sep>import {GameComponent} from '@src/app/components/game/game.component';

describe('GameComponent', () => {
  // let component: GameComponent;
  // let fixture: ComponentFixture<GameComponent>;
  // const gameService = jasmine.createSpyObj('GameService', ['getGame', 'setGame']);
  // const authService = jasmine.createSpyObj('AuthService', ['getCurrentUser', 'googleSignin', 'signOut']);
  //
  // beforeEach(async(() => {
  //   TestBed.configureTestingModule({
  //     declarations: [GameComponent],
  //     providers: [
  //       {provide: GameService, useValue: gameService},
  //       {provide: AuthService, useValue: authService},
  //     ],
  //   })
  //       .compileComponents();
  // }));
  //
  // beforeEach(async () => {
  //   const graph = Graph.initialize(5, 5);
  //   player1 = Player.create(0, 'Alice', '<PASSWORD>', 0);
  //   player2 = Player.create(1, 'Bob', '#F08080', 0);
  //   gameService.getGame.and.returnValue(from(Promise.resolve(
  //       <Game>{
  //         id: '1234',
  //         graph: graph,
  //         squares: Square.fromGraph(graph),
  //         players: [
  //           player1,
  //           player2,
  //         ],
  //         currentPlayerIdx: 0,
  //       },
  //   )));
  //   authService.getCurrentUser.and.returnValue(from(Promise.resolve(
  //       <User>{
  //         uid: '1',
  //         email: '<EMAIL>',
  //       },
  //   )));
  //   fixture = TestBed.createComponent(GameComponent);
  //   component = fixture.componentInstance;
  //   await component.onlineGame$.toPromise();
  //   fixture.detectChanges();
  // });
  //
  // it('should render ownerless edges grey', () => {
  //   const edge = fixture.debugElement.query(By.css('#edge-0-0-1-1'));
  //   expect(edge.attributes['stroke']).toEqual('#ccc');
  // });
  //
  // it('should change the owner of an edge on click and move to the next player', () => {
  //   const currentPlayer = component.currentPlayer(component.game$.value);
  //   const edge = fixture.debugElement.query(By.css('#edge-0-0-1-1'));
  //   edge.triggerEventHandler('click', null);
  //   expect(component.game$.value.graph.edges.find(e => e.owner.id === currentPlayer.id))
  //       .toBeTruthy();
  //   expect(component.currentPlayer(component.game$.value) === currentPlayer).toEqual(false);
  // });
  //
  // it('should render player edges with the respective color', () => {
  //   component.game$.value.graph.edges[0].owner = player1;
  //   fixture.detectChanges();
  //
  //   const edge = fixture.debugElement.query(By.css('#edge-0-0-1-1'));
  //   expect(edge.attributes['stroke']).toEqual(player1.color);
  // });
  //
  // it('should render ownerless squares with the respective color', () => {
  //   const edge = fixture.debugElement.query(By.css('#square-0-0'));
  //   expect(edge.attributes['fill']).toEqual('#ccc');
  // });
  //
  // it('should render player squares with the respective color', () => {
  //   component.game$.value.squares[0].owner = player1;
  //   component.game$.next(component.game$.value);
  //   fixture.detectChanges();
  //   const square = fixture.debugElement.query(By.css('#square-0-0'));
  //   expect(square.attributes['fill']).toEqual(player1.color);
  // });
  //
  // it('should render the score', () => {
  //   component.game$.value.players[0].score = 5;
  //   component.game$.value.players[1].score = 3;
  //   component.game$.next(component.game$.value);
  //   fixture.detectChanges();
  //   const score1 = fixture.debugElement.query(By.css('#score-0'));
  //   const score2 = fixture.debugElement.query(By.css('#score-1'));
  //   expect(score1.nativeElement.textContent.trim()).toEqual('Alice: 5');
  //   expect(score2.nativeElement.textContent.trim()).toEqual('Bob: 3');
  // });
  //
  // it('should behave correctly after closing a section', () => {
  //   drawEdge(0, 0, 1, 0);
  //   drawEdge(1, 0, 1, 1);
  //   drawEdge(0, 1, 1, 1);
  //   drawEdge(0, 0, 0, 1);
  //   expect(component.game$.value.squares[0].owner.id).toEqual(player2.id);
  //   expect(component.game$.value.players[1].score).toEqual(1);
  // });
  //
  // it('should not change the owner squares that already have an owner', () => {
  //   // TODO but needs multiplayer capabilities
  // });
  //
  // function drawEdge(x1: number, y1: number, x2: number, y2: number) {
  //   component.drawEdge(
  //       component.game$.value.graph.edges.find(e =>
  //           e.source.x === x1 && e.source.y === y1 && e.target.x === x2 && e.target.y === y2,
  //       ),
  //       component.game$.value,
  //   );
  // }
});
<file_sep>import {Component, OnDestroy, OnInit} from '@angular/core';
import {Graph} from '@src/app/models/graph';
import {Edge} from '@src/app/models/edge';
import {Square} from '@src/app/models/square';
import {Polygon} from '@src/app/models/polygon';
import {GameService} from '@src/app/services/game.service';
import {AuthService} from '@src/app/services/auth.service';
import {map, switchMap} from 'rxjs/operators';
import {Game} from '@src/app/models/game';
import {UserStateType} from '@src/app/models/user-state';
import {ActivatedRoute} from '@angular/router';
import {EdgeStyle} from '@src/app/models/edge-style';

@Component({
  selector: 'pnp-game',
  templateUrl: './game.component.html',
  styleUrls: ['./game.component.css'],
})
export class GameComponent implements OnInit, OnDestroy {

  constructor(
      public gameService: GameService,
      public auth: AuthService,
      private route: ActivatedRoute,
  ) {
  }

  game$ = this.route.paramMap.pipe(
      switchMap(params => this.gameService.getGame(params.get('id'))),
  );
  svgScalingFactor = 25;
  viewBox$ = this.game$.pipe(
      map(game => ({
            width: game.graph.xSize * this.svgScalingFactor,
            height: game.graph.ySize * this.svgScalingFactor,
          }),
      ),
  );
  defaultEdgeStroke = '#ccc';
  defaultEdgeOpacity = 1;
  edgeStrokes$ = this.game$.pipe(
      map(game => game.graph.edges.map(() => (<EdgeStyle>{
        stroke: this.defaultEdgeStroke,
        opacity: this.defaultEdgeOpacity,
      }))),
  );

  userState = UserStateType;

  currentPlayer(game: Game) {
    return game.players[game.currentPlayerIdx];
  }

  scalePosition(pos: number): number {
    return pos * this.svgScalingFactor + (this.svgScalingFactor / 2);
  }

  drawEdge(edge: Edge, game: Game) {
    if (!Edge.isOwned(edge)) {
      const player = this.currentPlayer(game);
      const path = Graph.findPath(game.graph, edge.source, edge.target);
      if (path.length > 0) {
        const polygon = Polygon.fromPath(path);
        const wonSquares = game.squares.filter(square => polygon.contains(square) && !Square.isOwned(square));
        wonSquares.forEach(square => square.owner = player);
        player.score += wonSquares.length;
      } else {
        this.nextPlayer(game);
      }
      if (Array.isArray(game.moves)) {
        game.moves.push(edge);
      }
      edge.owner = player;
      this.gameService.setGame(game);
    }
  }

  nextPlayer(game: Game) {
    if (game.currentPlayerIdx === game.players.length - 1) {
      game.currentPlayerIdx = 0;
    } else {
      game.currentPlayerIdx++;
    }
  }

  wasRecentlyDrawn(edge: Edge, game: Game): boolean {
    if (Array.isArray(game.moves) && game.moves.length > 0) {
      const lastMove = game.moves[game.moves.length - 1];
      return Edge.hasSameCoordinates(edge, lastMove);
    } else {
      return false;
    }
  }

  ngOnInit() {
  }

  ngOnDestroy() {
  }
}
<file_sep>import {Graph} from '@src/app/models/graph';
import {Square} from '@src/app/models/square';
import {Vertex} from '@src/app/models/vertex';

describe('Square', () => {
  describe('fromGraph', () => {
    it('should initialize from an empty graph', () => {
      const graph = Graph.initialize(0, 0);
      const squares = Square.fromGraph(graph);
      expect(squares).toEqual([]);
    });

    it('should initialize from a single vertex graph', () => {
      const graph = Graph.initialize(1, 1);
      const squares = Square.fromGraph(graph);
      expect(squares).toEqual([]);
    });

    it('should initialize from a single square graph', () => {
      const graph = Graph.initialize(2, 2);
      const squares = Square.fromGraph(graph);
      expect(squares).toEqual([
        Square.create(
            Vertex.create(0, 0, 0),
            Vertex.create(1, 1, 0),
            Vertex.create(2, 0, 1),
            Vertex.create(3, 1, 1),
            null,
        ),
      ]);
    });

    it('should initialize from a complex graph', () => {
      const graph = Graph.initialize(4, 3);
      const squares = Square.fromGraph(graph);
      expect(squares).toEqual(jasmine.arrayContaining([
        Square.create(
            Vertex.create(0, 0, 0),
            Vertex.create(1, 1, 0),
            Vertex.create(4, 0, 1),
            Vertex.create(5, 1, 1),
            null,
        ),
        Square.create(
            Vertex.create(1, 1, 0),
            Vertex.create(2, 2, 0),
            Vertex.create(5, 1, 1),
            Vertex.create(6, 2, 1),
            null,
        ),
        Square.create(
            Vertex.create(2, 2, 0),
            Vertex.create(3, 3, 0),
            Vertex.create(6, 2, 1),
            Vertex.create(7, 3, 1),
            null,
        ),
        Square.create(
            Vertex.create(4, 0, 1),
            Vertex.create(5, 1, 1),
            Vertex.create(8, 0, 2),
            Vertex.create(9, 1, 2),
            null,
        ),
        Square.create(
            Vertex.create(5, 1, 1),
            Vertex.create(6, 2, 1),
            Vertex.create(9, 1, 2),
            Vertex.create(10, 2, 2),
            null,
        ),
        Square.create(
            Vertex.create(6, 2, 1),
            Vertex.create(7, 3, 1),
            Vertex.create(10, 2, 2),
            Vertex.create(11, 3, 2),
            null,
        ),
      ]));
    });
  });
});
<file_sep>export interface Vertex {
  id: number;
  x: number;
  y: number;
}

export const Vertex = {
  create: (id: number, x: number, y: number) => <Vertex>{
    id: id,
    x: x,
    y: y,
  },
};
<file_sep>export interface Player {
  id: number;
  name: string;
  color: string;
  score: number;
}

export const Player = {
  create: (id: number, name: string, color: string, score: number) => <Player>{
    id: id,
    name: name,
    color: color,
    score: score,
  },
};
<file_sep>import {Player} from '@src/app/models/player';
import {Graph} from '@src/app/models/graph';
import {Vertex} from '@src/app/models/vertex';

export interface Square {
  topLeft: Vertex;
  topRight: Vertex;
  bottomLeft: Vertex;
  bottomRight: Vertex;
  owner: Player | null;
}

export const Square = {
  create: (topLeft: Vertex, topRight: Vertex, bottomLeft: Vertex, bottomRight: Vertex, owner: Player | null) => <Square>{
    topLeft: topLeft,
    topRight: topRight,
    bottomLeft: bottomLeft,
    bottomRight: bottomRight,
    owner: owner,
  },

  fromGraph: (graph: Graph) => {
    if (graph.xSize <= 1 || graph.ySize <= 1) {
      return [];
    }
    const squares: Square[] = [];
    for (let y = 0; y < graph.ySize - 1; y++) {
      for (let x = 0; x < graph.xSize - 1; x++) {
        const i = x + y * graph.xSize;
        squares.push(<Square>{
          topLeft: graph.vertices[i],
          topRight: graph.vertices[i + 1],
          bottomLeft: graph.vertices[i + graph.xSize],
          bottomRight: graph.vertices[i + graph.xSize + 1],
          owner: null,
        });
      }
    }
    return squares;
  },

  isOwned: (square: Square) => square.owner !== null,
};
<file_sep>import {User} from '@src/app/models/user';

export interface LoggedIn {
  type: UserStateType.LOGGED_IN;
  user: User;
}

export interface NotLoggedIn {
  type: UserStateType.NOT_LOGGED_IN;
}

export interface Loading {
  type: UserStateType.LOADING;
}

export type UserState = LoggedIn | NotLoggedIn | Loading;

export enum UserStateType {
  LOGGED_IN = 'LoggedIn',
  NOT_LOGGED_IN = 'NotLoggedIn',
  LOADING = 'Loading',
}

export const UserState = {
  loggedIn: (user: User) => <LoggedIn>{
    type: UserStateType.LOGGED_IN,
    user: user,
  },
  notLoggedIn: <NotLoggedIn>{type: UserStateType.NOT_LOGGED_IN},
  loading: <Loading>{type: UserStateType.LOADING},
};

export function isLoggedIn(user: UserState): user is LoggedIn {
  return user.type === UserStateType.LOGGED_IN;
}
<file_sep>import {Vertex} from '@src/app/models/vertex';

export class SearchVertex {
  constructor(
      public self: Vertex,
      public neighbors: SearchVertex[],
      public seen: boolean,
  ) {
  }

  public static fromVertex(vertex: Vertex): SearchVertex {
    return new SearchVertex(
        vertex,
        [],
        false,
    );
  }
}
<file_sep>import {Vertex} from '@src/app/models/vertex';
import {Edge} from '@src/app/models/edge';
import {Utils} from '@src/app/utils';
import {SearchVertex} from '@src/app/models/search-vertex';

export interface Graph {
  vertices: Vertex[];
  edges: Edge[];
  xSize: number;
  ySize: number;
}

export const Graph = {
  initialize: (xSize: number, ySize: number) => {
    const vertices = Array.from(Array(xSize * ySize).keys())
        .map(i =>
            Vertex.create(
                i,
                i % xSize,
                Math.floor(i / xSize),
            ),
        );
    const edges = Utils.flatMap(
        vertices,
        (vertex: Vertex, i: number) => {
          const e: Edge[] = [];
          if (vertex.x < xSize - 1) {
            e.push(Edge.initialize(vertex, vertices[i + 1]));
          }
          if (vertex.y < ySize - 1) {
            e.push(Edge.initialize(vertex, vertices[i + xSize]));
          }
          return e;
        },
    );
    return <Graph>{
      vertices: vertices,
      edges: edges,
      xSize: xSize,
      ySize: ySize,
    };
  },

  findPath: (graph: Graph, start: Vertex, end: Vertex) => {
    let found = false;
    const pathStack: Vertex[] = [];
    let finalPath: Vertex[] = [];

    function search(v: SearchVertex) {
      pathStack.push(v.self);
      if (v.self.id === end.id) {
        found = true;
        finalPath = [...pathStack];
        return;
      } else {
        v.seen = true;
        for (let i = 0; i < v.neighbors.length; i++) {
          const neighbor = v.neighbors[i];
          if (!neighbor.seen && !found) {
            search(neighbor);
          }
        }
      }
      pathStack.pop();
    }

    const searchGraph = graph.vertices.map(SearchVertex.fromVertex);
    graph.edges.filter(Edge.isOwned)
        .forEach(edge => {
          searchGraph[edge.source.id].neighbors.push(searchGraph[edge.target.id]);
          searchGraph[edge.target.id].neighbors.push(searchGraph[edge.source.id]);
        });
    search(searchGraph[start.id]);
    return finalPath;
  },
};

<file_sep>import {Vertex} from '@src/app/models/vertex';
import {Player} from '@src/app/models/player';

export interface Edge {
  source: Vertex;
  target: Vertex;
  owner: Player | null;
}

export const Edge = {
      initialize: (source: Vertex, target: Vertex) => <Edge>{
        source: source,
        target: target,
        owner: null,
      },

      isOwned: (edge: Edge) => edge.owner !== null,

      hasSameCoordinates: (edgeA: Edge, edgeB: Edge) =>
          edgeA.source.id === edgeB.source.id && edgeA.target.id === edgeB.target.id,
    }
;
<file_sep>import {Vertex} from '@src/app/models/vertex';
import {Polygon} from '@src/app/models/polygon';
import {Edge} from '@src/app/models/edge';
import {Square} from '@src/app/models/square';

describe('Polygon', () => {
  describe('fromPath', () => {
    it('should create a polygon', () => {
      const path: Vertex[] = [
        Vertex.create(0, 0, 0),
        Vertex.create(1, 1, 0),
        Vertex.create(2, 0, 1),
        Vertex.create(3, 1, 1),
      ];

      expect(Polygon.fromPath(path)).toEqual(
          new Polygon([
            Edge.initialize(path[0], path[1]),
            Edge.initialize(path[1], path[2]),
            Edge.initialize(path[2], path[3]),
            Edge.initialize(path[3], path[0]),
          ]),
      );
    });

    it('should throw an exception if created from a path with fewer than 4 vertices', () => {
      const path: Vertex[] = [
        Vertex.create(0, 0, 0),
        Vertex.create(1, 1, 0),
        Vertex.create(2, 0, 1),
      ];

      expect(() => Polygon.fromPath(path)).toThrowError();
    });
  });

  describe('contains', () => {
    /*
        A-----B
        |     |
        |  x~~+~~
        |     |
        C-----D
    */
    it('should return true if the square lies within a simple polygon', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 0, 1);
      const D = Vertex.create(3, 1, 1);
      const polygon = Polygon.fromPath([A, B, D, C]);
      const square = Square.create(A, B, C, D, null);
      expect(polygon.contains(square)).toEqual(true);
    });

    /*
        A-----B-----C-----D
        |                 |
        |                 |
        |                 |
        L     I-----H     E
        |     |     |     |
        |  x~~+~~~~~+~~~~~+~~
        |     |     |     |
        K-----J     G-----F
    */
    it('should return true if the square within a complex polygon such that the ray intersects more than once', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 2, 0);
      const D = Vertex.create(3, 3, 0);
      const E = Vertex.create(4, 3, 1);
      const F = Vertex.create(5, 3, 2);
      const G = Vertex.create(6, 2, 2);
      const H = Vertex.create(7, 2, 1);
      const I = Vertex.create(8, 1, 1);
      const J = Vertex.create(9, 1, 2);
      const K = Vertex.create(10, 0, 2);
      const L = Vertex.create(11, 0, 1);
      const polygon = Polygon.fromPath([A, B, C, D, E, F, G, H, I, J, K, L]);
      const square = Square.create(L, I, K, J, null);
      expect(polygon.contains(square)).toEqual(true);

    });

    /*
        A     B

           x~~~~~~

        C-----D
        |     |
        |     |
        |     |
        E-----F
    */
    it('should return false if the square lies above the polygon', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 0, 1);
      const D = Vertex.create(3, 1, 1);
      const E = Vertex.create(4, 0, 2);
      const F = Vertex.create(5, 1, 2);
      const polygon = Polygon.fromPath([C, D, F, E]);
      const square = Square.create(A, B, C, D, null);
      expect(polygon.contains(square)).toEqual(false);
    });

    /*
        A-----B
        |     |
        |     |
        |     |
        C-----D

           x~~~~~~

        E     F
    */
    it('should return false if the square lies below the polygon', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 0, 1);
      const D = Vertex.create(3, 1, 1);
      const E = Vertex.create(4, 0, 2);
      const F = Vertex.create(5, 1, 2);
      const polygon = Polygon.fromPath([A, B, C, D]);
      const square = Square.create(C, D, E, F, null);
      expect(polygon.contains(square)).toEqual(false);
    });

    /*
      A     B-----C
            |     |
         x~~+~~~~~+~~~
            |     |
      D     E-----F
    */
    it('should return false if the square lies left of the polygon', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 2, 0);
      const D = Vertex.create(3, 0, 1);
      const E = Vertex.create(4, 1, 1);
      const F = Vertex.create(5, 2, 1);
      const polygon = Polygon.fromPath([B, C, F, E]);
      const square = Square.create(A, B, D, E, null);
      expect(polygon.contains(square)).toEqual(false);
    });

    /*
        A-----B     C
        |     |
        |     |  x~~~~~~
        |     |
        D-----E     F
    */
    it('should return false if the square lies right of the polygon', () => {
      const A = Vertex.create(0, 0, 0);
      const B = Vertex.create(1, 1, 0);
      const C = Vertex.create(2, 2, 0);
      const D = Vertex.create(3, 0, 1);
      const E = Vertex.create(4, 1, 1);
      const F = Vertex.create(5, 2, 1);
      const polygon = Polygon.fromPath([A, B, E, D]);
      const square = Square.create(B, C, E, F, null);
      expect(polygon.contains(square)).toEqual(false);
    });
  });
});
<file_sep>import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import {AppRoutingModule} from '@src/app/app-routing.module';
import {AppComponent} from '@src/app/app.component';
import {HomeComponent} from '@src/app/components/home/home.component';
import {GameComponent} from '@src/app/components/game/game.component';
import {AngularFireModule} from '@angular/fire';
import {AngularFireAuthModule} from '@angular/fire/auth';
import {AngularFirestoreModule} from '@angular/fire/firestore';
import {NgxFpTsModule} from 'ngx-fp-ts';
import {
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatDividerModule,
  MatMenuModule,
  MatProgressSpinnerModule,
  MatToolbarModule
} from '@angular/material';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MenuBarComponent} from '@src/app/components/menu-bar/menu-bar.component';
import {AuthService} from '@src/app/services/auth.service';
import {GameService} from '@src/app/services/game.service';

const firebaseConfig = {
  apiKey: '<KEY>',
  authDomain: 'stift-und-papier.firebaseapp.com',
  databaseURL: 'https://stift-und-papier.firebaseio.com',
  projectId: 'stift-und-papier',
  storageBucket: 'stift-und-papier.appspot.com',
  messagingSenderId: '436152985520',
  appId: '1:436152985520:web:29245f67f32864151c1875',
};

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    GameComponent,
    MenuBarComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(firebaseConfig),
    AngularFireAuthModule,
    AngularFirestoreModule,
    NgxFpTsModule,
    MatProgressSpinnerModule,
    MatCardModule,
    MatButtonToggleModule,
    MatButtonModule,
    MatToolbarModule,
    MatDividerModule,
    MatMenuModule,
    BrowserAnimationsModule,
    FontAwesomeModule,
  ],
  providers: [
    AuthService,
    GameService,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {
}
<file_sep>export interface EdgeStyle {
  stroke: string;
  opacity: number;
}
<file_sep>import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {AngularFireAuth} from '@angular/fire/auth';
import {auth} from 'firebase';
import {UserState} from '@src/app/models/user-state';

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  private currentUser = new BehaviorSubject<UserState>(UserState.loading);
  currentUser$ = this.currentUser.asObservable();

  constructor(
    public fireAuth: AngularFireAuth,
  ) {
    fireAuth.authState.subscribe(user => {
      if (user) {
        this.currentUser.next(UserState.loggedIn(user));
      } else {
        this.currentUser.next(UserState.notLoggedIn);
      }
    });
  }

  async googleSignin() {
    const provider = new auth.GoogleAuthProvider();
    await this.fireAuth.auth.signInWithPopup(provider);
  }

  async signOut() {
    await this.fireAuth.auth.signOut();
  }

}
<file_sep>import {Injectable} from '@angular/core';
import {AngularFirestore} from '@angular/fire/firestore';
import {from, Observable} from 'rxjs';
import {Game} from '@src/app/models/game';
import {switchMap} from 'rxjs/operators';
import {Graph} from '@src/app/models/graph';
import {Square} from '@src/app/models/square';
import {Player} from '@src/app/models/player';

@Injectable({
  providedIn: 'root',
})
export class GameService {

  constructor(
      private firestore: AngularFirestore,
  ) {
  }

  private gamesCollection = this.firestore.collection('games');

  getGame(userId: string): Observable<Game> {
    const gameDoc = this.gamesCollection.doc<Game>(userId);
    return gameDoc.get().pipe(
        switchMap(g => {
          if (!g.exists) {
            return from(this.resetGame(userId));
          } else {
            return from(Promise.resolve());
          }
        }),
        switchMap(() => gameDoc.valueChanges()),
    );
  }

  resetGame(userId: string): Promise<void> {
    return this.gamesCollection.doc<Game>(userId).set(this.initializeGame(userId));
  }

  setGame(game: Game): Promise<void> {
    return this.gamesCollection.doc<Game>(game.id).set(game);
  }

  private initializeGame(id: string): Game {
    const graph = Graph.initialize(5, 5);
    return <Game>{
      id: id,
      graph: graph,
      squares: Square.fromGraph(graph),
      players: [
        Player.create(0, 'Alice', 'royalblue', 0),
        Player.create(1, 'Bob', '#F08080', 0),
      ],
      currentPlayerIdx: 0,
      moves: [],
    };
  }

}
<file_sep># 📝 Pen & Paper

<a target="_blank" href="https://angular.io/"><img rel="noopener noreferrer" src="https://img.shields.io/badge/made%20with-Angular-blue.svg" alt="made with Angular"></a> <a target="_blank" href="https://www.electronjs.org/"><img rel="noopener noreferrer" src="https://img.shields.io/badge/made%20with-Electron-blue.svg" alt="made with Angular"></a> <a rel="noopener noreferrer" target="_blank" href="https://www.nativescript.org/"><img src="https://img.shields.io/badge/made%20with-NativeScript-blue.svg" alt="made with NativeScript"></a> 

<img src="https://img.shields.io/badge/runs%20on-macOS-green.svg" alt="runs on macOS"> <img src="https://img.shields.io/badge/runs%20on-Linux-green.svg" alt="runs on Linux"> <img src="https://img.shields.io/badge/runs%20on-Windows-green.svg" alt="runs on Windows"> <img src="https://img.shields.io/badge/runs%20on-iOS-green.svg" alt="runs on iOS"> <img src="https://img.shields.io/badge/runs%20on-Android-green.svg" alt="runs on Android">

## Description

Want to join for a game, fam? https://stift-und-papier.web.app/

## Development

### Requirements

Go through the [NativeScript full setup](https://docs.nativescript.org/angular/start/quick-setup#full-setup).

### Commands

- `npm run start` to start the web app 
- `npm run desktop` to start the desktop app
- `npm run android` to start the Android app
- `npm run ios` to start the iOS app

### Code Sharing / Separation

By adding a `.tns` before the file extension, you can indicate that this file is NativeScript-specific, while the same file without the .tns extension is marked as web-specific. If we have just one file without the .tns extension, then this makes it a shared file.
<file_sep>import {Component, Input, OnInit} from '@angular/core';
import {faBars, faHome, faSync} from '@fortawesome/free-solid-svg-icons';
import {GameService} from '@src/app/services/game.service';
import {AuthService} from '@src/app/services/auth.service';
import {filter, map} from 'rxjs/operators';
import {isLoggedIn, UserStateType} from '@src/app/models/user-state';

@Component({
  selector: 'pnp-menu-bar',
  templateUrl: './menu-bar.component.html',
  styleUrls: ['./menu-bar.component.css'],
})
export class MenuBarComponent implements OnInit {

  @Input()
  title: string;

  faBars = faBars;
  faHome = faHome;
  faSync = faSync;
  userState = UserStateType;

  constructor(
      private gameService: GameService,
      public authService: AuthService,
  ) {
  }

  async resetGame(): Promise<void> {
    return await this.authService.currentUser$.pipe(
        filter(isLoggedIn),
        map(loggedIn => this.gameService.resetGame(loggedIn.user.uid)),
    ).toPromise();
  }

  ngOnInit() {
  }

}
<file_sep># Security Policy

## Reporting a Vulnerability

If you find a vulnerability, please create a GitHub issue.
<file_sep>export class Utils {
  public static flatMap<I, O>(xs: I[], f: (I, number) => O[]): O[] {
    return xs.map(f).reduce((x, y) => x.concat(y), []);
  }
}
<file_sep>export interface ScoreBoard {
  [playerId: number]: number;
}
<file_sep>import {Graph} from '@src/app/models/graph';
import {Vertex} from '@src/app/models/vertex';
import {Edge} from '@src/app/models/edge';
import {Player} from '@src/app/models/player';

describe('Graph', () => {
  describe('Initialization', () => {
    it('should initialize an empty graph', () => {
      const graph = Graph.initialize(0, 0);
      expect(graph.vertices).toEqual([]);
      expect(graph.edges).toEqual([]);
    });

    it('should initialize a graph with a single node', () => {
      const graph = Graph.initialize(1, 1);
      expect(graph.vertices).toEqual([
        Vertex.create(0, 0, 0),
      ]);
      expect(graph.edges).toEqual([]);
    });

    it('should initialize a non-empty graph', () => {
      const graph = Graph.initialize(4, 3);
      expect(graph.vertices).toEqual([
        Vertex.create(0, 0, 0),
        Vertex.create(1, 1, 0),
        Vertex.create(2, 2, 0),
        Vertex.create(3, 3, 0),
        Vertex.create(4, 0, 1),
        Vertex.create(5, 1, 1),
        Vertex.create(6, 2, 1),
        Vertex.create(7, 3, 1),
        Vertex.create(8, 0, 2),
        Vertex.create(9, 1, 2),
        Vertex.create(10, 2, 2),
        Vertex.create(11, 3, 2),
      ]);
      expect(graph.edges).toEqual(jasmine.arrayContaining([
        Edge.initialize(graph.vertices[0], graph.vertices[1]),
        Edge.initialize(graph.vertices[1], graph.vertices[2]),
        Edge.initialize(graph.vertices[2], graph.vertices[3]),
        Edge.initialize(graph.vertices[0], graph.vertices[4]),
        Edge.initialize(graph.vertices[1], graph.vertices[5]),
        Edge.initialize(graph.vertices[2], graph.vertices[6]),
        Edge.initialize(graph.vertices[3], graph.vertices[7]),
        Edge.initialize(graph.vertices[4], graph.vertices[5]),
        Edge.initialize(graph.vertices[5], graph.vertices[6]),
        Edge.initialize(graph.vertices[6], graph.vertices[7]),
        Edge.initialize(graph.vertices[4], graph.vertices[8]),
        Edge.initialize(graph.vertices[5], graph.vertices[9]),
        Edge.initialize(graph.vertices[6], graph.vertices[10]),
        Edge.initialize(graph.vertices[7], graph.vertices[11]),
        Edge.initialize(graph.vertices[8], graph.vertices[9]),
        Edge.initialize(graph.vertices[9], graph.vertices[10]),
        Edge.initialize(graph.vertices[10], graph.vertices[11]),
      ]));
    });
  });

  describe('findPath', () => {
    const player = Player.create(0, 'currentPlayer', 'green', 0);

    it('should not detect a path if there is none due to missing ownership in a trivial graph', () => {
      const graph = Graph.initialize(1, 2);
      expect(Graph.findPath(graph, graph.vertices[0], graph.vertices[1])).toEqual([]);
    });

    it('should detect an existing path in a trivial graph', () => {
      const graph = Graph.initialize(1, 2);
      graph.edges[0].owner = player;
      expect(Graph.findPath(graph, graph.vertices[0], graph.vertices[1])).toEqual([graph.vertices[0], graph.vertices[1]]);
    });

    it('should not detect a path if there is none due to missing ownership in a complex graph', () => {
      const graph = Graph.initialize(100, 100);
      expect(Graph.findPath(graph, graph.vertices[0], graph.vertices[99])).toEqual([]);
    });

    it('should detect an existing path in a complex graph', () => {
      const graph = Graph.initialize(100, 100);
      const expectedPath: Vertex[] = [graph.vertices[0]];
      for (let x = 0; x < graph.xSize - 1; x++) {
        const edge = graph.edges.find(e => e.source.x === x && e.source.y === 0);
        edge.owner = player;
        expectedPath.push(edge.target);
      }
      for (let y = 0; y < graph.xSize - 1; y++) {
        const edge = graph.edges.find(e => e.source.x === graph.xSize - 1 && e.source.y === y);
        edge.owner = player;
        expectedPath.push(edge.target);
      }
      expect(Graph.findPath(graph, graph.vertices[0], graph.vertices[graph.vertices.length - 1])).toEqual(expectedPath);
    });
  });
});
<file_sep>import {Component, OnInit} from '@angular/core';
import {AuthService} from '@src/app/services/auth.service';
import {GameService} from '@src/app/services/game.service';
import {faGoogle} from '@fortawesome/free-brands-svg-icons';
import {faSignOutAlt} from '@fortawesome/free-solid-svg-icons';
import {UserStateType} from '@src/app/models/user-state';

@Component({
  selector: 'pnp-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent implements OnInit {
  title = 'Pen & Paper';
  googleIcon = faGoogle;
  logoutIcon = faSignOutAlt;

  constructor(
    public auth: AuthService,
    public game: GameService,
  ) {
  }

  userState = UserStateType;

  ngOnInit() {
  }
}
<file_sep>import {Edge} from '@src/app/models/edge';
import {Vertex} from '@src/app/models/vertex';
import {Square} from '@src/app/models/square';

export class Polygon {
  constructor(public edges: Edge[]) {
  }

  public static fromPath(path: Vertex[]): Polygon {
    if (path.length < 4) {
      throw new Error('Polygon must have at least 4 edges');
    }

    const edges: Edge[] = [];
    for (let i = 0; i < path.length - 1; i++) {
      edges.push(Edge.initialize(path[i], path[i + 1]));
    }
    edges.push(Edge.initialize(path[path.length - 1], path[0]));
    return new Polygon(edges);
  }

  /*
    Checks whether the square is contained within the polygon. We use
    a simplified version of the ray casting algorithm. To solve our
    square-in-polygon problem, we cast a ray from the center of the square
    to the right and count the number of intersections with the polygon.
    If the number of intersections is odd, the square lies within the polygon.

    See the test cases for examples.
   */
  public contains(square: Square): boolean {
    const isVertical = (e: Edge) =>
        e.source.x === e.target.x;
    const squareIsLeftFromEdge = (s: Square, e: Edge) =>
        s.topRight.x <= e.source.x;
    const hasSameY = (s: Square, e: Edge) =>
        Math.min(e.source.y, e.target.y) === square.topRight.y &&
        Math.max(e.source.y, e.target.y) === square.bottomRight.y;

    const numIntersections = this.edges
        .filter(edge => isVertical(edge) && squareIsLeftFromEdge(square, edge) && hasSameY(square, edge))
        .length;
    return numIntersections % 2 === 1;
  }

}

