Some teamwork improvements

This commit is contained in:
FinnStutzenstein 2019-01-18 16:55:56 +01:00
parent 47a09e4bce
commit 728f3f84c4
3 changed files with 619 additions and 4 deletions

View File

@ -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: `
<h2 mat-dialog-title>{{ caption | translate }}</h2>
<mat-dialog-content>
<ng-container *ngIf="partnerName">
<div class="space">
<div class="coin info-coin thisPlayer"></div>
<span>{{ getPlayerName() }}</span>
</div>
<div>
<div class="coin info-coin partner"></div>
<span>{{ partnerName }}</span>
</div>
</ng-container>
<table [class]="disableBoard ? 'disabled' : ''" id="c4">
<tr *ngFor="let row of [5, 4, 3, 2, 1, 0]">
<td *ngFor="let col of [0, 1, 2, 3, 4, 5, 6]">
<div [class]="getCoinClass(row, col)" (click)="clickField(row, col)"></div>
</td>
</tr>
</table>
<div class="center">
<button
(click)="handleEvent('searchClicked')"
*ngIf="state === 'start'"
mat-raised-button
color="accent"
translate
>
Search player
</button>
</div>
</mat-dialog-content>
<mat-dialog-actions> <button mat-button mat-dialog-close translate>Close</button> </mat-dialog-actions>
`,
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<C4DialogComponent>,
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(<any>this.waitTimout);
}
this.waitTimout = <any>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: `
<span (click)="launchC4($event)">©</span>
`,
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(<any>this.clickTimeout);
}
if (this.clickCounter === 5) {
this.clickCounter = 0;
this.dialog.open(C4DialogComponent, { width: '550px' });
} else {
this.clickTimeout = <any>setTimeout(() => {
this.clickCounter = 0;
}, 200);
}
}
}

View File

@ -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 { 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 { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component';
import { LogoComponent } from './components/logo/logo.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. * Share Module for all "dumb" components and pipes.
@ -173,7 +174,9 @@ import { LogoComponent } from './components/logo/logo.component';
SortingTreeComponent, SortingTreeComponent,
TreeModule, TreeModule,
OsSortFilterBarComponent, OsSortFilterBarComponent,
LogoComponent LogoComponent,
CopyrightSignComponent,
C4DialogComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -191,7 +194,9 @@ import { LogoComponent } from './components/logo/logo.component';
OsSortFilterBarComponent, OsSortFilterBarComponent,
OsSortBottomSheetComponent, OsSortBottomSheetComponent,
FilterMenuComponent, FilterMenuComponent,
LogoComponent LogoComponent,
CopyrightSignComponent,
C4DialogComponent
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },
@ -201,6 +206,6 @@ import { LogoComponent } from './components/logo/logo.component';
OsSortFilterBarComponent, OsSortFilterBarComponent,
OsSortBottomSheetComponent OsSortBottomSheetComponent
], ],
entryComponents: [OsSortBottomSheetComponent] entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
}) })
export class SharedModule {} export class SharedModule {}

View File

@ -120,7 +120,7 @@
target="_blank" target="_blank"
(click)="toggleSideNav()" (click)="toggleSideNav()"
> >
<span><small>© Copyright by OpenSlides</small></span> <span><small><os-copyright-sign></os-copyright-sign>&nbsp;Copyright by OpenSlides</small></span>
</a> </a>
<div class="os-footer-logo-container"> <div class="os-footer-logo-container">
<os-logo inputAction="logo_web_header" <os-logo inputAction="logo_web_header"