Compare commits

...

38 commits

Author SHA1 Message Date
Marie
610f208c57
fix: don't let user invite themself to reversi game 2024-01-25 14:23:51 +01:00
Marie
913dd581ef
merge: upstream 2024-01-25 14:21:42 +01:00
Marie
92ee0a5863
fix: notes/versions not checking visibility
Issue reported by fEmber
2024-01-25 14:17:57 +01:00
Marie
b68c358688
fix: reversi allowing you to search globally instead of just locally
Misskey used a local option in the select section but os nor userselectdialog supported this.
2024-01-25 14:11:41 +01:00
syuilo
5342692b1e 2024.2.0-beta.7 2024-01-24 20:31:05 +09:00
syuilo
ef8eaf8e89
New Crowdin updates (#13080)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)
2024-01-24 20:29:05 +09:00
syuilo
4553d6426b Revert "Create deploy-test-environment.yml (#13079)"
This reverts commit 4de14fb5cf.
2024-01-24 17:31:34 +09:00
syuilo
b33cfc2876 test 2024-01-24 17:13:58 +09:00
Srgr0
4de14fb5cf
Create deploy-test-environment.yml (#13079) 2024-01-24 17:11:52 +09:00
syuilo
60156a40b2 fix(reversi/backend): refactor and fixes 2024-01-24 16:44:12 +09:00
syuilo
5719a929ad enhance(reversi): 変則なしマッチングを可能に 2024-01-24 16:37:06 +09:00
syuilo
2b6bf074c6 2024.2.0-beta.6 2024-01-24 15:20:12 +09:00
syuilo
37d87854c2
New translations ja-jp.yml (Japanese, Kansai) (#13077) 2024-01-24 15:19:14 +09:00
syuilo
d27b3525cd enhance(reversi): improve matching system 2024-01-24 15:18:50 +09:00
syuilo
7beb4ed131 fix(frontend/reversi): fix game preview 2024-01-24 14:52:19 +09:00
かっこかり
177c35e321
fix(frontend/pizzax): オブジェクトにnullがある場合に正しくマージされないのを修正 (#13073)
* fix(frontend/pizzax): オブジェクトにnullがある場合に正しくマージされない

* fix types

* マージを内製
2024-01-24 14:45:27 +09:00
syuilo
ca9be872a8 2024.2.0-beta.5 2024-01-24 13:55:57 +09:00
syuilo
a97d4fa4ef fix(reversi): wait redis operation to improve stability 2024-01-24 13:53:55 +09:00
syuilo
908e0f3b8b perf(reversi): set expire matchSpecific and matchAny 2024-01-24 13:51:16 +09:00
syuilo
b68446b289 enhance(reversi): tweak MATCHING_TIMEOUT_MS 2024-01-24 13:32:08 +09:00
syuilo
608e7c1546 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-01-24 13:17:36 +09:00
syuilo
a3ba315dc6 enhance(reversi): improve game setting flow 2024-01-24 13:17:34 +09:00
syuilo
df5f14ca7a
New translations ja-jp.yml (Japanese, Kansai) (#13074) 2024-01-24 10:52:47 +09:00
syuilo
d060bb44e1 enhance(reversi): improve stability 2024-01-24 10:51:49 +09:00
syuilo
645f5e8633 enhance(reversi): 開始時に対局をシェアできるように 2024-01-24 10:36:02 +09:00
syuilo
547be1973d fix of 65557d5f27 2024-01-24 10:35:44 +09:00
syuilo
65557d5f27 enhance(reversi): more robust matching process 2024-01-24 10:16:05 +09:00
syuilo
cc420c245f enhance(reversi): 準備中の自分の対局も一覧に表示するように 2024-01-24 09:41:22 +09:00
syuilo
443d1b2f5c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-01-24 09:31:06 +09:00
syuilo
1f8d275094 🎨 2024-01-24 09:30:38 +09:00
かっこかり
2efcb27043
fix(frontend/HorizontalSwipe): スワイプ・UIアニメーションが無効の際はトランジションを行わないように (#13076)
* fix(frontend/HorizontalSwipe): アニメーションを減らすが考慮されるように

* fix

* fix

* revert unused change

* fix
2024-01-24 09:22:51 +09:00
syuilo
298bc34eaf 2024.2.0-beta.4 2024-01-23 10:54:04 +09:00
syuilo
62f6f6af02
New Crowdin updates (#13061)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)
2024-01-23 10:53:47 +09:00
syuilo
e8ba0b3f54 enhance(reversi): improve desync handling 2024-01-23 10:51:59 +09:00
syuilo
f48f7149f8 🎨 2024-01-23 09:43:54 +09:00
まっちゃとーにゅ
d2ccce6366
fix(build): スクリプトの名前の変更漏れ (#13068)
* fix(build): スクリプトの名前の変更漏れ

* 漏れの漏れ
2024-01-23 07:57:56 +09:00
tamaina
af2d81a990
perf: (productionの)dependenciesから@typesを削除、reversi/bubble-gameをesbuildにする (#13067)
* perf: (productionの)dependenciesから@typesを削除、reversi/bubble-gameをesbuildにする

* fix

* fix
2024-01-23 06:36:44 +09:00
ikasoba
58ac8bc8e9
修正できたかも (#13066) 2024-01-23 06:35:15 +09:00
44 changed files with 842 additions and 204 deletions

View file

@ -88,8 +88,8 @@ jobs:
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- run: pnpm --filter misskey-js run build - run: pnpm --filter misskey-js run build
if: ${{ matrix.workspace == 'backend' }} if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-reversi run build - run: pnpm --filter misskey-reversi run build:tsc
if: ${{ matrix.workspace == 'backend' }} if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-bubble-game run build - run: pnpm --filter misskey-bubble-game run build:tsc
if: ${{ matrix.workspace == 'backend' }} if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter ${{ matrix.workspace }} run typecheck - run: pnpm --filter ${{ matrix.workspace }} run typecheck

20
locales/index.d.ts vendored
View file

@ -9846,6 +9846,26 @@ export interface Locale extends ILocale {
* *
*/ */
"gameCanceled": string; "gameCanceled": string;
/**
* 稿
*/
"shareToTlTheGameWhenStart": string;
/**
* #MisskeyReversi
*/
"iStartedAGame": string;
/**
*
*/
"opponentHasSettingsChanged": string;
/**
* ()
*/
"allowIrregularRules": string;
/**
*
*/
"disallowIrregularRules": string;
}; };
"_offlineScreen": { "_offlineScreen": {
/** /**

View file

@ -2622,6 +2622,11 @@ _reversi:
freeMatch: "フリーマッチ" freeMatch: "フリーマッチ"
lookingForPlayer: "対戦相手を探しています" lookingForPlayer: "対戦相手を探しています"
gameCanceled: "対局がキャンセルされました" gameCanceled: "対局がキャンセルされました"
shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿"
iStartedAGame: "対局を開始しました! #MisskeyReversi"
opponentHasSettingsChanged: "相手が設定を変更しました"
allowIrregularRules: "変則許可 (完全フリー)"
disallowIrregularRules: "変則なし"
_offlineScreen: _offlineScreen:
title: "オフライン - サーバーに接続できません" title: "オフライン - サーバーに接続できません"

View file

@ -382,8 +382,11 @@ hcaptcha: "hCaptchaキャプチャ"
enableHcaptcha: "hCaptchaキャプチャをつけとく" enableHcaptcha: "hCaptchaキャプチャをつけとく"
hcaptchaSiteKey: "サイトキー" hcaptchaSiteKey: "サイトキー"
hcaptchaSecretKey: "シークレットキー" hcaptchaSecretKey: "シークレットキー"
mcaptcha: "mCaptcha"
enableMcaptcha: "hCaptchaキャプチャをつけとく"
mcaptchaSiteKey: "サイトキー" mcaptchaSiteKey: "サイトキー"
mcaptchaSecretKey: "シークレットキー" mcaptchaSecretKey: "シークレットキー"
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHAリキャプチャを有効にする" enableRecaptcha: "reCAPTCHAリキャプチャを有効にする"
recaptchaSiteKey: "サイトキー" recaptchaSiteKey: "サイトキー"
@ -631,6 +634,7 @@ medium: "中"
small: "小" small: "小"
generateAccessToken: "アクセストークンの発行" generateAccessToken: "アクセストークンの発行"
permission: "権限" permission: "権限"
adminPermission: "管理者権限"
enableAll: "全部使えるようにする" enableAll: "全部使えるようにする"
disableAll: "全部使えへんようにする" disableAll: "全部使えへんようにする"
tokenRequested: "アカウントへのアクセス許してやったらどうや" tokenRequested: "アカウントへのアクセス許してやったらどうや"
@ -1057,6 +1061,8 @@ limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく
noteIdOrUrl: "ートIDかURL" noteIdOrUrl: "ートIDかURL"
video: "動画" video: "動画"
videos: "動画" videos: "動画"
audio: "音声"
audioFiles: "音声"
dataSaver: "データケチケチ" dataSaver: "データケチケチ"
accountMigration: "アカウントのお引っ越し" accountMigration: "アカウントのお引っ越し"
accountMoved: "このユーザーはさらのアカウントに引っ越したで:" accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
@ -1189,7 +1195,25 @@ seasonalScreenEffect: "季節にあった画面の動き"
decorate: "デコる" decorate: "デコる"
addMfmFunction: "装飾つける" addMfmFunction: "装飾つける"
enableQuickAddMfmFunction: "ややこしいMFMのピッカーを出す" enableQuickAddMfmFunction: "ややこしいMFMのピッカーを出す"
bubbleGame: "バブルゲーム"
sfx: "効果音"
soundWillBePlayed: "サウンドが再生されるで"
showReplay: "リプレイ見る"
replay: "リプレイ"
replaying: "リプレイ中"
ranking: "ランキング"
lastNDays: "直近{n}日" lastNDays: "直近{n}日"
backToTitle: "タイトルへ"
hemisphere: "住んでる地域"
withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える"
_bubbleGame:
howToPlay: "遊び方"
_howToPlay:
section1: "位置を調整してハコにモノを落とすで。"
section2: "同じもんがくっついたら別のやつになって、スコアがもらえるで。"
section3: "モノがハコからあふれたらゲームオーバーや。ハコからあふれんようにしながらモノを融合させてハイスコアを目指しいや!"
_announcement: _announcement:
forExistingUsers: "もうおるユーザーのみ" forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。" forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。"
@ -1560,6 +1584,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "Sharkeyひよっこ講座 修了証" title: "Sharkeyひよっこ講座 修了証"
description: "チュートリアル全部やった" description: "チュートリアル全部やった"
_bubbleGameExplodingHead:
title: "🤯"
description: "バブルゲームで最も大きいモノを出した"
_bubbleGameDoubleExplodingHead:
title: "ダブル🤯"
description: "バブルゲームで最も大きいモを2つ同時に出した"
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
edit: "ロールの編集" edit: "ロールの編集"
@ -2412,6 +2443,51 @@ _dataSaver:
_code: _code:
title: "コードハイライト" title: "コードハイライト"
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
_hemisphere:
N: "北半球"
S: "南半球"
caption: "一部のクライアント設定で、季節を判定するのに使用するで。"
_reversi: _reversi:
reversi: "リバーシ"
gameSettings: "対局の設定"
chooseBoard: "ボードを選択"
blackOrWhite: "先行/後攻"
blackIs: "{name}が黒(先行)"
rules: "ルール"
thisGameIsStartedSoon: "対局、そろそろ開始されるで。"
waitingForOther: "相手の準備が完了するのを待ってんで。"
waitingForMe: "あんさんの準備が完了すんのを待ってんで"
waitingBoth: "準備してなー"
ready: "準備完了"
cancelReady: "準備を再開"
opponentTurn: "相手のターンやで"
myTurn: "あんさんのターンや"
turnOf: "{name}のターンやで"
pastTurnOf: "{name}のターン"
surrender: "投了"
surrendered: "投了により"
timeout: "時間切れ"
drawn: "引き分け"
won: "{name}の勝ち"
black: "黒"
white: "白"
total: "合計" total: "合計"
turnCount: "{count}ターン目"
myGames: "自分の対局"
allGames: "みんなの対局"
ended: "終了"
playing: "対局中"
isLlotheo: "石の少ない方が勝ち(ロセオ)"
loopedMap: "ループマップ"
canPutEverywhere: "どこでも置けるモード"
timeLimitForEachTurn: "1ターンの時間制限"
freeMatch: "フリーマッチ"
lookingForPlayer: "対戦相手を探してるで"
gameCanceled: "対局がキャンセルされたわ"
shareToTlTheGameWhenStart: "初めの時に対局をタイムラインに投稿するで"
iStartedAGame: "対局し始めたで! #MisskeyReversi"
opponentHasSettingsChanged: "相手が設定変えたで"
_offlineScreen:
title: "オフライン - サーバーに接続できひんで"
header: "サーバーに接続できへんわ"

View file

@ -380,9 +380,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha 활성화" enableHcaptcha: "hCaptcha 활성화"
hcaptchaSiteKey: "사이트 키" hcaptchaSiteKey: "사이트 키"
hcaptchaSecretKey: "시크릿 키" hcaptchaSecretKey: "시크릿 키"
mcaptcha: "mCaptcha"
enableMcaptcha: "mCaptcha 활성화" enableMcaptcha: "mCaptcha 활성화"
mcaptchaSiteKey: "사이트 키" mcaptchaSiteKey: "사이트 키"
mcaptchaSecretKey: "시크릿 키" mcaptchaSecretKey: "시크릿 키"
mcaptchaInstanceUrl: "mCaptcha 인스턴스 URL"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA 활성화" enableRecaptcha: "reCAPTCHA 활성화"
recaptchaSiteKey: "사이트 키" recaptchaSiteKey: "사이트 키"
@ -630,6 +632,7 @@ medium: "보통"
small: "작게" small: "작게"
generateAccessToken: "액세스 토큰 생성" generateAccessToken: "액세스 토큰 생성"
permission: "권한" permission: "권한"
adminPermission: "관리자 권한"
enableAll: "전체 선택" enableAll: "전체 선택"
disableAll: "전체 해제" disableAll: "전체 해제"
tokenRequested: "계정 접근 허용" tokenRequested: "계정 접근 허용"
@ -673,6 +676,7 @@ useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용됩니
other: "기타" other: "기타"
regenerateLoginToken: "로그인 토큰을 재생성" regenerateLoginToken: "로그인 토큰을 재생성"
regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다." regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다."
theKeywordWhenSearchingForCustomEmoji: "맞춤 이모티콘을 검색할 때 키워드가 됩니다."
setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다." setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다."
fileIdOrUrl: "파일 ID 또는 URL" fileIdOrUrl: "파일 ID 또는 URL"
behavior: "동작" behavior: "동작"
@ -1055,6 +1059,8 @@ limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하
noteIdOrUrl: "노트 ID 및 URL" noteIdOrUrl: "노트 ID 및 URL"
video: "동영상" video: "동영상"
videos: "동영상" videos: "동영상"
audio: "소리"
audioFiles: "소리"
dataSaver: "데이터 절약 모드" dataSaver: "데이터 절약 모드"
accountMigration: "계정 이동" accountMigration: "계정 이동"
accountMoved: "이 사용자는 다음 계정으로 이사했습니다:" accountMoved: "이 사용자는 다음 계정으로 이사했습니다:"
@ -1187,10 +1193,25 @@ seasonalScreenEffect: "계절에 따른 효과 보이기"
decorate: "장식하기" decorate: "장식하기"
addMfmFunction: "장식 추가하기" addMfmFunction: "장식 추가하기"
enableQuickAddMfmFunction: "상급자용 MFM 선택기 표시하기" enableQuickAddMfmFunction: "상급자용 MFM 선택기 표시하기"
bubbleGame: "버블 게임"
sfx: "효과음" sfx: "효과음"
soundWillBePlayed: "소리가 재생됩니다"
showReplay: "리플레이 보기"
replay: "리플레이"
replaying: "리플레이 중"
ranking: "랭킹"
lastNDays: "최근 {n}일" lastNDays: "최근 {n}일"
backToTitle: "타이틀로 가기"
hemisphere: "거주 지역"
withSensitive: "민감한 파일이 포함된 노트 보기"
userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물"
enableHorizontalSwipe: "스와이프하여 탭 전환"
_bubbleGame: _bubbleGame:
howToPlay: "설명" howToPlay: "설명"
_howToPlay:
section1: "위치를 조정하여 상자에 물건을 떨어뜨립니다."
section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻게 됩니다."
section3: "상자에서 물건이 넘치면 게임 오버입니다. 상자에서 물건이 넘치지 않도록 하면서 물건을 융합하여 높은 점수를 획득하세요!"
_announcement: _announcement:
forExistingUsers: "기존 유저에게만 알림" forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
@ -1561,6 +1582,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "Misskey 입문자 과정 수료증" title: "Misskey 입문자 과정 수료증"
description: "튜토리얼을 완료했습니다" description: "튜토리얼을 완료했습니다"
_bubbleGameExplodingHead:
title: "🤯"
description: "버블 게임에서 가장 큰 물건을 내놓았다"
_bubbleGameDoubleExplodingHead:
title: "더블 🤯"
description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다."
flavor: "이 정도만 도시락통에 🤯 🤯 조금만 더"
_role: _role:
new: "새 역할 생성" new: "새 역할 생성"
edit: "역할 수정" edit: "역할 수정"
@ -2413,6 +2441,48 @@ _dataSaver:
_code: _code:
title: "문자열 강조" title: "문자열 강조"
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다." description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
_hemisphere:
N: "북반구"
S: "남반구"
caption: "일부 클라이언트 설정에서 계절을 판단하기 위해 사용합니다."
_reversi: _reversi:
reversi: "리버시"
gameSettings: "대국 설정"
chooseBoard: "보드 선택"
blackOrWhite: "선공/후공"
blackIs: "{name}님이 흑(선공)"
rules: "규칙"
thisGameIsStartedSoon: "대국이 곧 시작됩니다"
waitingForOther: "상대방의 준비가 완료되기를 기다리고 있습니다."
waitingForMe: "당신의 준비가 완료되기를 기다리고 있습니다."
waitingBoth: "준비하세요"
ready: "준비 완료"
cancelReady: "준비 다시 시작"
opponentTurn: "상대의 차례입니다"
myTurn: "당신의 차례입니다"
turnOf: "{name}의 차례입니다"
pastTurnOf: "{name}의 차례"
surrender: "기권"
surrendered: "기권에 의해"
timeout: "시간 초과"
drawn: "무승부"
won: "{name}의 승리"
black: "흑"
white: "백"
total: "합계" total: "합계"
turnCount: "{count}턴 째"
myGames: "내 대국"
allGames: "모두의 대국"
ended: "종료"
playing: "대국 중"
isLlotheo: "돌이 적은 사람이 승리 (로세오)"
loopedMap: "루프 지도"
canPutEverywhere: "어디에도 둘 수 있는 모드"
timeLimitForEachTurn: "1턴의 시간 제한"
freeMatch: "프리매치"
lookingForPlayer: "상대를 찾고 있습니다"
gameCanceled: "대국이 취소되었습니다"
_offlineScreen:
title: "오프라인 - 서버에 접속할 수 없습니다"
header: "서버에 접속할 수 없습니다"

View file

@ -2473,7 +2473,7 @@ _reversi:
turnCount: "{count} 回合" turnCount: "{count} 回合"
myGames: "我的對弈" myGames: "我的對弈"
allGames: "所有對弈" allGames: "所有對弈"
ended: "" ended: "已結束"
playing: "正在對弈" playing: "正在對弈"
isLlotheo: "子較少的一方為勝(顛倒規則)" isLlotheo: "子較少的一方為勝(顛倒規則)"
loopedMap: "循環棋盤" loopedMap: "循環棋盤"
@ -2482,6 +2482,11 @@ _reversi:
freeMatch: "自由對戰" freeMatch: "自由對戰"
lookingForPlayer: "正在搜尋對手" lookingForPlayer: "正在搜尋對手"
gameCanceled: "對弈已被取消" gameCanceled: "對弈已被取消"
shareToTlTheGameWhenStart: "在遊戲開始時將對弈資訊發布到時間軸"
iStartedAGame: "對弈開始了! #MisskeyReversi"
opponentHasSettingsChanged: "對手更改了設定"
allowIrregularRules: "允許異常規則(完全自由)"
disallowIrregularRules: "不允許異常規則"
_offlineScreen: _offlineScreen:
title: "離線-無法連接伺服器" title: "離線-無法連接伺服器"
header: "無法連接伺服器" header: "無法連接伺服器"

View file

@ -1,6 +1,6 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2024.2.0-beta1", "version": "2024.2.0-beta2",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Reversi61706081514499 {
name = 'Reversi61706081514499'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`);
}
}

View file

@ -107,7 +107,6 @@
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"color-convert": "2.0.1", "color-convert": "2.0.1",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"crc-32": "^1.2.2",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify-multer": "^2.0.3", "fastify-multer": "^2.0.3",

View file

@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import CRC32 from 'crc-32';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import * as Reversi from 'misskey-reversi'; import * as Reversi from 'misskey-reversi';
import { IsNull } from 'typeorm'; import { IsNull, LessThan, MoreThan } from 'typeorm';
import type { import type {
MiReversiGame, MiReversiGame,
ReversiGamesRepository, ReversiGamesRepository,
@ -25,7 +24,7 @@ import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
@Injectable() @Injectable()
export class ReversiService implements OnApplicationShutdown, OnModuleInit { export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@ -86,29 +85,53 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: game.map, map: game.map,
bw: game.bw, bw: game.bw,
crc32: game.crc32, crc32: game.crc32,
noIrregularRules: game.noIrregularRules,
} satisfies Partial<MiReversiGame>; } satisfies Partial<MiReversiGame>;
} }
@bindThis @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) { if (targetUser.id === me.id) {
throw new Error('You cannot match yourself.'); throw new Error('You cannot match yourself.');
} }
if (!multiple) {
// 既にマッチしている対局が無いか探す(3分以内)
const games = await this.reversiGamesRepository.find({
where: [
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
],
relations: ['user1', 'user2'],
order: { id: 'DESC' },
});
if (games.length > 0) {
return games[0];
}
}
//#region 相手から既に招待されてないか確認
const invitations = await this.redisClient.zrange( const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${me.id}`, `reversi:matchSpecific:${me.id}`,
Date.now() - MATCHING_TIMEOUT_MS, Date.now() - INVITATION_TIMEOUT_MS,
'+inf', '+inf',
'BYSCORE'); 'BYSCORE');
if (invitations.includes(targetUser.id)) { if (invitations.includes(targetUser.id)) {
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
const game = await this.matched(targetUser.id, me.id); const game = await this.matched(targetUser.id, me.id, {
noIrregularRules: false,
});
return game; return game;
} else { }
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); //#endregion
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX');
await redisPipeline.exec();
this.globalEventService.publishReversiStream(targetUser.id, 'invited', { this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
user: await this.userEntityService.pack(me, targetUser), user: await this.userEntityService.pack(me, targetUser),
@ -116,14 +139,28 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
return null; return null;
} }
}
@bindThis @bindThis
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> { public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> {
if (!multiple) {
// 既にマッチしている対局が無いか探す(3分以内)
const games = await this.reversiGamesRepository.find({
where: [
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
],
relations: ['user1', 'user2'],
order: { id: 'DESC' },
});
if (games.length > 0) {
return games[0];
}
}
//#region まず自分宛ての招待を探す //#region まず自分宛ての招待を探す
const invitations = await this.redisClient.zrange( const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${me.id}`, `reversi:matchSpecific:${me.id}`,
Date.now() - MATCHING_TIMEOUT_MS, Date.now() - INVITATION_TIMEOUT_MS,
'+inf', '+inf',
'BYSCORE'); 'BYSCORE');
@ -131,7 +168,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const invitorId = invitations[Math.floor(Math.random() * invitations.length)]; const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId); await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
const game = await this.matched(invitorId, me.id); const game = await this.matched(invitorId, me.id, {
noIrregularRules: false,
});
return game; return game;
} }
@ -139,23 +178,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const matchings = await this.redisClient.zrange( const matchings = await this.redisClient.zrange(
'reversi:matchAny', 'reversi:matchAny',
Date.now() - MATCHING_TIMEOUT_MS, 0,
'+inf', 2, // 自分自身のIDが入っている場合もあるので2つ取得
'BYSCORE'); 'REV');
const userIds = matchings.filter(id => id !== me.id); const items = matchings.filter(id => !id.startsWith(me.id));
if (userIds.length > 0) { if (items.length > 0) {
// pick random const [matchedUserId, option] = items[0].split(':');
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); await this.redisClient.zrem('reversi:matchAny',
me.id,
matchedUserId,
me.id + ':noIrregularRules',
matchedUserId + ':noIrregularRules');
const game = await this.matched(matchedUserId, me.id); const game = await this.matched(matchedUserId, me.id, {
noIrregularRules: options.noIrregularRules || option === 'noIrregularRules',
});
return game; return game;
} else { } else {
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id); const redisPipeline = this.redisClient.pipeline();
if (options.noIrregularRules) {
redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules');
} else {
redisPipeline.zadd('reversi:matchAny', Date.now(), me.id);
}
redisPipeline.expire('reversi:matchAny', 15, 'NX');
await redisPipeline.exec();
return null; return null;
} }
} }
@ -167,7 +218,15 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis @bindThis
public async matchAnyUserCancel(user: MiUser) { public async matchAnyUserCancel(user: MiUser) {
await this.redisClient.zrem('reversi:matchAny', user.id); await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules');
}
@bindThis
public async cleanOutdatedGames() {
await this.reversiGamesRepository.delete({
id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
isStarted: false,
});
} }
@bindThis @bindThis
@ -221,7 +280,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> { private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
const game = await this.reversiGamesRepository.insert({ const game = await this.reversiGamesRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
user1Id: parentId, user1Id: parentId,
@ -234,6 +293,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: Reversi.maps.eighteight.data, map: Reversi.maps.eighteight.data,
bw: 'random', bw: 'random',
isLlotheo: false, isLlotheo: false,
noIrregularRules: options.noIrregularRules,
}).then(x => this.reversiGamesRepository.findOneOrFail({ }).then(x => this.reversiGamesRepository.findOneOrFail({
where: { id: x.identifiers[0].id }, where: { id: x.identifiers[0].id },
relations: ['user1', 'user2'], relations: ['user1', 'user2'],
@ -255,7 +315,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw = parseInt(game.bw, 10); bw = parseInt(game.bw, 10);
} }
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); const engine = new Reversi.Game(game.map, {
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
});
const crc32 = engine.calcCrc32().toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({ .set({
@ -276,12 +342,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const engine = new Reversi.Game(updatedGame.map, {
isLlotheo: updatedGame.isLlotheo,
canPutEverywhere: updatedGame.canPutEverywhere,
loopedBoard: updatedGame.loopedBoard,
});
if (engine.isEnded) { if (engine.isEnded) {
let winnerId; let winnerId;
if (engine.winner === true) { if (engine.winner === true) {
@ -335,7 +395,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> { public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
const invitations = await this.redisClient.zrange( const invitations = await this.redisClient.zrange(
`reversi:matchSpecific:${user.id}`, `reversi:matchSpecific:${user.id}`,
Date.now() - MATCHING_TIMEOUT_MS, Date.now() - INVITATION_TIMEOUT_MS,
'+inf', '+inf',
'BYSCORE'); 'BYSCORE');
return invitations; return invitations;
@ -406,7 +466,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const serializeLogs = Reversi.Serializer.serializeLogs(logs); const serializeLogs = Reversi.Serializer.serializeLogs(logs);
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); const crc32 = engine.calcCrc32().toString();
const updatedGame = { const updatedGame = {
...game, ...game,
@ -538,7 +598,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (game == null) throw new Error('game not found'); if (game == null) throw new Error('game not found');
if (crc32.toString() !== game.crc32) { if (crc32.toString() !== game.crc32) {
return await this.reversiGameEntityService.packDetail(game); return game;
} else { } else {
return null; return null;
} }

View file

@ -61,6 +61,7 @@ export class ReversiGameEntityService {
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn, timeLimitForEachTurn: game.timeLimitForEachTurn,
noIrregularRules: game.noIrregularRules,
logs: game.logs, logs: game.logs,
map: game.map, map: game.map,
}); });
@ -105,6 +106,7 @@ export class ReversiGameEntityService {
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn, timeLimitForEachTurn: game.timeLimitForEachTurn,
noIrregularRules: game.noIrregularRules,
}); });
} }

View file

@ -106,6 +106,11 @@ export class MiReversiGame {
}) })
public bw: string; public bw: string;
@Column('boolean', {
default: false,
})
public noIrregularRules: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
noIrregularRules: {
type: 'boolean',
optional: false, nullable: false,
},
isLlotheo: { isLlotheo: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
noIrregularRules: {
type: 'boolean',
optional: false, nullable: false,
},
isLlotheo: { isLlotheo: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ReversiService } from '@/core/ReversiService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -32,6 +33,7 @@ export class CleanProcessorService {
private roleAssignmentsRepository: RoleAssignmentsRepository, private roleAssignmentsRepository: RoleAssignmentsRepository,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private reversiService: ReversiService,
private idService: IdService, private idService: IdService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('clean'); this.logger = this.queueLoggerService.logger.createSubLogger('clean');
@ -65,6 +67,8 @@ export class CleanProcessorService {
}); });
} }
this.reversiService.cleanOutdatedGames();
this.logger.succ('Cleaned.'); this.logger.succ('Cleaned.');
} }
} }

View file

@ -385,6 +385,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js';
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
import { GetterService } from './GetterService.js'; import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js'; import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -768,6 +769,7 @@ const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___r
const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default }; const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default };
const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default };
@Module({ @Module({
imports: [ imports: [
@ -1155,6 +1157,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
$reversi_invitations, $reversi_invitations,
$reversi_showGame, $reversi_showGame,
$reversi_surrender, $reversi_surrender,
$reversi_verify,
], ],
exports: [ exports: [
$admin_meta, $admin_meta,
@ -1533,6 +1536,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
$reversi_invitations, $reversi_invitations,
$reversi_showGame, $reversi_showGame,
$reversi_surrender, $reversi_surrender,
$reversi_verify,
], ],
}) })
export class EndpointsModule {} export class EndpointsModule {}

View file

@ -386,6 +386,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js';
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
const eps = [ const eps = [
['admin/meta', ep___admin_meta], ['admin/meta', ep___admin_meta],
@ -767,6 +768,7 @@ const eps = [
['reversi/invitations', ep___reversi_invitations], ['reversi/invitations', ep___reversi_invitations],
['reversi/show-game', ep___reversi_showGame], ['reversi/show-game', ep___reversi_showGame],
['reversi/surrender', ep___reversi_surrender], ['reversi/surrender', ep___reversi_surrender],
['reversi/verify', ep___reversi_verify],
]; ];
interface IEndpointMetaBase { interface IEndpointMetaBase {

View file

@ -3,9 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { NotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { QueryService } from '@/core/QueryService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -38,9 +41,25 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private getterService: GetterService, private getterService: GetterService,
private queryService: QueryService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = await this.notesRepository.createQueryBuilder('note')
.select('note.id')
.where('note.id = :noteId', { noteId: ps.noteId });
this.queryService.generateVisibilityQuery(query, me);
const note = await query.getOne();
if (note === null) {
throw new ApiError(meta.errors.noSuchNote);
}
const edits = await this.getterService.getEdits(ps.noteId).catch(err => { const edits = await this.getterService.getEdits(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err; throw err;

View file

@ -43,7 +43,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
.andWhere('game.isStarted = TRUE')
.innerJoinAndSelect('game.user1', 'user1') .innerJoinAndSelect('game.user1', 'user1')
.innerJoinAndSelect('game.user2', 'user2'); .innerJoinAndSelect('game.user2', 'user2');
@ -53,6 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.where('game.user1Id = :userId', { userId: me.id }) .where('game.user1Id = :userId', { userId: me.id })
.orWhere('game.user2Id = :userId', { userId: me.id }); .orWhere('game.user2Id = :userId', { userId: me.id });
})); }));
} else {
query.andWhere('game.isStarted = TRUE');
} }
const games = await query.take(ps.limit).getMany(); const games = await query.take(ps.limit).getMany();

View file

@ -37,6 +37,8 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id', nullable: true }, userId: { type: 'string', format: 'misskey:id', nullable: true },
noIrregularRules: { type: 'boolean', default: false },
multiple: { type: 'boolean', default: false },
}, },
required: [], required: [],
} as const; } as const;
@ -56,7 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err; throw err;
}) : null; }) : null;
const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me); const game = target
? await this.reversiService.matchSpecificUser(me, target, ps.multiple)
: await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple);
if (game == null) return; if (game == null) return;

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { ApiError } from '../../error.js';
export const meta = {
errors: {
noSuchGame: {
message: 'No such game.',
code: 'NO_SUCH_GAME',
id: '8fb05624-b525-43dd-90f7-511852bdfeee',
},
},
res: {
type: 'object',
optional: false, nullable: false,
properties: {
desynced: { type: 'boolean' },
game: {
type: 'object',
optional: true, nullable: true,
ref: 'ReversiGameDetailed',
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
gameId: { type: 'string', format: 'misskey:id' },
crc32: { type: 'string' },
},
required: ['gameId', 'crc32'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32);
if (game) {
return {
desynced: true,
game: await this.reversiGameEntityService.packDetail(game),
};
} else {
return {
desynced: false,
};
}
});
}
}

View file

@ -4,7 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; import type { MiReversiGame } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js'; import { ReversiService } from '@/core/ReversiService.js';
@ -19,7 +19,6 @@ class ReversiGameChannel extends Channel {
constructor( constructor(
private reversiService: ReversiService, private reversiService: ReversiService,
private reversiGamesRepository: ReversiGamesRepository,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
id: string, id: string,
@ -42,7 +41,6 @@ class ReversiGameChannel extends Channel {
case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'cancel': this.cancelGame(); break; case 'cancel': this.cancelGame(); break;
case 'putStone': this.putStone(body.pos, body.id); break; case 'putStone': this.putStone(body.pos, body.id); break;
case 'resync': this.resync(body.crc32); break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
} }
} }
@ -75,14 +73,6 @@ class ReversiGameChannel extends Channel {
this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id); this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id);
} }
@bindThis
private async resync(crc32: string | number) {
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
if (game) {
this.send('resynced', game);
}
}
@bindThis @bindThis
private async claimTimeIsUp() { private async claimTimeIsUp() {
if (this.user == null) return; if (this.user == null) return;
@ -104,9 +94,6 @@ export class ReversiGameChannelService implements MiChannelService<false> {
public readonly kind = ReversiGameChannel.kind; public readonly kind = ReversiGameChannel.kind;
constructor( constructor(
@Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository,
private reversiService: ReversiService, private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
) { ) {
@ -116,7 +103,6 @@ export class ReversiGameChannelService implements MiChannelService<false> {
public create(id: string, connection: Channel['connection']): ReversiGameChannel { public create(id: string, connection: Channel['connection']): ReversiGameChannel {
return new ReversiGameChannel( return new ReversiGameChannel(
this.reversiService, this.reversiService,
this.reversiGamesRepository,
this.reversiGameEntityService, this.reversiGameEntityService,
id, id,
connection, connection,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View file

@ -42,10 +42,8 @@
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "10.3.1", "chromatic": "10.3.1",
"compare-versions": "6.1.0", "compare-versions": "6.1.0",
"crc-32": "^1.2.2",
"cropperjs": "2.0.0-beta.4", "cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"defu": "^6.1.4",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div <div
ref="rootEl" ref="rootEl"
:class="[$style.transitionRoot]" :class="[$style.transitionRoot, { [$style.enableAnimation]: shouldAnimate }]"
@touchstart.passive="touchStart" @touchstart.passive="touchStart"
@touchmove.passive="touchMove" @touchmove.passive="touchMove"
@touchend.passive="touchEnd" @touchend.passive="touchEnd"
@ -44,6 +44,8 @@ const emit = defineEmits<{
(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; (ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
}>(); }>();
const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
// // // //
// //
@ -188,7 +190,9 @@ watch(tabModel, (newTab, oldTab) => {
.transitionChildren { .transitionChildren {
grid-area: 1 / 1 / 2 / 2; grid-area: 1 / 1 / 2 / 2;
transform: translateX(var(--swipe)); transform: translateX(var(--swipe));
}
.enableAnimation .transitionChildren {
&.swipeAnimation_enterActive, &.swipeAnimation_enterActive,
&.swipeAnimation_leaveActive { &.swipeAnimation_leaveActive {
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1); transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);

View file

@ -76,6 +76,7 @@ const emit = defineEmits<{
const props = defineProps<{ const props = defineProps<{
includeSelf?: boolean; includeSelf?: boolean;
local?: boolean;
}>(); }>();
const username = ref(''); const username = ref('');
@ -90,6 +91,7 @@ function search() {
users.value = []; users.value = [];
return; return;
} }
if (props.local) host.value = '.';
misskeyApi('users/search-by-username-and-host', { misskeyApi('users/search-by-username-and-host', {
username: username.value, username: username.value,
host: host.value, host: host.value,

View file

@ -419,10 +419,11 @@ export function form(title, form) {
}); });
} }
export async function selectUser(opts: { includeSelf?: boolean } = {}): Promise<Misskey.entities.UserLite> { export async function selectUser(opts: { includeSelf?: boolean, local?: boolean } = {}): Promise<Misskey.entities.UserLite> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf, includeSelf: opts.includeSelf,
local: opts.local,
}, { }, {
ok: user => { ok: user => {
resolve(user); resolve(user);

View file

@ -143,7 +143,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
import * as CRC32 from 'crc-32';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi'; import * as Reversi from 'misskey-reversi';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -240,11 +239,17 @@ watch(logPos, (v) => {
if (game.value.isStarted && !game.value.isEnded) { if (game.value.isStarted && !game.value.isEnded) {
useInterval(() => { useInterval(() => {
if (game.value.isEnded || props.connection == null) return; if (game.value.isEnded) return;
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); const crc32 = engine.value.calcCrc32();
if (_DEV_) console.log('crc32', crc32); if (_DEV_) console.log('crc32', crc32);
props.connection.send('resync', { misskeyApi('reversi/verify', {
crc32: crc32, gameId: game.value.id,
crc32: crc32.toString(),
}).then((res) => {
if (res.desynced) {
console.log('resynced');
restoreGame(res.game!);
}
}); });
}, 10000, { immediate: false, afterMounted: true }); }, 10000, { immediate: false, afterMounted: true });
} }
@ -392,12 +397,6 @@ function restoreGame(_game) {
checkEnd(); checkEnd();
} }
function onStreamResynced(_game) {
console.log('resynced');
restoreGame(_game);
}
async function surrender() { async function surrender() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
@ -450,7 +449,6 @@ function share() {
onMounted(() => { onMounted(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('log', onStreamLog);
props.connection.on('resynced', onStreamResynced);
props.connection.on('ended', onStreamEnded); props.connection.on('ended', onStreamEnded);
} }
}); });
@ -458,7 +456,6 @@ onMounted(() => {
onActivated(() => { onActivated(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('log', onStreamLog);
props.connection.on('resynced', onStreamResynced);
props.connection.on('ended', onStreamEnded); props.connection.on('ended', onStreamEnded);
} }
}); });
@ -466,7 +463,6 @@ onActivated(() => {
onDeactivated(() => { onDeactivated(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('log', onStreamLog);
props.connection.off('resynced', onStreamResynced);
props.connection.off('ended', onStreamEnded); props.connection.off('ended', onStreamEnded);
} }
}); });
@ -474,7 +470,6 @@ onDeactivated(() => {
onUnmounted(() => { onUnmounted(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('log', onStreamLog);
props.connection.off('resynced', onStreamResynced);
props.connection.off('ended', onStreamEnded); props.connection.off('ended', onStreamEnded);
} }
}); });

View file

@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" :class="{ [$style.disallowInner]: isReady }"> <div class="_gaps" :class="{ [$style.disallowInner]: isReady }">
<div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div> <div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
<template v-if="game.noIrregularRules">
<div>{{ i18n.ts._reversi.disallowIrregularRules }}</div>
</template>
<template v-else>
<div class="_panel"> <div class="_panel">
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
<div>{{ mapName }}</div> <div>{{ mapName }}</div>
@ -75,13 +79,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch> <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
</template>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
<template #footer> <template #footer>
<div :class="$style.footer"> <div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<div style="text-align: center; margin-bottom: 10px;"> <div style="text-align: center;" class="_gaps_s">
<div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
<div>
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template> <template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template> <template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
<template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template> <template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
@ -92,6 +99,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton> <MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton> <MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
</div> </div>
<div>
<MkSwitch v-model="shareWhenStart">{{ i18n.ts._reversi.shareToTlTheGameWhenStart }}</MkSwitch>
</div>
</div>
</MkSpacer> </MkSpacer>
</div> </div>
</template> </template>
@ -124,6 +135,8 @@ const props = defineProps<{
connection: Misskey.ChannelConnection; connection: Misskey.ChannelConnection;
}>(); }>();
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game)); const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
const mapName = computed(() => { const mapName = computed(() => {
@ -142,6 +155,8 @@ const isOpReady = computed(() => {
return false; return false;
}); });
const opponentHasSettingsChanged = ref(false);
watch(() => game.value.bw, () => { watch(() => game.value.bw, () => {
updateSettings('bw'); updateSettings('bw');
}); });
@ -190,6 +205,7 @@ async function cancel() {
function ready() { function ready() {
props.connection.send('ready', true); props.connection.send('ready', true);
opponentHasSettingsChanged.value = false;
} }
function unready() { function unready() {
@ -212,6 +228,10 @@ function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof M
if (userId === $i.id) return; if (userId === $i.id) return;
if (game.value[key] === value) return; if (game.value[key] === value) return;
game.value[key] = value; game.value[key] = value;
if (isReady.value) {
opponentHasSettingsChanged.value = true;
unready();
}
} }
function onMapCellClick(pos: number, pixel: string) { function onMapCellClick(pos: number, pixel: string) {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div> <div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/> <GameSetting v-else-if="!game.isStarted" v-model:shareWhenStart="shareWhenStart" :game="game" :connection="connection!"/>
<GameBoard v-else :game="game" :connection="connection"/> <GameBoard v-else :game="game" :connection="connection"/>
</template> </template>
@ -21,6 +21,7 @@ import { signinRequired } from '@/account.js';
import { useRouter } from '@/global/router/supplier.js'; import { useRouter } from '@/global/router/supplier.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
const $i = signinRequired(); const $i = signinRequired();
@ -32,17 +33,32 @@ const props = defineProps<{
const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null); const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null);
const connection = shallowRef<Misskey.ChannelConnection | null>(null); const connection = shallowRef<Misskey.ChannelConnection | null>(null);
const shareWhenStart = ref(false);
watch(() => props.gameId, () => { watch(() => props.gameId, () => {
fetchGame(); fetchGame();
}); });
function start(_game: Misskey.entities.ReversiGameDetailed) {
if (game.value?.isStarted) return;
if (shareWhenStart.value) {
misskeyApi('notes/create', {
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
visibility: 'home',
});
}
game.value = _game;
}
async function fetchGame() { async function fetchGame() {
const _game = await misskeyApi('reversi/show-game', { const _game = await misskeyApi('reversi/show-game', {
gameId: props.gameId, gameId: props.gameId,
}); });
game.value = _game; game.value = _game;
shareWhenStart.value = false;
if (connection.value) { if (connection.value) {
connection.value.dispose(); connection.value.dispose();
@ -52,7 +68,7 @@ async function fetchGame() {
gameId: game.value.id, gameId: game.value.id,
}); });
connection.value.on('started', x => { connection.value.on('started', x => {
game.value = x.game; start(x.game);
}); });
connection.value.on('canceled', x => { connection.value.on('canceled', x => {
connection.value?.dispose(); connection.value?.dispose();
@ -68,6 +84,25 @@ async function fetchGame() {
} }
} }
//
useInterval(async () => {
if (game.value == null) return;
if (game.value.isStarted) return;
const _game = await misskeyApi('reversi/show-game', {
gameId: props.gameId,
});
if (_game.isStarted) {
start(_game);
} else {
game.value = _game;
}
}, 1000 * 10, {
immediate: false,
afterMounted: true,
});
onMounted(() => { onMounted(() => {
fetchGame(); fetchGame();
}); });
@ -78,10 +113,6 @@ onUnmounted(() => {
} }
}); });
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({ definePageMetadata(computed(() => ({
title: 'Reversi', title: 'Reversi',
icon: 'ph-game-controller ph-bold ph-lg', icon: 'ph-game-controller ph-bold ph-lg',

View file

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="myGamesPagination" :disableAutoLoad="true"> <MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.gamePreviews"> <div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
<div :class="$style.gamePreviewPlayers"> <div :class="$style.gamePreviewPlayers">
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
@ -45,7 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
</div> </div>
<div :class="$style.gamePreviewFooter"> <div :class="$style.gamePreviewFooter">
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
<span v-else>{{ i18n.ts._reversi.ended }}</span> <span v-else>{{ i18n.ts._reversi.ended }}</span>
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/> <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
</div> </div>
@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="gamesPagination" :disableAutoLoad="true"> <MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.gamePreviews"> <div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
<div :class="$style.gamePreviewPlayers"> <div :class="$style.gamePreviewPlayers">
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
@ -71,7 +72,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
</div> </div>
<div :class="$style.gamePreviewFooter"> <div :class="$style.gamePreviewFooter">
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
<span v-else>{{ i18n.ts._reversi.ended }}</span> <span v-else>{{ i18n.ts._reversi.ended }}</span>
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/> <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
</div> </div>
@ -137,7 +139,9 @@ if ($i) {
const connection = useStream().useChannel('reversi'); const connection = useStream().useChannel('reversi');
connection.on('matched', x => { connection.on('matched', x => {
if (matchingUser.value != null || matchingAny.value) {
startGame(x.game); startGame(x.game);
}
}); });
connection.on('invited', invitation => { connection.on('invited', invitation => {
@ -153,6 +157,7 @@ if ($i) {
const invitations = ref<Misskey.entities.UserLite[]>([]); const invitations = ref<Misskey.entities.UserLite[]>([]);
const matchingUser = ref<Misskey.entities.UserLite | null>(null); const matchingUser = ref<Misskey.entities.UserLite | null>(null);
const matchingAny = ref<boolean>(false); const matchingAny = ref<boolean>(false);
const noIrregularRules = ref<boolean>(false);
function startGame(game: Misskey.entities.ReversiGameDetailed) { function startGame(game: Misskey.entities.ReversiGameDetailed) {
matchingUser.value = null; matchingUser.value = null;
@ -178,6 +183,7 @@ async function matchHeatbeat() {
} else if (matchingAny.value) { } else if (matchingAny.value) {
const res = await misskeyApi('reversi/match', { const res = await misskeyApi('reversi/match', {
userId: null, userId: null,
noIrregularRules: noIrregularRules.value,
}); });
if (res != null) { if (res != null) {
@ -187,7 +193,7 @@ async function matchHeatbeat() {
} }
async function matchUser() { async function matchUser() {
const user = await os.selectUser({ local: true }); const user = await os.selectUser({ local: true, includeSelf: false });
if (user == null) return; if (user == null) return;
matchingUser.value = user; matchingUser.value = user;
@ -195,10 +201,22 @@ async function matchUser() {
matchHeatbeat(); matchHeatbeat();
} }
async function matchAny() { function matchAny(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts._reversi.allowIrregularRules,
action: () => {
noIrregularRules.value = false;
matchingAny.value = true; matchingAny.value = true;
matchHeatbeat(); matchHeatbeat();
},
}, {
text: i18n.ts._reversi.disallowIrregularRules,
action: () => {
noIrregularRules.value = true;
matchingAny.value = true;
matchHeatbeat();
},
}], ev.currentTarget ?? ev.target);
} }
function cancelMatching() { function cancelMatching() {
@ -220,12 +238,14 @@ async function accept(user) {
} }
} }
useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true }); useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
onMounted(() => { onMounted(() => {
misskeyApi('reversi/invitations').then(_invitations => { misskeyApi('reversi/invitations').then(_invitations => {
invitations.value = _invitations; invitations.value = _invitations;
}); });
window.addEventListener('beforeunload', cancelMatching);
}); });
onDeactivated(() => { onDeactivated(() => {
@ -273,6 +293,10 @@ definePageMetadata(computed(() => ({
box-shadow: inset 0 0 8px 0px var(--accent); box-shadow: inset 0 0 8px 0px var(--accent);
} }
.gamePreviewWaiting {
box-shadow: inset 0 0 8px 0px var(--warn);
}
.gamePreviewPlayers { .gamePreviewPlayers {
text-align: center; text-align: center;
padding: 16px; padding: 16px;
@ -306,6 +330,12 @@ definePageMetadata(computed(() => ({
animation: blink 2s infinite; animation: blink 2s infinite;
} }
.gamePreviewStatusWaiting {
color: var(--warn);
font-weight: bold;
animation: blink 2s infinite;
}
.waitingScreen { .waitingScreen {
text-align: center; text-align: center;
} }

View file

@ -7,7 +7,6 @@
import { onUnmounted, Ref, ref, watch } from 'vue'; import { onUnmounted, Ref, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel'; import { BroadcastChannel } from 'broadcast-channel';
import { defu } from 'defu';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { get, set } from '@/scripts/idb-proxy.js'; import { get, set } from '@/scripts/idb-proxy.js';
@ -81,14 +80,37 @@ export class Storage<T extends StateDef> {
this.loaded = this.ready.then(() => this.load()); this.loaded = this.ready.then(() => this.load());
} }
private isPureObject(value: unknown): value is Record<string, unknown> { private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value); return typeof value === 'object' && value !== null && !Array.isArray(value);
} }
private mergeState<T>(value: T, def: T): T { /**
* valueにないキーをdefからもらう\
* nullはそのままundefinedはdefの値
**/
private mergeObject<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) { if (this.isPureObject(value) && this.isPureObject(def)) {
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def); const result = structuredClone(value) as X;
return defu(value, def) as T; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
result[k] = v;
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
result[k] = this.mergeObject<typeof v>(child, v);
}
}
return result;
}
return value;
}
private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
const merged = this.mergeObject(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
return merged as X;
} }
return value; return value;
} }

View file

@ -0,0 +1,31 @@
import { build } from "esbuild";
import { globSync } from "glob";
const entryPoints = globSync("./src/**/**.{ts,tsx}");
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints,
minify: true,
outdir: "./built/esm",
target: "es2022",
platform: "browser",
format: "esm",
};
if (process.env.WATCH === "true") {
options.watch = {
onRebuild(error, result) {
if (error) {
console.error("watch build failed:", error);
} else {
console.log("watch build succeeded:", result);
}
},
};
}
build(options).catch((err) => {
process.stderr.write(err.stderr);
process.exit(1);
});

View file

@ -13,18 +13,21 @@
} }
}, },
"scripts": { "scripts": {
"build": "npm run ts", "build": "node ./build.js",
"ts": "npm run ts-esm && npm run ts-dts", "build:tsc": "npm run tsc",
"ts-esm": "tsc --outDir built/esm", "tsc": "npm run ts-esm && npm run ts-dts",
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true", "tsc-esm": "tsc --outDir built/esm",
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", "tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@types/matter-js": "0.19.6",
"@types/node": "20.11.5", "@types/node": "20.11.5",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "6.18.1",
"eslint": "8.56.0", "eslint": "8.56.0",
@ -35,9 +38,9 @@
"built" "built"
], ],
"dependencies": { "dependencies": {
"@types/matter-js": "0.19.6", "esbuild": "0.19.11",
"@types/seedrandom": "3.0.8",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"glob": "^10.3.10",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"seedrandom": "3.0.5" "seedrandom": "3.0.5"
} }

View file

@ -1633,6 +1633,8 @@ declare namespace entities {
ReversiShowGameRequest, ReversiShowGameRequest,
ReversiShowGameResponse, ReversiShowGameResponse,
ReversiSurrenderRequest, ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
Error_2 as Error, Error_2 as Error,
UserLite, UserLite,
UserDetailedNotMeOnly, UserDetailedNotMeOnly,
@ -2644,6 +2646,12 @@ type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200
// @public (undocumented) // @public (undocumented)
type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json'];
// @public (undocumented)
type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json'];
// @public (undocumented)
type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type Role = components['schemas']['Role']; type Role = components['schemas']['Role'];

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.2 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-22T07:11:08.412Z * generatedAt: 2024-01-24T07:32:10.455Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';
@ -4140,5 +4140,16 @@ declare module '../api.js' {
params: P, params: P,
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *No*
*/
request<E extends 'reversi/verify', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.2 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-22T07:11:08.410Z * generatedAt: 2024-01-24T07:32:10.453Z
*/ */
import type { import type {
@ -569,6 +569,8 @@ import type {
ReversiShowGameRequest, ReversiShowGameRequest,
ReversiShowGameResponse, ReversiShowGameResponse,
ReversiSurrenderRequest, ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
} from './entities.js'; } from './entities.js';
export type Endpoints = { export type Endpoints = {
@ -951,4 +953,5 @@ export type Endpoints = {
'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse };
'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse };
'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
} }

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.2 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-22T07:11:08.408Z * generatedAt: 2024-01-24T07:32:10.452Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';
@ -571,3 +571,5 @@ export type ReversiInvitationsResponse = operations['reversi/invitations']['resp
export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json'];
export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json'];
export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json'];
export type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json'];
export type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json'];

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.2 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-22T07:11:08.408Z * generatedAt: 2024-01-24T07:32:10.450Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -2,8 +2,8 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */
/* /*
* version: 2024.2.0-beta.2 * version: 2024.2.0-beta.6
* generatedAt: 2024-01-22T07:11:08.327Z * generatedAt: 2024-01-24T07:32:10.370Z
*/ */
/** /**
@ -3645,6 +3645,15 @@ export type paths = {
*/ */
post: operations['reversi/surrender']; post: operations['reversi/surrender'];
}; };
'/reversi/verify': {
/**
* reversi/verify
* @description No description provided.
*
* **Credential required**: *No*
*/
post: operations['reversi/verify'];
};
}; };
export type webhooks = Record<string, never>; export type webhooks = Record<string, never>;
@ -4617,6 +4626,7 @@ export type components = {
timeoutUserId: string | null; timeoutUserId: string | null;
black: number | null; black: number | null;
bw: string; bw: string;
noIrregularRules: boolean;
isLlotheo: boolean; isLlotheo: boolean;
canPutEverywhere: boolean; canPutEverywhere: boolean;
loopedBoard: boolean; loopedBoard: boolean;
@ -4652,6 +4662,7 @@ export type components = {
timeoutUserId: string | null; timeoutUserId: string | null;
black: number | null; black: number | null;
bw: string; bw: string;
noIrregularRules: boolean;
isLlotheo: boolean; isLlotheo: boolean;
canPutEverywhere: boolean; canPutEverywhere: boolean;
loopedBoard: boolean; loopedBoard: boolean;
@ -26677,6 +26688,10 @@ export type operations = {
'application/json': { 'application/json': {
/** Format: misskey:id */ /** Format: misskey:id */
userId?: string | null; userId?: string | null;
/** @default false */
noIrregularRules?: boolean;
/** @default false */
multiple?: boolean;
}; };
}; };
}; };
@ -26871,5 +26886,63 @@ export type operations = {
}; };
}; };
}; };
/**
* reversi/verify
* @description No description provided.
*
* **Credential required**: *No*
*/
'reversi/verify': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
gameId: string;
crc32: string;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
desynced: boolean;
game?: components['schemas']['ReversiGameDetailed'] | null;
};
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
}; };

View file

@ -1,4 +1,5 @@
module.exports = { module.exports = {
root: true,
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],

View file

@ -0,0 +1,31 @@
import { build } from "esbuild";
import { globSync } from "glob";
const entryPoints = globSync("./src/**/**.{ts,tsx}");
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints,
minify: true,
outdir: "./built/esm",
target: "es2022",
platform: "browser",
format: "esm",
};
if (process.env.WATCH === "true") {
options.watch = {
onRebuild(error, result) {
if (error) {
console.error("watch build failed:", error);
} else {
console.log("watch build succeeded:", result);
}
},
};
}
build(options).catch((err) => {
process.stderr.write(err.stderr);
process.exit(1);
});

View file

@ -13,11 +13,12 @@
} }
}, },
"scripts": { "scripts": {
"build": "npm run ts", "build": "node ./build.js",
"ts": "npm run ts-esm && npm run ts-dts", "build:tsc": "npm run tsc",
"ts-esm": "tsc --outDir built/esm", "tsc": "npm run tsc-esm && npm run tsc-dts",
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true", "tsc-esm": "tsc --outDir built/esm",
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", "tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
@ -35,5 +36,8 @@
"built" "built"
], ],
"dependencies": { "dependencies": {
"crc-32": "1.2.2",
"esbuild": "0.19.11",
"glob": "10.3.10"
} }
} }

View file

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import CRC32 from 'crc-32';
/** /**
* true ... * true ...
* false ... * false ...
@ -204,6 +206,13 @@ export class Game {
return ([] as number[]).concat(...diffVectors.map(effectsInLine)); return ([] as number[]).concat(...diffVectors.map(effectsInLine));
} }
public calcCrc32(): number {
return CRC32.str(JSON.stringify({
board: this.board,
turn: this.turn,
}));
}
public get isEnded(): boolean { public get isEnded(): boolean {
return this.turn === null; return this.turn === null;
} }

View file

@ -187,9 +187,6 @@ importers:
content-disposition: content-disposition:
specifier: 0.5.4 specifier: 0.5.4
version: 0.5.4 version: 0.5.4
crc-32:
specifier: ^1.2.2
version: 1.2.2
date-fns: date-fns:
specifier: 2.30.0 specifier: 2.30.0
version: 2.30.0 version: 2.30.0
@ -750,18 +747,12 @@ importers:
compare-versions: compare-versions:
specifier: 6.1.0 specifier: 6.1.0
version: 6.1.0 version: 6.1.0
crc-32:
specifier: ^1.2.2
version: 1.2.2
cropperjs: cropperjs:
specifier: 2.0.0-beta.4 specifier: 2.0.0-beta.4
version: 2.0.0-beta.4 version: 2.0.0-beta.4
date-fns: date-fns:
specifier: 2.30.0 specifier: 2.30.0
version: 2.30.0 version: 2.30.0
defu:
specifier: ^6.1.4
version: 6.1.4
escape-regexp: escape-regexp:
specifier: 0.0.1 specifier: 0.0.1
version: 0.0.1 version: 0.0.1
@ -1130,15 +1121,15 @@ importers:
packages/misskey-bubble-game: packages/misskey-bubble-game:
dependencies: dependencies:
'@types/matter-js': esbuild:
specifier: 0.19.6 specifier: 0.19.11
version: 0.19.6 version: 0.19.11
'@types/seedrandom':
specifier: 3.0.8
version: 3.0.8
eventemitter3: eventemitter3:
specifier: 5.0.1 specifier: 5.0.1
version: 5.0.1 version: 5.0.1
glob:
specifier: ^10.3.10
version: 10.3.10
matter-js: matter-js:
specifier: 0.19.0 specifier: 0.19.0
version: 0.19.0 version: 0.19.0
@ -1149,9 +1140,15 @@ importers:
'@misskey-dev/eslint-plugin': '@misskey-dev/eslint-plugin':
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0) version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
'@types/matter-js':
specifier: 0.19.6
version: 0.19.6
'@types/node': '@types/node':
specifier: 20.11.5 specifier: 20.11.5
version: 20.11.5 version: 20.11.5
'@types/seedrandom':
specifier: 3.0.8
version: 3.0.8
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: 6.18.1 specifier: 6.18.1
version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3) version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
@ -1269,6 +1266,16 @@ importers:
version: 5.3.3 version: 5.3.3
packages/misskey-reversi: packages/misskey-reversi:
dependencies:
crc-32:
specifier: 1.2.2
version: 1.2.2
esbuild:
specifier: 0.19.11
version: 0.19.11
glob:
specifier: 10.3.10
version: 10.3.10
devDependencies: devDependencies:
'@misskey-dev/eslint-plugin': '@misskey-dev/eslint-plugin':
specifier: 1.0.0 specifier: 1.0.0
@ -7839,6 +7846,7 @@ packages:
/@types/matter-js@0.19.6: /@types/matter-js@0.19.6:
resolution: {integrity: sha512-ffk6tqJM5scla+ThXmnox+mdfCo3qYk6yMjQsNcrbo6eQ5DqorVdtnaL+1agCoYzxUjmHeiNB7poBMAmhuLY7w==} resolution: {integrity: sha512-ffk6tqJM5scla+ThXmnox+mdfCo3qYk6yMjQsNcrbo6eQ5DqorVdtnaL+1agCoYzxUjmHeiNB7poBMAmhuLY7w==}
dev: true
/@types/mdx@2.0.3: /@types/mdx@2.0.3:
resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==}
@ -8020,7 +8028,7 @@ packages:
/@types/seedrandom@3.0.8: /@types/seedrandom@3.0.8:
resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==}
dev: false dev: true
/@types/semver@7.5.6: /@types/semver@7.5.6:
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
@ -10926,6 +10934,7 @@ packages:
/defu@6.1.4: /defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
dev: true
/del@6.1.1: /del@6.1.1:
resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}