diff --git a/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts b/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts
new file mode 100644
index 000000000..d844c3cf0
--- /dev/null
+++ b/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts
@@ -0,0 +1,610 @@
+/**
+ * Remember: Do not tell, Do not ask.
+ */
+
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { MatDialogRef, MatDialog } from '@angular/material';
+import { OperatorService } from '../../../core/services/operator.service';
+import { NotifyService, NotifyResponse } from '../../../core/services/notify.service';
+import { Subscription } from 'rxjs';
+
+/**
+ * All player types.
+ */
+enum Player {
+ noPlayer,
+ thisPlayer,
+ partner
+}
+
+/**
+ * All states the game board can have.
+ */
+enum BoardStatus {
+ Draw = 'draw',
+ thisPlayer = 'thisPlayer',
+ partner = 'partner',
+ NotDecided = 'not decided'
+}
+
+/**
+ * All states for the statemachine
+ */
+type State = 'start' | 'search' | 'waitForResponse' | 'myTurn' | 'foreignTurn';
+
+/**
+ * All events that can be handled by the statemachine.
+ */
+type StateEvent =
+ | 'searchClicked'
+ | 'recievedSearchRequest'
+ | 'recievedSearchResponse'
+ | 'recievedACK'
+ | 'waitTimeout'
+ | 'fieldClicked'
+ | 'recievedTurn'
+ | 'recievedRagequit';
+
+/**
+ * An action in one state.
+ */
+interface SMAction {
+ handle: (data?: any) => State | null;
+}
+
+/**
+ * The statemachine. Mapps events in states to actions.
+ */
+type StateMachine = { [state in State]?: { [event in StateEvent]?: SMAction } };
+
+@Component({
+ selector: 'os-c4dialog',
+ template: `
+
{{ caption | translate }}
+
+
+
+
+
{{ getPlayerName() }}
+
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [
+ `
+ span {
+ font-size: 20px;
+ padding-left: 10px;
+ }
+ .center {
+ text-align: center;
+ }
+ .space {
+ margin-bottom: 5px;
+ }
+ #c4 {
+ background-color: yellow;
+ padding: 10px;
+ margin: 15px auto;
+ }
+ #c4.disabled {
+ background-color: grey;
+ }
+ #c4 td {
+ width: 50px;
+ height: 50px;
+ }
+ #c4 .notSelected {
+ cursor: pointer;
+ background-color: white;
+ }
+ #c4.disabled .notSelected {
+ cursor: auto;
+ }
+ .thisPlayer {
+ background-color: blue;
+ }
+ #c4.disabled .thisPlayer {
+ background-color: #8888ff;
+ }
+ .partner {
+ background-color: red;
+ }
+ #c4.disabled .partner {
+ background-color: #ff8888;
+ }
+ .coin {
+ border-radius: 50%;
+ width: 100%;
+ height: 100%;
+ }
+ .coin.info-coin {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ }
+ `
+ ]
+})
+export class C4DialogComponent implements OnInit, OnDestroy {
+ /**
+ * The dialogs caption
+ */
+ public caption: string;
+
+ /**
+ * Saves, if the board is disabled.
+ */
+ public disableBoard: boolean;
+
+ /**
+ * The board. First columns, then rows. Size is 7x6.
+ */
+ public board: Player[][];
+
+ /**
+ * The channel of the partner.
+ */
+ private replyChannel: string;
+
+ /**
+ * The partners name.
+ */
+ public partnerName: string;
+
+ /**
+ * A timeout to go from waiting to search state.
+ */
+ private waitTimout: number | null;
+
+ /**
+ * A list of all subscriptions, so they can b unsubscribed on desroy.
+ */
+ private subscriptions: Subscription[] = [];
+
+ /**
+ * The current state of the state machine.
+ */
+ public state: State;
+
+ /**
+ * This is the state machine for this game :)
+ */
+ public SM: StateMachine = {
+ start: {
+ searchClicked: {
+ handle: () => {
+ this.disableBoard = false;
+ this.resetBoard();
+ return 'search';
+ }
+ }
+ },
+ search: {
+ recievedSearchRequest: {
+ handle: (notify: NotifyResponse<{ name: string }>) => {
+ this.replyChannel = notify.senderChannelName;
+ this.partnerName = notify.content.name;
+ return 'waitForResponse';
+ }
+ },
+ recievedSearchResponse: {
+ handle: (notify: NotifyResponse<{ name: string }>) => {
+ this.replyChannel = notify.senderChannelName;
+ this.partnerName = notify.content.name;
+ // who starts?
+ const startPlayer = Math.random() < 0.5 ? Player.thisPlayer : Player.partner;
+ const startPartner: boolean = startPlayer === Player.partner;
+ // send ACK
+ this.notifyService.sendToChannels('c4_ACK', startPartner, this.replyChannel);
+ return startPlayer === Player.thisPlayer ? 'myTurn' : 'foreignTurn';
+ }
+ }
+ },
+ waitForResponse: {
+ recievedACK: {
+ handle: (notify: NotifyResponse<{}>) => {
+ if (notify.senderChannelName !== this.replyChannel) {
+ return null;
+ }
+ return notify.content ? 'myTurn' : 'foreignTurn';
+ }
+ },
+ waitTimeout: {
+ handle: () => 'search'
+ },
+ recievedRagequit: {
+ handle: (notify: NotifyResponse<{}>) => {
+ return notify.senderChannelName === this.replyChannel ? 'search' : null;
+ }
+ }
+ },
+ myTurn: {
+ fieldClicked: {
+ handle: (data: { col: number; row: number }) => {
+ if (this.colFree(data.col)) {
+ this.setCoin(data.col, Player.thisPlayer);
+ this.notifyService.sendToChannels('c4_turn', { col: data.col }, this.replyChannel);
+ const nextState = this.getStateFromBoardStatus();
+ return nextState === null ? 'foreignTurn' : nextState;
+ } else {
+ return null;
+ }
+ }
+ },
+ recievedRagequit: {
+ handle: () => {
+ this.caption = "Your partner couldn't stand it anymore... You are the winner!";
+ return 'start';
+ }
+ }
+ },
+ foreignTurn: {
+ recievedTurn: {
+ handle: (notify: NotifyResponse<{ col: number }>) => {
+ if (notify.senderChannelName !== this.replyChannel) {
+ return null;
+ }
+ const col: number = notify.content.col;
+ if (!this.colFree(col)) {
+ return null;
+ }
+ this.setCoin(col, Player.partner);
+ const nextState = this.getStateFromBoardStatus();
+ return nextState === null ? 'myTurn' : nextState;
+ }
+ },
+ recievedRagequit: {
+ handle: () => {
+ this.caption = "Your partner couldn't stand it anymore... You are the winner!";
+ return 'start';
+ }
+ }
+ }
+ };
+
+ public constructor(
+ public dialogRef: MatDialogRef,
+ private notifyService: NotifyService,
+ private op: OperatorService
+ ) {
+ this.resetBoard();
+ }
+
+ public ngOnInit(): void {
+ // Setup initial values.
+ this.state = 'start';
+ this.caption = 'Connect 4';
+ this.disableBoard = true;
+
+ // Setup all subscription for needed notify messages
+ this.subscriptions = [
+ this.notifyService.getMessageObservable('c4_ACK').subscribe(notify => {
+ if (!notify.sendByThisUser) {
+ this.handleEvent('recievedACK', notify);
+ }
+ }),
+ this.notifyService.getMessageObservable('c4_ragequit').subscribe(notify => {
+ if (!notify.sendByThisUser) {
+ this.handleEvent('recievedRagequit', notify);
+ }
+ }),
+ this.notifyService.getMessageObservable('c4_search_request').subscribe(notify => {
+ if (!notify.sendByThisUser) {
+ this.handleEvent('recievedSearchRequest', notify);
+ }
+ }),
+ this.notifyService.getMessageObservable('c4_search_response').subscribe(notify => {
+ if (!notify.sendByThisUser) {
+ this.handleEvent('recievedSearchResponse', notify);
+ }
+ }),
+ this.notifyService.getMessageObservable('c4_turn').subscribe(notify => {
+ if (!notify.sendByThisUser) {
+ this.handleEvent('recievedTurn', notify);
+ }
+ })
+ ];
+ }
+
+ public ngOnDestroy(): void {
+ // send ragequit and unsubscribe all subscriptions.
+ if (this.replyChannel) {
+ this.notifyService.sendToChannels('c4_ragequit', null, this.replyChannel);
+ }
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
+ }
+
+ /**
+ * Resets the board.
+ */
+ private resetBoard(): void {
+ this.board = [];
+ for (let i = 0; i < 7; i++) {
+ const row = [];
+ for (let j = 0; j < 6; j++) {
+ row.push(Player.noPlayer);
+ }
+ this.board.push(row);
+ }
+ }
+
+ /**
+ * Returns the class needed in the board.
+ * @param row The row
+ * @param col The column
+ */
+ public getCoinClass(row: number, col: number): string {
+ switch (this.board[col][row]) {
+ case Player.noPlayer:
+ return 'coin notSelected';
+ case Player.thisPlayer:
+ return 'coin thisPlayer';
+ case Player.partner:
+ return 'coin partner';
+ }
+ }
+
+ /**
+ * Returns the operators name.
+ */
+ public getPlayerName(): string {
+ return this.op.user.short_name;
+ }
+
+ /**
+ * Returns null, if the game is not finished.
+ */
+ private getStateFromBoardStatus(): State {
+ switch (this.boardStatus()) {
+ case BoardStatus.Draw:
+ this.caption = 'Game draw!';
+ return 'start';
+ case BoardStatus.thisPlayer:
+ this.caption = 'You won!';
+ return 'start';
+ case BoardStatus.partner:
+ this.caption = 'Your partner has won!';
+ return 'start';
+ case BoardStatus.NotDecided:
+ return null;
+ }
+ }
+
+ /**
+ * Main state machine handler. The current state handler will be called with
+ * the given event. If the handler returns a state (and not null), this will be
+ * the next state. The state enter method will be called.
+ * @param e The event for the statemachine.
+ * @param data Additional data for the handler.
+ */
+ public handleEvent(e: StateEvent, data?: any): void {
+ let action: SMAction = null;
+ if (this.SM[this.state] && this.SM[this.state][e]) {
+ action = this.SM[this.state][e];
+ const nextState = action.handle(data);
+ if (nextState !== null) {
+ this.state = nextState;
+ if (this['enter_' + nextState]) {
+ this['enter_' + nextState]();
+ }
+ }
+ }
+ }
+
+ /**
+ * Handler for clicks on the field.
+ * @param row the row clicked
+ * @param col the col clicked
+ */
+ public clickField(row: number, col: number): void {
+ if (!this.disableBoard) {
+ this.handleEvent('fieldClicked', { row: row, col: col });
+ }
+ }
+
+ // Enter state methods
+ /**
+ * Resets all attributes of the state machine.
+ */
+ public enter_start(): void {
+ this.disableBoard = true;
+ this.replyChannel = null;
+ this.partnerName = null;
+ }
+
+ /**
+ * Sends a search request for other players.
+ */
+ public enter_search(): void {
+ this.caption = 'Searching for players...';
+ this.notifyService.sendToAllUsers('c4_search_request', { name: this.getPlayerName() });
+ }
+
+ /**
+ * Sends a search response for a previous request.
+ * Also sets up a timeout to go back into the search state.
+ */
+ public enter_waitForResponse(): void {
+ this.caption = 'Wait for response...';
+ this.notifyService.send('c4_search_response', { name: this.getPlayerName() });
+ if (this.waitTimout) {
+ clearTimeout(this.waitTimout);
+ }
+ this.waitTimout = setTimeout(() => {
+ this.handleEvent('waitTimeout');
+ }, 5000);
+ }
+
+ /**
+ * Sets the caption.
+ */
+ public enter_myTurn(): void {
+ this.caption = "It's your turn!";
+ }
+
+ /**
+ * Sets the caption.
+ */
+ public enter_foreignTurn(): void {
+ this.caption = "It's your partners turn";
+ }
+
+ // Board function
+ /**
+ * Places a coin on the board
+ * @param col The col to place a coin
+ * @param player The player who placed the coin
+ */
+ private setCoin(col: number, player: Player): void {
+ for (let row = 0; row < 6; row++) {
+ if (this.board[col][row] === Player.noPlayer) {
+ this.board[col][row] = player;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns true, if the given col is free to place a coin there
+ * @param col the col
+ */
+ private colFree(col: number): boolean {
+ return this.board[col][5] === Player.noPlayer;
+ }
+
+ /**
+ * Returns the current state of the board
+ */
+ private boardStatus(): BoardStatus {
+ // check if a player has won
+ // vertical
+ let won: Player;
+ for (let row = 0; row < 6; row++) {
+ for (let col = 0; col < 4; col++) {
+ won = this.board[col][row];
+ for (let i = 1; i < 4 && won !== Player.noPlayer; i++) {
+ if (this.board[col + i][row] !== won) {
+ won = Player.noPlayer;
+ }
+ }
+ if (won !== Player.noPlayer) {
+ return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner;
+ }
+ }
+ }
+ // horizontal
+ for (let col = 0; col < 7; col++) {
+ for (let row = 0; row < 3; row++) {
+ won = this.board[col][row];
+ for (let i = 1; i < 4 && won !== Player.noPlayer; i++) {
+ if (this.board[col][row + i] !== won) {
+ won = Player.noPlayer;
+ }
+ }
+ if (won !== Player.noPlayer) {
+ return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner;
+ }
+ }
+ }
+ // diag 1
+ for (let col = 0; col < 4; col++) {
+ for (let row = 0; row < 3; row++) {
+ won = this.board[col][row];
+ for (let i = 1; i < 4 && won !== Player.noPlayer; i++) {
+ if (this.board[col + i][row + i] !== won) {
+ won = Player.noPlayer;
+ }
+ }
+ if (won !== Player.noPlayer) {
+ return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner;
+ }
+ }
+ }
+ // diag 1
+ for (let col = 3; col < 7; col++) {
+ for (let row = 0; row < 3; row++) {
+ won = this.board[col][row];
+ for (let i = 1; i < 4 && won !== Player.noPlayer; i++) {
+ if (this.board[col - i][row + i] !== won) {
+ won = Player.noPlayer;
+ }
+ }
+ if (won !== Player.noPlayer) {
+ return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner;
+ }
+ }
+ }
+ // game draw?
+ let draw = true;
+ for (let col = 0; col < 7; col++) {
+ if (this.board[col][5] === Player.noPlayer) {
+ draw = false;
+ break;
+ }
+ }
+ return draw ? BoardStatus.Draw : BoardStatus.NotDecided;
+ }
+}
+
+@Component({
+ selector: 'os-copyright-sign',
+ template: `
+ ©
+ `,
+ styles: [``]
+})
+export class CopyrightSignComponent {
+ private clickTimeout: number | null;
+ private clickCounter = 0;
+
+ public constructor(private dialog: MatDialog, private op: OperatorService) {}
+
+ public launchC4(event: Event): void {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // no anonymous invited..
+ if (!this.op.user) {
+ return;
+ }
+
+ this.clickCounter++;
+ if (this.clickTimeout) {
+ clearTimeout(this.clickTimeout);
+ }
+
+ if (this.clickCounter === 5) {
+ this.clickCounter = 0;
+ this.dialog.open(C4DialogComponent, { width: '550px' });
+ } else {
+ this.clickTimeout = setTimeout(() => {
+ this.clickCounter = 0;
+ }, 200);
+ }
+ }
+}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 71c17ac88..566b1632f 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -71,6 +71,7 @@ import { OsSortFilterBarComponent } from './components/os-sort-filter-bar/os-sor
import { OsSortBottomSheetComponent } from './components/os-sort-filter-bar/os-sort-bottom-sheet/os-sort-bottom-sheet.component';
import { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component';
import { LogoComponent } from './components/logo/logo.component';
+import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component';
/**
* Share Module for all "dumb" components and pipes.
@@ -173,7 +174,9 @@ import { LogoComponent } from './components/logo/logo.component';
SortingTreeComponent,
TreeModule,
OsSortFilterBarComponent,
- LogoComponent
+ LogoComponent,
+ CopyrightSignComponent,
+ C4DialogComponent
],
declarations: [
PermsDirective,
@@ -191,7 +194,9 @@ import { LogoComponent } from './components/logo/logo.component';
OsSortFilterBarComponent,
OsSortBottomSheetComponent,
FilterMenuComponent,
- LogoComponent
+ LogoComponent,
+ CopyrightSignComponent,
+ C4DialogComponent
],
providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
@@ -201,6 +206,6 @@ import { LogoComponent } from './components/logo/logo.component';
OsSortFilterBarComponent,
OsSortBottomSheetComponent
],
- entryComponents: [OsSortBottomSheetComponent]
+ entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
})
export class SharedModule {}
diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html
index 46f051577..261d8526c 100644
--- a/client/src/app/site/site.component.html
+++ b/client/src/app/site/site.component.html
@@ -120,7 +120,7 @@
target="_blank"
(click)="toggleSideNav()"
>
- © Copyright by OpenSlides
+ Copyright by OpenSlides