Merge pull request #4118 from FinnStutzenstein/notify
Make OS3 notify ready
This commit is contained in:
commit
8bd24d690d
@ -1,10 +1,62 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Subject, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||||
import { WebsocketService } from './websocket.service';
|
import { WebsocketService } from './websocket.service';
|
||||||
|
import { OperatorService } from './operator.service';
|
||||||
|
|
||||||
interface NotifyFormat {
|
/**
|
||||||
id: number; // Dummy
|
* Encapslates the name and content of every message regardless of being a request or response.
|
||||||
|
*/
|
||||||
|
interface NotifyBase<T> {
|
||||||
|
/**
|
||||||
|
* The name of the notify message.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content to send.
|
||||||
|
*/
|
||||||
|
content: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface has all fields for a notify request to the server. Next to name and content
|
||||||
|
* one can give an array of user ids (or the value `true` for all users) and an array of
|
||||||
|
* channel names.
|
||||||
|
*/
|
||||||
|
export interface NotifyRequest<T> extends NotifyBase<T> {
|
||||||
|
/**
|
||||||
|
* User ids (or `true` for all users) to send this message to.
|
||||||
|
*/
|
||||||
|
users?: number[] | boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of channels to send this message to.
|
||||||
|
*/
|
||||||
|
replyChannels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the notify-format one recieves from the server.
|
||||||
|
*/
|
||||||
|
export interface NotifyResponse<T> extends NotifyBase<T> {
|
||||||
|
/**
|
||||||
|
* This is the channel name of the one, who sends this message. Can be use to directly
|
||||||
|
* answer this message.
|
||||||
|
*/
|
||||||
|
senderChannelName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user id of the user who sends this message. It is 0 for Anonymous.
|
||||||
|
*/
|
||||||
|
senderUserId: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is validated here and is true, if the senderUserId matches the current operator's id.
|
||||||
|
* It's also true, if one recieves a request from an anonymous and the operator itself is the anonymous.
|
||||||
|
*/
|
||||||
|
sendByThisUser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,29 +66,102 @@ interface NotifyFormat {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class NotifyService extends OpenSlidesComponent {
|
export class NotifyService extends OpenSlidesComponent {
|
||||||
|
/**
|
||||||
|
* A general subject for all messages.
|
||||||
|
*/
|
||||||
|
private notifySubject = new Subject<NotifyResponse<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subjects for specific messages.
|
||||||
|
*/
|
||||||
|
private messageSubjects: {
|
||||||
|
[name: string]: Subject<NotifyResponse<any>>;
|
||||||
|
} = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor to create the NotifyService. Registers itself to the WebsocketService.
|
* Constructor to create the NotifyService. Registers itself to the WebsocketService.
|
||||||
* @param websocketService
|
* @param websocketService
|
||||||
*/
|
*/
|
||||||
public constructor(private websocketService: WebsocketService) {
|
public constructor(private websocketService: WebsocketService, private operator: OperatorService) {
|
||||||
super();
|
super();
|
||||||
websocketService.getOberservable<any>('notify').subscribe(notify => {
|
|
||||||
this.receive(notify);
|
websocketService.getOberservable<NotifyResponse<any>>('notify').subscribe(notify => {
|
||||||
|
notify.sendByThisUser = notify.senderUserId === (this.operator.user ? this.operator.user.id : 0);
|
||||||
|
this.notifySubject.next(notify);
|
||||||
|
if (this.messageSubjects[notify.name]) {
|
||||||
|
this.messageSubjects[notify.name].next(notify);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement this
|
/**
|
||||||
private receive(notify: NotifyFormat): void {
|
* Sents a notify message to all users (so all clients that are online).
|
||||||
console.log('recv', notify);
|
* @param name The name of the notify message
|
||||||
// TODO: Use a Subject, so one can subscribe and get notifies.
|
* @param content The payload to send
|
||||||
|
*/
|
||||||
|
public sendToAllUsers<T>(name: string, content: T): void {
|
||||||
|
this.send(name, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this api better: e.g. send(data, users?, projectors?, channel?, ...)
|
|
||||||
/**
|
/**
|
||||||
* Sents a notify object to the server
|
* Sends a notify message to all open clients with the given users logged in.
|
||||||
* @param notify the notify objects
|
* @param name The name of th enotify message
|
||||||
|
* @param content The payload to send.
|
||||||
|
* @param users Multiple user ids.
|
||||||
*/
|
*/
|
||||||
public send(notify: NotifyFormat): void {
|
public sendToUsers<T>(name: string, content: T, ...users: number[]): void {
|
||||||
|
this.send(name, content, users);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a notify message to all given channels.
|
||||||
|
* @param name The name of th enotify message
|
||||||
|
* @param content The payload to send.
|
||||||
|
* @param channels Multiple channels to send this message to.
|
||||||
|
*/
|
||||||
|
public sendToChannels<T>(name: string, content: T, ...channles: string[]): void {
|
||||||
|
this.send(name, content, null, channles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General send function for notify messages.
|
||||||
|
* @param name The name of the notify message
|
||||||
|
* @param content The payload to send.
|
||||||
|
* @param users Either an array of IDs or `true` meaning of sending this message to all online users clients.
|
||||||
|
* @param channels An array of channels to send this message to.
|
||||||
|
*/
|
||||||
|
public send<T>(name: string, content: T, users?: number[] | boolean, channels?: string[]): void {
|
||||||
|
const notify: NotifyRequest<T> = {
|
||||||
|
name: name,
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
if (typeof users === 'boolean' && users !== true) {
|
||||||
|
throw new Error('You just can give true as a boolean to send this message to all users.');
|
||||||
|
}
|
||||||
|
if (users !== null) {
|
||||||
|
notify.users = users;
|
||||||
|
}
|
||||||
|
if (channels !== null) {
|
||||||
|
notify.replyChannels = channels;
|
||||||
|
}
|
||||||
this.websocketService.send('notify', notify);
|
this.websocketService.send('notify', notify);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a general observalbe of all notify messages.
|
||||||
|
*/
|
||||||
|
public getObservable(): Observable<NotifyResponse<any>> {
|
||||||
|
return this.notifySubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable for a specific type of messages.
|
||||||
|
* @param name The name of all messages to observe.
|
||||||
|
*/
|
||||||
|
public getMessageObservable<T>(name: string): Observable<NotifyResponse<T>> {
|
||||||
|
if (!this.messageSubjects[name]) {
|
||||||
|
this.messageSubjects[name] = new Subject<NotifyResponse<any>>();
|
||||||
|
}
|
||||||
|
return this.messageSubjects[name].asObservable() as Observable<NotifyResponse<T>>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,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.
|
||||||
@ -176,7 +177,9 @@ import { LogoComponent } from './components/logo/logo.component';
|
|||||||
SortingTreeComponent,
|
SortingTreeComponent,
|
||||||
TreeModule,
|
TreeModule,
|
||||||
OsSortFilterBarComponent,
|
OsSortFilterBarComponent,
|
||||||
LogoComponent
|
LogoComponent,
|
||||||
|
CopyrightSignComponent,
|
||||||
|
C4DialogComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -194,7 +197,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 },
|
||||||
@ -204,6 +209,6 @@ import { LogoComponent } from './components/logo/logo.component';
|
|||||||
OsSortFilterBarComponent,
|
OsSortFilterBarComponent,
|
||||||
OsSortBottomSheetComponent
|
OsSortBottomSheetComponent
|
||||||
],
|
],
|
||||||
entryComponents: [OsSortBottomSheetComponent]
|
entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
|
||||||
})
|
})
|
||||||
export class SharedModule {}
|
export class SharedModule {}
|
||||||
|
@ -10,8 +10,7 @@ import { DataStoreService } from '../../../../core/services/data-store.service';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-start',
|
selector: 'os-start',
|
||||||
templateUrl: './start.component.html',
|
templateUrl: './start.component.html'
|
||||||
styleUrls: ['./start.component.css']
|
|
||||||
})
|
})
|
||||||
export class StartComponent extends BaseComponent implements OnInit {
|
export class StartComponent extends BaseComponent implements OnInit {
|
||||||
public welcomeTitle: string;
|
public welcomeTitle: string;
|
||||||
|
@ -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> 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"
|
||||||
|
@ -17,18 +17,32 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
|||||||
identifier = "notify"
|
identifier = "notify"
|
||||||
schema = {
|
schema = {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "Notify elements.",
|
"title": "Notify element.",
|
||||||
"description": "Elements that one client can send to one or many other clients.",
|
"description": "Element that one client can send to one or many other clients.",
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"properties": {
|
||||||
"type": "object",
|
"name": {"description": "The name of the notify message", "type": "string"},
|
||||||
"properties": {
|
"content": {"description": "The actual content of this message."},
|
||||||
"projectors": {"type": "array", "items": {"type": "integer"}},
|
"reply_channels": {
|
||||||
"reply_channels": {"type": "array", "items": {"type": "string"}},
|
"description": "A list of channels to send this message to.",
|
||||||
"users": {"type": "array", "items": {"type": "integer"}},
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "A list of user ids to send this message to.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "integer"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This flag indicates, that this message should be send to all users.",
|
||||||
|
"enum": [True],
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"minItems": 1,
|
"required": ["name", "content"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def receive_content(
|
async def receive_content(
|
||||||
@ -39,7 +53,7 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
|||||||
{
|
{
|
||||||
"type": "send_notify",
|
"type": "send_notify",
|
||||||
"incomming": content,
|
"incomming": content,
|
||||||
"senderReplyChannelName": consumer.channel_name,
|
"senderChannelName": consumer.channel_name,
|
||||||
"senderUserId": consumer.scope["user"]["id"],
|
"senderUserId": consumer.scope["user"]["id"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -70,26 +70,21 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
|
|||||||
Send a notify message to the user.
|
Send a notify message to the user.
|
||||||
"""
|
"""
|
||||||
user_id = self.scope["user"]["id"]
|
user_id = self.scope["user"]["id"]
|
||||||
|
item = event["incomming"]
|
||||||
|
|
||||||
out = []
|
users = item.get("users")
|
||||||
for item in event["incomming"]:
|
reply_channels = item.get("replyChannels")
|
||||||
users = item.get("users")
|
if (
|
||||||
reply_channels = item.get("replyChannels")
|
(isinstance(users, bool) and users)
|
||||||
if (
|
or (isinstance(users, list) and user_id in users)
|
||||||
(isinstance(users, list) and user_id in users)
|
or (
|
||||||
or (
|
isinstance(reply_channels, list) and self.channel_name in reply_channels
|
||||||
isinstance(reply_channels, list)
|
)
|
||||||
and self.channel_name in reply_channels
|
or (users is None and reply_channels is None)
|
||||||
)
|
):
|
||||||
or users is None
|
item["senderChannelName"] = event["senderChannelName"]
|
||||||
and reply_channels is None
|
item["senderUserId"] = event["senderUserId"]
|
||||||
):
|
await self.send_json(type="notify", content=item)
|
||||||
item["senderReplyChannelName"] = event.get("senderReplyChannelName")
|
|
||||||
item["senderUserId"] = event.get("senderUserId")
|
|
||||||
out.append(item)
|
|
||||||
|
|
||||||
if out:
|
|
||||||
await self.send_json(type="notify", content=out)
|
|
||||||
|
|
||||||
async def send_data(self, event: Dict[str, Any]) -> None:
|
async def send_data(self, event: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -248,18 +248,18 @@ async def test_send_notify(communicator, set_config):
|
|||||||
await communicator.send_json_to(
|
await communicator.send_json_to(
|
||||||
{
|
{
|
||||||
"type": "notify",
|
"type": "notify",
|
||||||
"content": [{"testmessage": "foobar, what else."}],
|
"content": {"content": "foobar, what else.", "name": "message_name"},
|
||||||
"id": "test",
|
"id": "test",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await communicator.receive_json_from()
|
response = await communicator.receive_json_from()
|
||||||
|
|
||||||
content = response["content"]
|
content = response["content"]
|
||||||
assert isinstance(content, list)
|
assert isinstance(content, dict)
|
||||||
assert len(content) == 1
|
assert content["content"] == "foobar, what else."
|
||||||
assert content[0]["testmessage"] == "foobar, what else."
|
assert content["name"] == "message_name"
|
||||||
assert "senderReplyChannelName" in content[0]
|
assert "senderChannelName" in content
|
||||||
assert content[0]["senderUserId"] == 0
|
assert content["senderUserId"] == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -6,7 +6,11 @@ from openslides.utils.websocket import schema
|
|||||||
|
|
||||||
def test_notify_schema_validation():
|
def test_notify_schema_validation():
|
||||||
# This raises a validaten error if it fails
|
# This raises a validaten error if it fails
|
||||||
message = {"id": "test-message", "type": "notify", "content": [{"users": [5]}]}
|
message = {
|
||||||
|
"id": "test-message",
|
||||||
|
"type": "notify",
|
||||||
|
"content": {"name": "testname", "content": ["some content"]},
|
||||||
|
}
|
||||||
jsonschema.validate(message, schema)
|
jsonschema.validate(message, schema)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user