From 728f3f84c462113f8e71977ff71daf593a92b017 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Fri, 18 Jan 2019 16:55:56 +0100 Subject: [PATCH] Some teamwork improvements --- .../copyright-sign.component.ts | 610 ++++++++++++++++++ client/src/app/shared/shared.module.ts | 11 +- client/src/app/site/site.component.html | 2 +- 3 files changed, 619 insertions(+), 4 deletions(-) create mode 100644 client/src/app/shared/components/copyright-sign/copyright-sign.component.ts 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() }} +
+
+
+ {{ partnerName }} +
+
+ + + + +
+
+
+ +
+ +
+
+ + `, + 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