Skip to content

Commit

Permalink
Merge pull request #910 from tsg-ut/ricochet-robots-thread
Browse files Browse the repository at this point in the history
ハイパーロボットをスレッド化
  • Loading branch information
hakatashi authored Jul 24, 2024
2 parents 1d9750a + 1a3fad8 commit 5696487
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 20 deletions.
30 changes: 26 additions & 4 deletions atequiz/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class AteQuiz {
replaceKeys: { correctAnswerer: string } = { correctAnswerer: '[[!user]]' };
mutex: Mutex;
postOption: WebAPICallOptions;
threadTsDeferred: Deferred<string> = new Deferred();

judge(answer: string, _user: string): boolean {
return this.problem.correctAnswers.some(
(correctAnswer) => answer === correctAnswer
Expand All @@ -86,7 +88,7 @@ export class AteQuiz {
* @param {any} post the post judged as correct
* @returns a object that specifies the parameters of a solved message
*/
solvedMessageGen(post: any): ChatPostMessageArguments {
solvedMessageGen(post: any): ChatPostMessageArguments | Promise<ChatPostMessageArguments> {
const message = Object.assign({}, this.problem.solvedMessage);
message.text = message.text.replaceAll(
this.replaceKeys.correctAnswerer,
Expand All @@ -95,6 +97,13 @@ export class AteQuiz {
return message;
}

answerMessageGen(_post?: any): ChatPostMessageArguments | null | Promise<ChatPostMessageArguments | null> {
if (!this.problem.answerMessage) {
return null;
}
return this.problem.answerMessage;
}

incorrectMessageGen(post: any): ChatPostMessageArguments | null {
if (!this.problem.incorrectMessage) {
return null;
Expand Down Expand Up @@ -126,6 +135,16 @@ export class AteQuiz {
this.mutex = new Mutex();
}

async repostProblemMessage() {
const threadTs = await this.threadTsDeferred.promise;
return this.slack.chat.postMessage({
...this.problem.problemMessage,
...this.postOption,
thread_ts: threadTs,
reply_broadcast: true,
});
}

/**
* Start AteQuiz.
* @returns A promise of AteQuizResult that becomes resolved when the quiz ends.
Expand Down Expand Up @@ -171,6 +190,7 @@ export class AteQuiz {
Object.assign({}, this.problem.unsolvedMessage, { thread_ts })
);

const answerMessage = await this.answerMessageGen();
if (this.problem.answerMessage) {
await postMessage(
Object.assign({}, this.problem.answerMessage, { thread_ts })
Expand All @@ -196,12 +216,13 @@ export class AteQuiz {
clearInterval(tickTimer);

await postMessage(
Object.assign({}, this.solvedMessageGen(message), { thread_ts })
Object.assign({}, await this.solvedMessageGen(message), { thread_ts })
);

if (this.problem.answerMessage) {
const answerMessage = await this.answerMessageGen(message);
if (answerMessage) {
await postMessage(
Object.assign({}, this.problem.answerMessage, { thread_ts })
Object.assign({}, answerMessage, { thread_ts })
);
}

Expand Down Expand Up @@ -232,6 +253,7 @@ export class AteQuiz {
// Listeners should be added before postMessage is called.
const response = await postMessage(this.problem.problemMessage);
const thread_ts = response?.message?.thread_ts ?? response.ts;
this.threadTsDeferred.resolve(thread_ts);
assert(typeof thread_ts === 'string');

if (this.problem.immediateMessage) {
Expand Down
301 changes: 301 additions & 0 deletions ricochet-robots/SinglePlayRicochetRobot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import { AteQuiz, AteQuizProblem } from '../atequiz';
import { round } from 'lodash';
import cloudinary from 'cloudinary';
import type { Message } from '@slack/web-api/dist/response/ConversationsHistoryResponse';
import { SlackInterface } from '../lib/slack';
import * as image from './image';
import * as board from './board';
import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api';
import { unlock } from '../achievements';
import assert from 'assert';
import { stripIndent } from 'common-tags';

interface SingleRicochetRobotConstructor {
slackClients: SlackInterface,
channel: string,
depth: number,
size: {h: number, w: number},
numOfWalls: number,
threadTs: string,
originalUser: string,
}

export default class SinglePlayRicochetRobot extends AteQuiz {
startTime: number;
endTime: number;
boardData: board.Board;
answer: board.Move[];
originalUser: string;

constructor(
slackClients: SlackInterface,
problem: AteQuizProblem,
boardData: board.Board,
answer: board.Move[],
originalUser: string,
) {
super(slackClients, problem, {
username: 'hyperrobot',
icon_emoji: ':robot_face:',
});
this.boardData = boardData;
this.answer = answer;
this.originalUser = originalUser;
}

static async init({slackClients, channel, depth, size, numOfWalls, threadTs, originalUser}: SingleRicochetRobotConstructor) {
const [boardData, answer] = await board.getBoard({depth, size, numOfWalls});
const imageData = await image.upload(boardData);
const quizText = `${answer.length}手詰めです`;

const thumbnailUrl = cloudinary.v2.url(`${imageData.public_id}.jpg`, {
private_cdn: false,
secure: true,
secure_distribution: 'res.cloudinary.com',
background: 'white',
width: 400,
height: 400,
crop: 'pad',
});

const singleRicochetRobot = new SinglePlayRicochetRobot(slackClients, {
problemMessage: {
channel,
text: quizText,
blocks: [
{
type: 'section',
text: {
type: 'plain_text',
text: quizText,
},
accessory: {
type: 'image',
image_url: thumbnailUrl,
alt_text: 'ハイパーロボット',
},
},
],
thread_ts: threadTs,
reply_broadcast: true,
},
hintMessages: [],
immediateMessage: {
channel,
text: 'このスレッドに回答してね!',
blocks: [
{
type: 'section',
text: {
type: 'plain_text',
text: 'このスレッドに回答してね!',
},
},
{
type: 'image',
image_url: imageData.secure_url,
alt_text: 'ハイパーロボット',
},
],
},
solvedMessage: {
channel,
text: '',
reply_broadcast: true,
},
unsolvedMessage: {
channel,
text: '',
reply_broadcast: true,
},
correctAnswers: [],
}, boardData, answer, originalUser);

return singleRicochetRobot;
}

waitSecGen() {
return Infinity;
}

start() {
this.startTime = Date.now();
return super.start();
}

postMessage(message: Partial<ChatPostMessageArguments>) {
return this.slack.chat.postMessage({
...message,
channel: this.problem.problemMessage.channel,
thread_ts: this.problem.problemMessage.thread_ts,
username: 'hyperrobot',
icon_emoji: ':robot_face:',
});
}

async postNotSolvedMessage(board: board.Board) {
const message = '解けてませんね:thinking_face:';
const imageData = await image.upload(board);
await this.postMessage({
text: message,
blocks: [
{
type: 'section',
text: {
type: 'plain_text',
text: '解けてませんね:thinking_face:',
},
},
{
type: 'image',
image_url: imageData.secure_url,
alt_text: '結果',
},
],
});
}

judge(answer: string) {
if (board.iscommand(answer)) {
const command = board.str2command(answer);
if (!command.isMADE && command.moves.length > this.answer.length) {
this.postMessage({
text: stripIndent`
この問題は${this.answer.length}手詰めだよ。その手は${command.moves.length}手かかってるよ:thinking_face:
もし最短でなくてもよいなら、手順のあとに「まで」をつけてね。
`,
});
return false;
}
const playerBoard = this.boardData.clone();
playerBoard.movecommand(command.moves);
if (playerBoard.iscleared()) {
this.endTime = Date.now();
return true;
}
this.postNotSolvedMessage(playerBoard);
return false;
}
return false;
}

async solvedMessageGen(message: Message) {
const answer = message.text as string;
assert(board.iscommand(answer), 'answer is not command');

const command = board.str2command(answer);
const playerBoard = this.boardData.clone();
playerBoard.movecommand(command.moves);
assert(playerBoard.iscleared(), 'playerBoard is not cleared');

let comment = '正解です!:tada:';
if (command.moves.length === this.answer.length) {
comment += 'さらに最短勝利です!:waiwai:';
}

return {
channel: this.problem.solvedMessage.channel,
text: comment,
reply_broadcast: true,
};
}

async answerMessageGen(message: Message) {
const answer = message.text as string;
assert(board.iscommand(answer), 'answer is not command');

const command = board.str2command(answer);
const playerBoard = this.boardData.clone();
playerBoard.movecommand(command.moves);
assert(playerBoard.iscleared(), 'playerBoard is not cleared');

const blocks: KnownBlock[] = [];

const passedMs = (this.endTime - this.startTime) / 1000;

let comment = '';
if (command.moves.length < this.answer.length){
comment += 'というか:bug:ってますね...????? :hakatashi:に連絡してください。';
await unlock(message.user, 'ricochet-robots-debugger');
}

const playerBoardImageData = await image.upload(playerBoard);

if (comment.length > 0) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: comment,
},
});
}

blocks.push({
type: 'image',
image_url: playerBoardImageData.secure_url,
alt_text: 'プレイヤーの回答',
});

const botcomment = (command.moves.length > this.answer.length) ?
`実は${this.answer.length}手でたどり着けるんです。\n${board.logstringfy(this.answer)}`:
`僕の見つけた手順です。\n${board.logstringfy(this.answer)}`;

const botBoard = this.boardData.clone();
botBoard.movecommand(this.answer);
const botBoardImageData = await image.upload(botBoard);

blocks.push(
{
type: 'section',
text: {
type: 'mrkdwn',
text: botcomment,
},
},
{
type: 'image',
image_url: botBoardImageData.url,
alt_text: '想定回答',
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `経過時間: ${round(passedMs, 3)} 秒`,
},
}
);

if(command.moves.length <= this.answer.length){
await unlock(message.user, 'ricochet-robots-clear-shortest');
if (this.answer.length >= 10) {
await unlock(message.user, 'ricochet-robots-clear-shortest-over10');
}
if (this.answer.length >= 15) {
await unlock(message.user, 'ricochet-robots-clear-shortest-over15');
}
if (this.answer.length >= 20) {
await unlock(message.user, 'ricochet-robots-clear-shortest-over20');
}
}
await unlock(message.user, 'ricochet-robots-clear');
if (this.answer.length >= 8 && command.moves.length <= this.answer.length) {
if (passedMs <= this.answer.length * 10 * 1000) {
await unlock(message.user, 'ricochet-robots-clear-in-10sec-per-move-over8');
}
if (passedMs <= this.answer.length * 5 * 1000) {
await unlock(message.user, 'ricochet-robots-clear-in-5sec-per-move-over8');
}
if (passedMs <= this.answer.length * 1 * 1000) {
await unlock(message.user, 'ricochet-robots-clear-in-1sec-per-move-over8');
}
}

return {
channel: this.problem.solvedMessage.channel,
text: comment,
blocks,
};
}
}
2 changes: 1 addition & 1 deletion ricochet-robots/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const logstringfy = (log: Move[]) => {
return log.map(v => colournames[v.c]+directionnames[v.d]).join();
};

export const getBoard = async (boardspec: BoardSpec) => {
export const getBoard = async (boardspec: BoardSpec): Promise<[Board, Move[]]> => {
const data = await rust_proxy.get_data(boardspec);
let lines = data.split('\n').filter((line) => line);
lines = lines.slice(lines.length-4,lines.length);
Expand Down
Loading

0 comments on commit 5696487

Please sign in to comment.