From 1ef66c962a1cea81dee4f5db32cd011feac7de44 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 17 Jun 2018 08:10:54 +0900 Subject: reversi :white_flower: :100: --- .../app/common/scripts/compose-notification.ts | 2 +- .../app/common/scripts/streaming/othello-game.ts | 11 - src/client/app/common/scripts/streaming/othello.ts | 31 - .../app/common/scripts/streaming/reversi-game.ts | 11 + src/client/app/common/scripts/streaming/reversi.ts | 31 + src/client/app/common/views/components/index.ts | 4 +- .../app/common/views/components/othello.game.vue | 324 -------- .../common/views/components/othello.gameroom.vue | 42 - .../app/common/views/components/othello.room.vue | 297 ------- src/client/app/common/views/components/othello.vue | 313 ------- .../app/common/views/components/reversi.game.vue | 324 ++++++++ .../common/views/components/reversi.gameroom.vue | 42 + .../app/common/views/components/reversi.room.vue | 297 +++++++ src/client/app/common/views/components/reversi.vue | 313 +++++++ src/client/app/desktop/script.ts | 10 +- .../app/desktop/views/components/game-window.vue | 6 +- .../app/desktop/views/components/ui.header.nav.vue | 12 +- src/client/app/desktop/views/pages/othello.vue | 50 -- src/client/app/desktop/views/pages/reversi.vue | 50 ++ src/client/app/mios.ts | 8 +- src/client/app/mobile/script.ts | 6 +- .../app/mobile/views/components/ui.header.vue | 12 +- src/client/app/mobile/views/components/ui.nav.vue | 14 +- src/client/app/mobile/views/pages/othello.vue | 50 -- src/client/app/mobile/views/pages/reversi.vue | 50 ++ src/client/assets/othello-put-me.mp3 | Bin 15672 -> 0 bytes src/client/assets/othello-put-you.mp3 | Bin 26121 -> 0 bytes src/client/assets/reversi-put-me.mp3 | Bin 0 -> 15672 bytes src/client/assets/reversi-put-you.mp3 | Bin 0 -> 26121 bytes src/config/types.ts | 2 +- src/models/othello-game.ts | 109 --- src/models/othello-matching.ts | 44 - src/models/reversi-game.ts | 109 +++ src/models/reversi-matching.ts | 44 + src/othello/ai/back.ts | 377 --------- src/othello/ai/front.ts | 233 ------ src/othello/ai/index.ts | 1 - src/othello/core.ts | 340 -------- src/othello/maps.ts | 911 --------------------- src/publishers/stream.ts | 12 +- src/reversi/ai/back.ts | 377 +++++++++ src/reversi/ai/front.ts | 233 ++++++ src/reversi/ai/index.ts | 1 + src/reversi/core.ts | 340 ++++++++ src/reversi/maps.ts | 911 +++++++++++++++++++++ src/server/api/endpoints.ts | 10 +- src/server/api/endpoints/othello/games.ts | 62 -- src/server/api/endpoints/othello/games/show.ts | 32 - src/server/api/endpoints/othello/invitations.ts | 15 - src/server/api/endpoints/othello/match.ts | 95 --- src/server/api/endpoints/othello/match/cancel.ts | 9 - src/server/api/endpoints/reversi/games.ts | 62 ++ src/server/api/endpoints/reversi/games/show.ts | 32 + src/server/api/endpoints/reversi/invitations.ts | 15 + src/server/api/endpoints/reversi/match.ts | 95 +++ src/server/api/endpoints/reversi/match/cancel.ts | 9 + src/server/api/stream/othello-game.ts | 333 -------- src/server/api/stream/othello.ts | 29 - src/server/api/stream/reversi-game.ts | 333 ++++++++ src/server/api/stream/reversi.ts | 29 + src/server/api/streaming.ts | 10 +- 61 files changed, 3762 insertions(+), 3762 deletions(-) delete mode 100644 src/client/app/common/scripts/streaming/othello-game.ts delete mode 100644 src/client/app/common/scripts/streaming/othello.ts create mode 100644 src/client/app/common/scripts/streaming/reversi-game.ts create mode 100644 src/client/app/common/scripts/streaming/reversi.ts delete mode 100644 src/client/app/common/views/components/othello.game.vue delete mode 100644 src/client/app/common/views/components/othello.gameroom.vue delete mode 100644 src/client/app/common/views/components/othello.room.vue delete mode 100644 src/client/app/common/views/components/othello.vue create mode 100644 src/client/app/common/views/components/reversi.game.vue create mode 100644 src/client/app/common/views/components/reversi.gameroom.vue create mode 100644 src/client/app/common/views/components/reversi.room.vue create mode 100644 src/client/app/common/views/components/reversi.vue delete mode 100644 src/client/app/desktop/views/pages/othello.vue create mode 100644 src/client/app/desktop/views/pages/reversi.vue delete mode 100644 src/client/app/mobile/views/pages/othello.vue create mode 100644 src/client/app/mobile/views/pages/reversi.vue delete mode 100644 src/client/assets/othello-put-me.mp3 delete mode 100644 src/client/assets/othello-put-you.mp3 create mode 100644 src/client/assets/reversi-put-me.mp3 create mode 100644 src/client/assets/reversi-put-you.mp3 delete mode 100644 src/models/othello-game.ts delete mode 100644 src/models/othello-matching.ts create mode 100644 src/models/reversi-game.ts create mode 100644 src/models/reversi-matching.ts delete mode 100644 src/othello/ai/back.ts delete mode 100644 src/othello/ai/front.ts delete mode 100644 src/othello/ai/index.ts delete mode 100644 src/othello/core.ts delete mode 100644 src/othello/maps.ts create mode 100644 src/reversi/ai/back.ts create mode 100644 src/reversi/ai/front.ts create mode 100644 src/reversi/ai/index.ts create mode 100644 src/reversi/core.ts create mode 100644 src/reversi/maps.ts delete mode 100644 src/server/api/endpoints/othello/games.ts delete mode 100644 src/server/api/endpoints/othello/games/show.ts delete mode 100644 src/server/api/endpoints/othello/invitations.ts delete mode 100644 src/server/api/endpoints/othello/match.ts delete mode 100644 src/server/api/endpoints/othello/match/cancel.ts create mode 100644 src/server/api/endpoints/reversi/games.ts create mode 100644 src/server/api/endpoints/reversi/games/show.ts create mode 100644 src/server/api/endpoints/reversi/invitations.ts create mode 100644 src/server/api/endpoints/reversi/match.ts create mode 100644 src/server/api/endpoints/reversi/match/cancel.ts delete mode 100644 src/server/api/stream/othello-game.ts delete mode 100644 src/server/api/stream/othello.ts create mode 100644 src/server/api/stream/reversi-game.ts create mode 100644 src/server/api/stream/reversi.ts (limited to 'src') diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index c19b1c5ad0..cc28f75998 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -55,7 +55,7 @@ export default function(type, data): Notification { icon: data.user.avatarUrl + '?thumbnail&size=64' }; - case 'othello_invited': + case 'reversi_invited': return { title: '対局への招待があります', body: `${getUserName(data.parent)}さんから`, diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts deleted file mode 100644 index 9e36f647bb..0000000000 --- a/src/client/app/common/scripts/streaming/othello-game.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../../mios'; - -export class OthelloGameStream extends Stream { - constructor(os: MiOS, me, game) { - super(os, 'othello-game', { - i: me ? me.token : null, - game: game.id - }); - } -} diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts deleted file mode 100644 index 8f4f217e39..0000000000 --- a/src/client/app/common/scripts/streaming/othello.ts +++ /dev/null @@ -1,31 +0,0 @@ -import StreamManager from './stream-manager'; -import Stream from './stream'; -import MiOS from '../../../mios'; - -export class OthelloStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'othello', { - i: me.token - }); - } -} - -export class OthelloStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new OthelloStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/reversi-game.ts b/src/client/app/common/scripts/streaming/reversi-game.ts new file mode 100644 index 0000000000..5638b3013f --- /dev/null +++ b/src/client/app/common/scripts/streaming/reversi-game.ts @@ -0,0 +1,11 @@ +import Stream from './stream'; +import MiOS from '../../../mios'; + +export class ReversiGameStream extends Stream { + constructor(os: MiOS, me, game) { + super(os, 'reversi-game', { + i: me ? me.token : null, + game: game.id + }); + } +} diff --git a/src/client/app/common/scripts/streaming/reversi.ts b/src/client/app/common/scripts/streaming/reversi.ts new file mode 100644 index 0000000000..2e4395f0f1 --- /dev/null +++ b/src/client/app/common/scripts/streaming/reversi.ts @@ -0,0 +1,31 @@ +import StreamManager from './stream-manager'; +import Stream from './stream'; +import MiOS from '../../../mios'; + +export class ReversiStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'reversi', { + i: me.token + }); + } +} + +export class ReversiStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new ReversiStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index b91008f718..5b2fa084fb 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -27,7 +27,7 @@ import urlPreview from './url-preview.vue'; import twitterSetting from './twitter-setting.vue'; import fileTypeIcon from './file-type-icon.vue'; import Switch from './switch.vue'; -import Othello from './othello.vue'; +import Reversi from './reversi.vue'; import welcomeTimeline from './welcome-timeline.vue'; import uiInput from './ui/input.vue'; import uiButton from './ui/button.vue'; @@ -65,7 +65,7 @@ Vue.component('mk-url-preview', urlPreview); Vue.component('mk-twitter-setting', twitterSetting); Vue.component('mk-file-type-icon', fileTypeIcon); Vue.component('mk-switch', Switch); -Vue.component('mk-othello', Othello); +Vue.component('mk-reversi', Reversi); Vue.component('mk-welcome-timeline', welcomeTimeline); Vue.component('ui-input', uiInput); Vue.component('ui-button', uiButton); diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue deleted file mode 100644 index 4c63bd18b8..0000000000 --- a/src/client/app/common/views/components/othello.game.vue +++ /dev/null @@ -1,324 +0,0 @@ - - - - - diff --git a/src/client/app/common/views/components/othello.gameroom.vue b/src/client/app/common/views/components/othello.gameroom.vue deleted file mode 100644 index 2dbd04ce49..0000000000 --- a/src/client/app/common/views/components/othello.gameroom.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue deleted file mode 100644 index dcf41f2e04..0000000000 --- a/src/client/app/common/views/components/othello.room.vue +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - - - - diff --git a/src/client/app/common/views/components/othello.vue b/src/client/app/common/views/components/othello.vue deleted file mode 100644 index a0971c45b4..0000000000 --- a/src/client/app/common/views/components/othello.vue +++ /dev/null @@ -1,313 +0,0 @@ - - - - - diff --git a/src/client/app/common/views/components/reversi.game.vue b/src/client/app/common/views/components/reversi.game.vue new file mode 100644 index 0000000000..dc79c95bb8 --- /dev/null +++ b/src/client/app/common/views/components/reversi.game.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/src/client/app/common/views/components/reversi.gameroom.vue b/src/client/app/common/views/components/reversi.gameroom.vue new file mode 100644 index 0000000000..7ce0112451 --- /dev/null +++ b/src/client/app/common/views/components/reversi.gameroom.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/client/app/common/views/components/reversi.room.vue b/src/client/app/common/views/components/reversi.room.vue new file mode 100644 index 0000000000..5074845758 --- /dev/null +++ b/src/client/app/common/views/components/reversi.room.vue @@ -0,0 +1,297 @@ + + + + + + + + + diff --git a/src/client/app/common/views/components/reversi.vue b/src/client/app/common/views/components/reversi.vue new file mode 100644 index 0000000000..e4d7740bde --- /dev/null +++ b/src/client/app/common/views/components/reversi.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 076d532d6d..201ab0a83d 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -34,7 +34,7 @@ import MkMessagingRoom from './views/pages/messaging-room.vue'; import MkNote from './views/pages/note.vue'; import MkSearch from './views/pages/search.vue'; import MkTag from './views/pages/tag.vue'; -import MkOthello from './views/pages/othello.vue'; +import MkReversi from './views/pages/reversi.vue'; import MkShare from './views/pages/share.vue'; /** @@ -64,8 +64,8 @@ init(async (launch) => { { path: '/search', component: MkSearch }, { path: '/tags/:tag', component: MkTag }, { path: '/share', component: MkShare }, - { path: '/othello', component: MkOthello }, - { path: '/othello/:game', component: MkOthello }, + { path: '/reversi', component: MkReversi }, + { path: '/reversi/:game', component: MkReversi }, { path: '/@:user', component: MkUser }, { path: '/notes/:note', component: MkNote } ] @@ -166,8 +166,8 @@ function registerNotifications(stream: HomeStreamManager) { setTimeout(n.close.bind(n), 7000); }); - connection.on('othello_invited', matching => { - const _n = composeNotification('othello_invited', matching); + connection.on('reversi_invited', matching => { + const _n = composeNotification('reversi_invited', matching); const n = new Notification(_n.title, { body: _n.body, icon: _n.icon diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue index c3c0f90075..7c6cb9cd40 100644 --- a/src/client/app/desktop/views/components/game-window.vue +++ b/src/client/app/desktop/views/components/game-window.vue @@ -1,7 +1,7 @@ @@ -18,8 +18,8 @@ export default Vue.extend({ computed: { popout(): string { return this.game - ? `${url}/othello/${this.game.id}` - : `${url}/othello`; + ? `${url}/reversi/${this.game.id}` + : `${url}/reversi`; } } }); diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index fe2637cec3..42211b57fe 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -56,23 +56,23 @@ export default Vue.extend({ this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); + this.connection.on('reversi_invited', this.onReversiInvited); + this.connection.on('reversi_no_invites', this.onReversiNoInvites); } }, beforeDestroy() { if (this.$store.getters.isSignedIn) { - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); + this.connection.off('reversi_invited', this.onReversiInvited); + this.connection.off('reversi_no_invites', this.onReversiNoInvites); (this as any).os.stream.dispose(this.connectionId); } }, methods: { - onOthelloInvited() { + onReversiInvited() { this.hasGameInvitations = true; }, - onOthelloNoInvites() { + onReversiNoInvites() { this.hasGameInvitations = false; }, diff --git a/src/client/app/desktop/views/pages/othello.vue b/src/client/app/desktop/views/pages/othello.vue deleted file mode 100644 index 0d8e987dd9..0000000000 --- a/src/client/app/desktop/views/pages/othello.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/src/client/app/desktop/views/pages/reversi.vue b/src/client/app/desktop/views/pages/reversi.vue new file mode 100644 index 0000000000..098fc41f1c --- /dev/null +++ b/src/client/app/desktop/views/pages/reversi.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index ba3f967a23..9a8d19adbd 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -11,7 +11,7 @@ import { DriveStreamManager } from './common/scripts/streaming/drive'; import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats'; import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats'; import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index'; -import { OthelloStreamManager } from './common/scripts/streaming/othello'; +import { ReversiStreamManager } from './common/scripts/streaming/reversi'; import Err from './common/views/components/connect-failed.vue'; import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; @@ -108,7 +108,7 @@ export default class MiOS extends EventEmitter { serverStatsStream: ServerStatsStreamManager; notesStatsStream: NotesStatsStreamManager; messagingIndexStream: MessagingIndexStreamManager; - othelloStream: OthelloStreamManager; + reversiStream: ReversiStreamManager; } = { localTimelineStream: null, globalTimelineStream: null, @@ -116,7 +116,7 @@ export default class MiOS extends EventEmitter { serverStatsStream: null, notesStatsStream: null, messagingIndexStream: null, - othelloStream: null + reversiStream: null }; /** @@ -233,7 +233,7 @@ export default class MiOS extends EventEmitter { this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i); this.streams.driveStream = new DriveStreamManager(this, this.store.state.i); this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i); - this.streams.othelloStream = new OthelloStreamManager(this, this.store.state.i); + this.streams.reversiStream = new ReversiStreamManager(this, this.store.state.i); }); //#endregion diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 1572fd73ed..cc0a8331ba 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -35,7 +35,7 @@ import MkFavorites from './views/pages/favorites.vue'; import MkUserLists from './views/pages/user-lists.vue'; import MkUserList from './views/pages/user-list.vue'; import MkSettings from './views/pages/settings.vue'; -import MkOthello from './views/pages/othello.vue'; +import MkReversi from './views/pages/reversi.vue'; import MkTag from './views/pages/tag.vue'; import MkShare from './views/pages/share.vue'; @@ -75,8 +75,8 @@ init((launch) => { { path: '/search', component: MkSearch }, { path: '/tags/:tag', component: MkTag }, { path: '/share', component: MkShare }, - { path: '/othello', name: 'othello', component: MkOthello }, - { path: '/othello/:game', component: MkOthello }, + { path: '/reversi', name: 'reversi', component: MkReversi }, + { path: '/reversi/:game', component: MkReversi }, { path: '/@:user', component: MkUser }, { path: '/@:user/followers', component: MkFollowers }, { path: '/@:user/following', component: MkFollowing }, diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index c72b15fb8b..c1ee70d105 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -45,8 +45,8 @@ export default Vue.extend({ this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); + this.connection.on('reversi_invited', this.onReversiInvited); + this.connection.on('reversi_no_invites', this.onReversiNoInvites); const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000; const isHisasiburi = ago >= 3600; @@ -98,16 +98,16 @@ export default Vue.extend({ }, beforeDestroy() { if (this.$store.getters.isSignedIn) { - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); + this.connection.off('reversi_invited', this.onReversiInvited); + this.connection.off('reversi_no_invites', this.onReversiNoInvites); (this as any).os.stream.dispose(this.connectionId); } }, methods: { - onOthelloInvited() { + onReversiInvited() { this.hasGameInvitation = true; }, - onOthelloNoInvites() { + onReversiNoInvites() { this.hasGameInvitation = false; } } diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 80f60e4232..bb7a2f558c 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -19,7 +19,7 @@
  • %fa:R bell%%i18n:@notifications%%fa:angle-right%
  • %fa:R comments%%i18n:@messaging%%fa:angle-right%
  • %fa:R envelope%%i18n:@follow-requests%%fa:angle-right%
  • -
  • %fa:gamepad%%i18n:@game%%fa:angle-right%
  • +
  • %fa:gamepad%%i18n:@game%%fa:angle-right%
    • %fa:R calendar-alt%%i18n:@widgets%%fa:angle-right%
    • @@ -66,14 +66,14 @@ export default Vue.extend({ this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); - this.connection.on('othello_invited', this.onOthelloInvited); - this.connection.on('othello_no_invites', this.onOthelloNoInvites); + this.connection.on('reversi_invited', this.onReversiInvited); + this.connection.on('reversi_no_invites', this.onReversiNoInvites); } }, beforeDestroy() { if (this.$store.getters.isSignedIn) { - this.connection.off('othello_invited', this.onOthelloInvited); - this.connection.off('othello_no_invites', this.onOthelloNoInvites); + this.connection.off('reversi_invited', this.onReversiInvited); + this.connection.off('reversi_no_invites', this.onReversiNoInvites); (this as any).os.stream.dispose(this.connectionId); } }, @@ -83,10 +83,10 @@ export default Vue.extend({ if (query == null || query == '') return; this.$router.push('/search?q=' + encodeURIComponent(query)); }, - onOthelloInvited() { + onReversiInvited() { this.hasGameInvitation = true; }, - onOthelloNoInvites() { + onReversiNoInvites() { this.hasGameInvitation = false; }, dark() { diff --git a/src/client/app/mobile/views/pages/othello.vue b/src/client/app/mobile/views/pages/othello.vue deleted file mode 100644 index e04e583c20..0000000000 --- a/src/client/app/mobile/views/pages/othello.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/src/client/app/mobile/views/pages/reversi.vue b/src/client/app/mobile/views/pages/reversi.vue new file mode 100644 index 0000000000..e2f0db6d87 --- /dev/null +++ b/src/client/app/mobile/views/pages/reversi.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/client/assets/othello-put-me.mp3 b/src/client/assets/othello-put-me.mp3 deleted file mode 100644 index 4e0e72091c..0000000000 Binary files a/src/client/assets/othello-put-me.mp3 and /dev/null differ diff --git a/src/client/assets/othello-put-you.mp3 b/src/client/assets/othello-put-you.mp3 deleted file mode 100644 index 9244189c2d..0000000000 Binary files a/src/client/assets/othello-put-you.mp3 and /dev/null differ diff --git a/src/client/assets/reversi-put-me.mp3 b/src/client/assets/reversi-put-me.mp3 new file mode 100644 index 0000000000..4e0e72091c Binary files /dev/null and b/src/client/assets/reversi-put-me.mp3 differ diff --git a/src/client/assets/reversi-put-you.mp3 b/src/client/assets/reversi-put-you.mp3 new file mode 100644 index 0000000000..9244189c2d Binary files /dev/null and b/src/client/assets/reversi-put-you.mp3 differ diff --git a/src/config/types.ts b/src/config/types.ts index 62f63d4a3c..49eeac508b 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -60,7 +60,7 @@ export type Source = { hook_secret: string; username: string; }; - othello_ai?: { + reversi_ai?: { id: string; i: string; }; diff --git a/src/models/othello-game.ts b/src/models/othello-game.ts deleted file mode 100644 index 1dd375d2e5..0000000000 --- a/src/models/othello-game.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import { IUser, pack as packUser } from './user'; - -const OthelloGame = db.get('othelloGames'); -export default OthelloGame; - -export interface IOthelloGame { - _id: mongo.ObjectID; - createdAt: Date; - startedAt: Date; - user1Id: mongo.ObjectID; - user2Id: mongo.ObjectID; - user1Accepted: boolean; - user2Accepted: boolean; - - /** - * どちらのプレイヤーが先行(黒)か - * 1 ... user1 - * 2 ... user2 - */ - black: number; - - isStarted: boolean; - isEnded: boolean; - winnerId: mongo.ObjectID; - logs: Array<{ - at: Date; - color: boolean; - pos: number; - }>; - settings: { - map: string[]; - bw: string | number; - isLlotheo: boolean; - canPutEverywhere: boolean; - loopedBoard: boolean; - }; - form1: any; - form2: any; - - // ログのposを文字列としてすべて連結したもののCRC32値 - crc32: string; -} - -/** - * Pack an othello game for API response - */ -export const pack = ( - game: any, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean - } -) => new Promise(async (resolve, reject) => { - const opts = Object.assign({ - detail: true - }, options); - - let _game: any; - - // Populate the game if 'game' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(game)) { - _game = await OthelloGame.findOne({ - _id: game - }); - } else if (typeof game === 'string') { - _game = await OthelloGame.findOne({ - _id: new mongo.ObjectID(game) - }); - } else { - _game = deepcopy(game); - } - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - // Rename _id to id - _game.id = _game._id; - delete _game._id; - - if (opts.detail === false) { - delete _game.logs; - delete _game.settings.map; - } else { - // 互換性のため - if (_game.settings.map.hasOwnProperty('size')) { - _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); - } - } - - // Populate user - _game.user1 = await packUser(_game.user1Id, meId); - _game.user2 = await packUser(_game.user2Id, meId); - if (_game.winnerId) { - _game.winner = await packUser(_game.winnerId, meId); - } else { - _game.winner = null; - } - - resolve(_game); -}); diff --git a/src/models/othello-matching.ts b/src/models/othello-matching.ts deleted file mode 100644 index 0efba3ae5d..0000000000 --- a/src/models/othello-matching.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import { IUser, pack as packUser } from './user'; - -const Matching = db.get('othelloMatchings'); -export default Matching; - -export interface IMatching { - _id: mongo.ObjectID; - createdAt: Date; - parentId: mongo.ObjectID; - childId: mongo.ObjectID; -} - -/** - * Pack an othello matching for API response - */ -export const pack = ( - matching: any, - me?: string | mongo.ObjectID | IUser -) => new Promise(async (resolve, reject) => { - - // Me - const meId: mongo.ObjectID = me - ? mongo.ObjectID.prototype.isPrototypeOf(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - const _matching = deepcopy(matching); - - // Rename _id to id - _matching.id = _matching._id; - delete _matching._id; - - // Populate user - _matching.parent = await packUser(_matching.parentId, meId); - _matching.child = await packUser(_matching.childId, meId); - - resolve(_matching); -}); diff --git a/src/models/reversi-game.ts b/src/models/reversi-game.ts new file mode 100644 index 0000000000..16b9ab0d4e --- /dev/null +++ b/src/models/reversi-game.ts @@ -0,0 +1,109 @@ +import * as mongo from 'mongodb'; +import * as deepcopy from 'deepcopy'; +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const ReversiGame = db.get('reversiGames'); +export default ReversiGame; + +export interface IReversiGame { + _id: mongo.ObjectID; + createdAt: Date; + startedAt: Date; + user1Id: mongo.ObjectID; + user2Id: mongo.ObjectID; + user1Accepted: boolean; + user2Accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + black: number; + + isStarted: boolean; + isEnded: boolean; + winnerId: mongo.ObjectID; + logs: Array<{ + at: Date; + color: boolean; + pos: number; + }>; + settings: { + map: string[]; + bw: string | number; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + }; + form1: any; + form2: any; + + // ログのposを文字列としてすべて連結したもののCRC32値 + crc32: string; +} + +/** + * Pack an reversi game for API response + */ +export const pack = ( + game: any, + me?: string | mongo.ObjectID | IUser, + options?: { + detail?: boolean + } +) => new Promise(async (resolve, reject) => { + const opts = Object.assign({ + detail: true + }, options); + + let _game: any; + + // Populate the game if 'game' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(game)) { + _game = await ReversiGame.findOne({ + _id: game + }); + } else if (typeof game === 'string') { + _game = await ReversiGame.findOne({ + _id: new mongo.ObjectID(game) + }); + } else { + _game = deepcopy(game); + } + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + // Rename _id to id + _game.id = _game._id; + delete _game._id; + + if (opts.detail === false) { + delete _game.logs; + delete _game.settings.map; + } else { + // 互換性のため + if (_game.settings.map.hasOwnProperty('size')) { + _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); + } + } + + // Populate user + _game.user1 = await packUser(_game.user1Id, meId); + _game.user2 = await packUser(_game.user2Id, meId); + if (_game.winnerId) { + _game.winner = await packUser(_game.winnerId, meId); + } else { + _game.winner = null; + } + + resolve(_game); +}); diff --git a/src/models/reversi-matching.ts b/src/models/reversi-matching.ts new file mode 100644 index 0000000000..5efa5d76ad --- /dev/null +++ b/src/models/reversi-matching.ts @@ -0,0 +1,44 @@ +import * as mongo from 'mongodb'; +import * as deepcopy from 'deepcopy'; +import db from '../db/mongodb'; +import { IUser, pack as packUser } from './user'; + +const Matching = db.get('reversiMatchings'); +export default Matching; + +export interface IMatching { + _id: mongo.ObjectID; + createdAt: Date; + parentId: mongo.ObjectID; + childId: mongo.ObjectID; +} + +/** + * Pack an reversi matching for API response + */ +export const pack = ( + matching: any, + me?: string | mongo.ObjectID | IUser +) => new Promise(async (resolve, reject) => { + + // Me + const meId: mongo.ObjectID = me + ? mongo.ObjectID.prototype.isPrototypeOf(me) + ? me as mongo.ObjectID + : typeof me === 'string' + ? new mongo.ObjectID(me) + : (me as IUser)._id + : null; + + const _matching = deepcopy(matching); + + // Rename _id to id + _matching.id = _matching._id; + delete _matching._id; + + // Populate user + _matching.parent = await packUser(_matching.parentId, meId); + _matching.child = await packUser(_matching.childId, meId); + + resolve(_matching); +}); diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts deleted file mode 100644 index e4d0cfdd33..0000000000 --- a/src/othello/ai/back.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * -AI- - * Botのバックエンド(思考を担当) - * - * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから - * 切断されてしまうので、別々のプロセスで行うようにします - */ - -import * as request from 'request-promise-native'; -import Othello, { Color } from '../core'; -import conf from '../../config'; -import getUserName from '../../renderers/get-user-name'; - -let game; -let form; - -/** - * BotアカウントのユーザーID - */ -const id = conf.othello_ai.id; - -/** - * BotアカウントのAPIキー - */ -const i = conf.othello_ai.i; - -let note; - -process.on('message', async msg => { - // 親プロセスからデータをもらう - if (msg.type == '_init_') { - game = msg.game; - form = msg.form; - } - - // フォームが更新されたとき - if (msg.type == 'update-form') { - form.find(i => i.id == msg.body.id).value = msg.body.value; - } - - // ゲームが始まったとき - if (msg.type == 'started') { - onGameStarted(msg.body); - - //#region TLに投稿する - const game = msg.body; - const url = `${conf.url}/othello/${game.id}`; - const user = game.user1Id == id ? game.user2 : game.user1; - const isSettai = form[0].value === 0; - const text = isSettai - ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!` - : `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; - - const res = await request.post(`${conf.api_url}/notes/create`, { - json: { i, - text: `${text}\n→[観戦する](${url})` - } - }); - - note = res.createdNote; - //#endregion - } - - // ゲームが終了したとき - if (msg.type == 'ended') { - // ストリームから切断 - process.send({ - type: 'close' - }); - - //#region TLに投稿する - const user = game.user1Id == id ? game.user2 : game.user1; - const isSettai = form[0].value === 0; - const text = isSettai - ? msg.body.winnerId === null - ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...` - : msg.body.winnerId == id - ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` - : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` - : msg.body.winnerId === null - ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~` - : msg.body.winnerId == id - ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪` - : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`; - - await request.post(`${conf.api_url}/notes/create`, { - json: { i, - renoteId: note.id, - text: text - } - }); - //#endregion - - process.exit(); - } - - // 打たれたとき - if (msg.type == 'set') { - onSet(msg.body); - } -}); - -let o: Othello; -let botColor: Color; - -// 各マスの強さ -let cellWeights; - -/** - * ゲーム開始時 - * @param g ゲーム情報 - */ -function onGameStarted(g) { - game = g; - - // オセロエンジン初期化 - o = new Othello(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard - }); - - // 各マスの価値を計算しておく - cellWeights = o.map.map((pix, i) => { - if (pix == 'null') return 0; - const [x, y] = o.transformPosToXy(i); - let count = 0; - const get = (x, y) => { - if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null'; - return o.mapDataGet(o.transformXyToPos(x, y)); - }; - - if (get(x , y - 1) == 'null') count++; - if (get(x + 1, y - 1) == 'null') count++; - if (get(x + 1, y ) == 'null') count++; - if (get(x + 1, y + 1) == 'null') count++; - if (get(x , y + 1) == 'null') count++; - if (get(x - 1, y + 1) == 'null') count++; - if (get(x - 1, y ) == 'null') count++; - if (get(x - 1, y - 1) == 'null') count++; - //return Math.pow(count, 3); - return count >= 4 ? 1 : 0; - }); - - botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2; - - if (botColor) { - think(); - } -} - -function onSet(x) { - o.put(x.color, x.pos); - - if (x.next === botColor) { - think(); - } -} - -const db = {}; - -function think() { - console.log('Thinking...'); - console.time('think'); - - const isSettai = form[0].value === 0; - - // 接待モードのときは、全力(5手先読みくらい)で負けるようにする - const maxDepth = isSettai ? 5 : form[0].value; - - /** - * Botにとってある局面がどれだけ有利か取得する - */ - function staticEval() { - let score = o.canPutSomewhere(botColor).length; - - cellWeights.forEach((weight, i) => { - // 係数 - const coefficient = 30; - weight = weight * coefficient; - - const stone = o.board[i]; - if (stone === botColor) { - // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する - score += weight; - } else if (stone !== null) { - score -= weight; - } - }); - - // ロセオならスコアを反転 - if (game.settings.isLlotheo) score = -score; - - // 接待ならスコアを反転 - if (isSettai) score = -score; - - return score; - } - - /** - * αβ法での探索 - */ - const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { - // 試し打ち - o.put(o.turn, pos); - - const key = o.board.toString(); - let cache = db[key]; - if (cache) { - if (alpha >= cache.upper) { - o.undo(); - return cache.upper; - } - if (beta <= cache.lower) { - o.undo(); - return cache.lower; - } - alpha = Math.max(alpha, cache.lower); - beta = Math.min(beta, cache.upper); - } else { - cache = { - upper: Infinity, - lower: -Infinity - }; - } - - const isBotTurn = o.turn === botColor; - - // 勝った - if (o.turn === null) { - const winner = o.winner; - - // 勝つことによる基本スコア - const base = 10000; - - let score; - - if (game.settings.isLlotheo) { - // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); - } else { - // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); - } - - // 巻き戻し - o.undo(); - - // 接待なら自分が負けた方が高スコア - return isSettai - ? winner !== botColor ? score : -score - : winner === botColor ? score : -score; - } - - if (depth === maxDepth) { - // 静的に評価 - const score = staticEval(); - - // 巻き戻し - o.undo(); - - return score; - } else { - const cans = o.canPutSomewhere(o.turn); - - let value = isBotTurn ? -Infinity : Infinity; - let a = alpha; - let b = beta; - - // 次のターンのプレイヤーにとって最も良い手を取得 - for (const p of cans) { - if (isBotTurn) { - const score = dive(p, a, beta, depth + 1); - value = Math.max(value, score); - a = Math.max(a, value); - if (value >= beta) break; - } else { - const score = dive(p, alpha, b, depth + 1); - value = Math.min(value, score); - b = Math.min(b, value); - if (value <= alpha) break; - } - } - - // 巻き戻し - o.undo(); - - if (value <= alpha) { - cache.upper = value; - } else if (value >= beta) { - cache.lower = value; - } else { - cache.upper = value; - cache.lower = value; - } - - db[key] = cache; - - return value; - } - }; - - /** - * αβ法での探索(キャッシュ無し)(デバッグ用) - */ - const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { - // 試し打ち - o.put(o.turn, pos); - - const isBotTurn = o.turn === botColor; - - // 勝った - if (o.turn === null) { - const winner = o.winner; - - // 勝つことによる基本スコア - const base = 10000; - - let score; - - if (game.settings.isLlotheo) { - // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); - } else { - // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する - score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); - } - - // 巻き戻し - o.undo(); - - // 接待なら自分が負けた方が高スコア - return isSettai - ? winner !== botColor ? score : -score - : winner === botColor ? score : -score; - } - - if (depth === maxDepth) { - // 静的に評価 - const score = staticEval(); - - // 巻き戻し - o.undo(); - - return score; - } else { - const cans = o.canPutSomewhere(o.turn); - - // 次のターンのプレイヤーにとって最も良い手を取得 - for (const p of cans) { - if (isBotTurn) { - alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1)); - } else { - beta = Math.min(beta, dive2(p, alpha, beta, depth + 1)); - } - if (alpha >= beta) break; - } - - // 巻き戻し - o.undo(); - - return isBotTurn ? alpha : beta; - } - }; - - const cans = o.canPutSomewhere(botColor); - const scores = cans.map(p => dive(p)); - const pos = cans[scores.indexOf(Math.max(...scores))]; - - console.log('Thinked:', pos); - console.timeEnd('think'); - - process.send({ - type: 'put', - pos - }); -} diff --git a/src/othello/ai/front.ts b/src/othello/ai/front.ts deleted file mode 100644 index ff74b7216e..0000000000 --- a/src/othello/ai/front.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * -AI- - * Botのフロントエンド(ストリームとの対話を担当) - * - * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから - * 切断されてしまうので、別々のプロセスで行うようにします - */ - -import * as childProcess from 'child_process'; -const WebSocket = require('ws'); -import * as ReconnectingWebSocket from 'reconnecting-websocket'; -import * as request from 'request-promise-native'; -import conf from '../../config'; - -// 設定 //////////////////////////////////////////////////////// - -/** - * BotアカウントのAPIキー - */ -const i = conf.othello_ai.i; - -/** - * BotアカウントのユーザーID - */ -const id = conf.othello_ai.id; - -//////////////////////////////////////////////////////////////// - -/** - * ホームストリーム - */ -const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, { - constructor: WebSocket -}); - -homeStream.on('open', () => { - console.log('home stream opened'); -}); - -homeStream.on('close', () => { - console.log('home stream closed'); -}); - -homeStream.on('message', message => { - const msg = JSON.parse(message.toString()); - - // タイムライン上でなんか言われたまたは返信されたとき - if (msg.type == 'mention' || msg.type == 'reply') { - const note = msg.body; - - if (note.userId == id) return; - - // リアクションする - request.post(`${conf.api_url}/notes/reactions/create`, { - json: { i, - noteId: note.id, - reaction: 'love' - } - }); - - if (note.text) { - if (note.text.indexOf('オセロ') > -1) { - request.post(`${conf.api_url}/notes/create`, { - json: { i, - replyId: note.id, - text: '良いですよ~' - } - }); - - invite(note.userId); - } - } - } - - // メッセージでなんか言われたとき - if (msg.type == 'messaging_message') { - const message = msg.body; - if (message.text) { - if (message.text.indexOf('オセロ') > -1) { - request.post(`${conf.api_url}/messaging/messages/create`, { - json: { i, - userId: message.userId, - text: '良いですよ~' - } - }); - - invite(message.userId); - } - } - } -}); - -// ユーザーを対局に誘う -function invite(userId) { - request.post(`${conf.api_url}/othello/match`, { - json: { i, - userId: userId - } - }); -} - -/** - * オセロストリーム - */ -const othelloStream = new ReconnectingWebSocket(`${conf.ws_url}/othello?i=${i}`, undefined, { - constructor: WebSocket -}); - -othelloStream.on('open', () => { - console.log('othello stream opened'); -}); - -othelloStream.on('close', () => { - console.log('othello stream closed'); -}); - -othelloStream.on('message', message => { - const msg = JSON.parse(message.toString()); - - // 招待されたとき - if (msg.type == 'invited') { - onInviteMe(msg.body.parent); - } - - // マッチしたとき - if (msg.type == 'matched') { - gameStart(msg.body); - } -}); - -/** - * ゲーム開始 - * @param game ゲーム情報 - */ -function gameStart(game) { - // ゲームストリームに接続 - const gw = new ReconnectingWebSocket(`${conf.ws_url}/othello-game?i=${i}&game=${game.id}`, undefined, { - constructor: WebSocket - }); - - gw.on('open', () => { - console.log('othello game stream opened'); - - // フォーム - const form = [{ - id: 'strength', - type: 'radio', - label: '強さ', - value: 2, - items: [{ - label: '接待', - value: 0 - }, { - label: '弱', - value: 1 - }, { - label: '中', - value: 2 - }, { - label: '強', - value: 3 - }, { - label: '最強', - value: 5 - }] - }]; - - //#region バックエンドプロセス開始 - const ai = childProcess.fork(__dirname + '/back.js'); - - // バックエンドプロセスに情報を渡す - ai.send({ - type: '_init_', - game, - form - }); - - ai.on('message', msg => { - if (msg.type == 'put') { - gw.send(JSON.stringify({ - type: 'set', - pos: msg.pos - })); - } else if (msg.type == 'close') { - gw.close(); - } - }); - - // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える - gw.on('message', message => { - const msg = JSON.parse(message.toString()); - ai.send(msg); - }); - //#endregion - - // フォーム初期化 - setTimeout(() => { - gw.send(JSON.stringify({ - type: 'init-form', - body: form - })); - }, 1000); - - // どんな設定内容の対局でも受け入れる - setTimeout(() => { - gw.send(JSON.stringify({ - type: 'accept' - })); - }, 2000); - }); - - gw.on('close', () => { - console.log('othello game stream closed'); - }); -} - -/** - * オセロの対局に招待されたとき - * @param inviter 誘ってきたユーザー - */ -async function onInviteMe(inviter) { - console.log(`Someone invited me: @${inviter.username}`); - - // 承認 - const game = await request.post(`${conf.api_url}/othello/match`, { - json: { - i, - userId: inviter.id - } - }); - - gameStart(game); -} diff --git a/src/othello/ai/index.ts b/src/othello/ai/index.ts deleted file mode 100644 index 5cd1db82da..0000000000 --- a/src/othello/ai/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./front'); diff --git a/src/othello/core.ts b/src/othello/core.ts deleted file mode 100644 index 217066d375..0000000000 --- a/src/othello/core.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * true ... 黒 - * false ... 白 - */ -export type Color = boolean; -const BLACK = true; -const WHITE = false; - -export type MapPixel = 'null' | 'empty'; - -export type Options = { - isLlotheo: boolean; - canPutEverywhere: boolean; - loopedBoard: boolean; -}; - -export type Undo = { - /** - * 色 - */ - color: Color, - - /** - * どこに打ったか - */ - pos: number; - - /** - * 反転した石の位置の配列 - */ - effects: number[]; - - /** - * ターン - */ - turn: Color; -}; - -/** - * オセロエンジン - */ -export default class Othello { - public map: MapPixel[]; - public mapWidth: number; - public mapHeight: number; - public board: Color[]; - public turn: Color = BLACK; - public opts: Options; - - public prevPos = -1; - public prevColor: Color = null; - - private logs: Undo[] = []; - - /** - * ゲームを初期化します - */ - constructor(map: string[], opts: Options) { - //#region binds - this.put = this.put.bind(this); - //#endregion - - //#region Options - this.opts = opts; - if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; - if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; - if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; - //#endregion - - //#region Parse map data - this.mapWidth = map[0].length; - this.mapHeight = map.length; - const mapData = map.join(''); - - this.board = mapData.split('').map(d => { - if (d == '-') return null; - if (d == 'b') return BLACK; - if (d == 'w') return WHITE; - return undefined; - }); - - this.map = mapData.split('').map(d => { - if (d == '-' || d == 'b' || d == 'w') return 'empty'; - return 'null'; - }); - //#endregion - - // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある - if (this.canPutSomewhere(BLACK).length == 0) { - if (this.canPutSomewhere(WHITE).length == 0) { - this.turn = null; - } else { - this.turn = WHITE; - } - } - } - - /** - * 黒石の数 - */ - public get blackCount() { - return this.board.filter(x => x === BLACK).length; - } - - /** - * 白石の数 - */ - public get whiteCount() { - return this.board.filter(x => x === WHITE).length; - } - - /** - * 黒石の比率 - */ - public get blackP() { - if (this.blackCount == 0 && this.whiteCount == 0) return 0; - return this.blackCount / (this.blackCount + this.whiteCount); - } - - /** - * 白石の比率 - */ - public get whiteP() { - if (this.blackCount == 0 && this.whiteCount == 0) return 0; - return this.whiteCount / (this.blackCount + this.whiteCount); - } - - public transformPosToXy(pos: number): number[] { - const x = pos % this.mapWidth; - const y = Math.floor(pos / this.mapWidth); - return [x, y]; - } - - public transformXyToPos(x: number, y: number): number { - return x + (y * this.mapWidth); - } - - /** - * 指定のマスに石を打ちます - * @param color 石の色 - * @param pos 位置 - */ - public put(color: Color, pos: number) { - this.prevPos = pos; - this.prevColor = color; - - this.board[pos] = color; - - // 反転させられる石を取得 - const effects = this.effects(color, pos); - - // 反転させる - for (const pos of effects) { - this.board[pos] = color; - } - - const turn = this.turn; - - this.logs.push({ - color, - pos, - effects, - turn - }); - - this.calcTurn(); - } - - private calcTurn() { - // ターン計算 - if (this.canPutSomewhere(!this.prevColor).length > 0) { - this.turn = !this.prevColor; - } else if (this.canPutSomewhere(this.prevColor).length > 0) { - this.turn = this.prevColor; - } else { - this.turn = null; - } - } - - public undo() { - const undo = this.logs.pop(); - this.prevColor = undo.color; - this.prevPos = undo.pos; - this.board[undo.pos] = null; - for (const pos of undo.effects) { - const color = this.board[pos]; - this.board[pos] = !color; - } - this.turn = undo.turn; - } - - /** - * 指定した位置のマップデータのマスを取得します - * @param pos 位置 - */ - public mapDataGet(pos: number): MapPixel { - const [x, y] = this.transformPosToXy(pos); - if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null'; - return this.map[pos]; - } - - /** - * 打つことができる場所を取得します - */ - public canPutSomewhere(color: Color): number[] { - const result = []; - - this.board.forEach((x, i) => { - if (this.canPut(color, i)) result.push(i); - }); - - return result; - } - - /** - * 指定のマスに石を打つことができるかどうかを取得します - * @param color 自分の色 - * @param pos 位置 - */ - public canPut(color: Color, pos: number): boolean { - // 既に石が置いてある場所には打てない - if (this.board[pos] !== null) return false; - - if (this.opts.canPutEverywhere) { - // 挟んでなくても置けるモード - return this.mapDataGet(pos) == 'empty'; - } else { - // 相手の石を1つでも反転させられるか - return this.effects(color, pos).length !== 0; - } - } - - /** - * 指定のマスに石を置いた時の、反転させられる石を取得します - * @param color 自分の色 - * @param pos 位置 - */ - public effects(color: Color, pos: number): number[] { - const enemyColor = !color; - - // ひっくり返せる石(の位置)リスト - let stones = []; - - const initPos = pos; - - // 走査 - const iterate = (fn: (i: number) => number[]) => { - let i = 1; - const found = []; - - while (true) { - let [x, y] = fn(i); - - // 座標が指し示す位置がボード外に出たとき - if (this.opts.loopedBoard) { - if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth); - if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight); - if (x >= this.mapWidth ) x = x % this.mapWidth; - if (y >= this.mapHeight) y = y % this.mapHeight; - - // for debug - //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) { - // console.log(x, y); - //} - - // 一周して自分に帰ってきたら - if (this.transformXyToPos(x, y) == initPos) { - // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、 - // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります) - // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます - // (あと無効な方がゲームとしておもしろそうだった) - stones = stones.concat(found); - break; - } - } else { - if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break; - } - - const pos = this.transformXyToPos(x, y); - - //#region 「配置不能」マスに当たった場合走査終了 - const pixel = this.mapDataGet(pos); - if (pixel == 'null') break; - //#endregion - - // 石取得 - const stone = this.board[pos]; - - // 石が置かれていないマスなら走査終了 - if (stone === null) break; - - // 相手の石なら「ひっくり返せるかもリスト」に入れておく - if (stone === enemyColor) found.push(pos); - - // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了 - if (stone === color) { - stones = stones.concat(found); - break; - } - - i++; - } - }; - - const [x, y] = this.transformPosToXy(pos); - - iterate(i => [x , y - i]); // 上 - iterate(i => [x + i, y - i]); // 右上 - iterate(i => [x + i, y ]); // 右 - iterate(i => [x + i, y + i]); // 右下 - iterate(i => [x , y + i]); // 下 - iterate(i => [x - i, y + i]); // 左下 - iterate(i => [x - i, y ]); // 左 - iterate(i => [x - i, y - i]); // 左上 - - return stones; - } - - /** - * ゲームが終了したか否か - */ - public get isEnded(): boolean { - return this.turn === null; - } - - /** - * ゲームの勝者 (null = 引き分け) - */ - public get winner(): Color { - if (!this.isEnded) return undefined; - - if (this.blackCount == this.whiteCount) return null; - - if (this.opts.isLlotheo) { - return this.blackCount > this.whiteCount ? WHITE : BLACK; - } else { - return this.blackCount > this.whiteCount ? BLACK : WHITE; - } - } -} diff --git a/src/othello/maps.ts b/src/othello/maps.ts deleted file mode 100644 index 68e5a446f1..0000000000 --- a/src/othello/maps.ts +++ /dev/null @@ -1,911 +0,0 @@ -/** - * 組み込みマップ定義 - * - * データ値: - * (スペース) ... マス無し - * - ... マス - * b ... 初期配置される黒石 - * w ... 初期配置される白石 - */ - -export type Map = { - name?: string; - category?: string; - author?: string; - data: string[]; -}; - -export const fourfour: Map = { - name: '4x4', - category: '4x4', - data: [ - '----', - '-wb-', - '-bw-', - '----' - ] -}; - -export const sixsix: Map = { - name: '6x6', - category: '6x6', - data: [ - '------', - '------', - '--wb--', - '--bw--', - '------', - '------' - ] -}; - -export const roundedSixsix: Map = { - name: '6x6 rounded', - category: '6x6', - author: 'syuilo', - data: [ - ' ---- ', - '------', - '--wb--', - '--bw--', - '------', - ' ---- ' - ] -}; - -export const roundedSixsix2: Map = { - name: '6x6 rounded 2', - category: '6x6', - author: 'syuilo', - data: [ - ' -- ', - ' ---- ', - '--wb--', - '--bw--', - ' ---- ', - ' -- ' - ] -}; - -export const eighteight: Map = { - name: '8x8', - category: '8x8', - data: [ - '--------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const eighteightH1: Map = { - name: '8x8 handicap 1', - category: '8x8', - data: [ - 'b-------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const eighteightH2: Map = { - name: '8x8 handicap 2', - category: '8x8', - data: [ - 'b-------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '-------b' - ] -}; - -export const eighteightH3: Map = { - name: '8x8 handicap 3', - category: '8x8', - data: [ - 'b------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '-------b' - ] -}; - -export const eighteightH4: Map = { - name: '8x8 handicap 4', - category: '8x8', - data: [ - 'b------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - 'b------b' - ] -}; - -export const eighteightH12: Map = { - name: '8x8 handicap 12', - category: '8x8', - data: [ - 'bb----bb', - 'b------b', - '--------', - '---wb---', - '---bw---', - '--------', - 'b------b', - 'bb----bb' - ] -}; - -export const eighteightH16: Map = { - name: '8x8 handicap 16', - category: '8x8', - data: [ - 'bbb---bb', - 'b------b', - '-------b', - '---wb---', - '---bw---', - 'b-------', - 'b------b', - 'bb---bbb' - ] -}; - -export const eighteightH20: Map = { - name: '8x8 handicap 20', - category: '8x8', - data: [ - 'bbb--bbb', - 'b------b', - 'b------b', - '---wb---', - '---bw---', - 'b------b', - 'b------b', - 'bbb---bb' - ] -}; - -export const eighteightH28: Map = { - name: '8x8 handicap 28', - category: '8x8', - data: [ - 'bbbbbbbb', - 'b------b', - 'b------b', - 'b--wb--b', - 'b--bw--b', - 'b------b', - 'b------b', - 'bbbbbbbb' - ] -}; - -export const roundedEighteight: Map = { - name: '8x8 rounded', - category: '8x8', - author: 'syuilo', - data: [ - ' ------ ', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - ' ------ ' - ] -}; - -export const roundedEighteight2: Map = { - name: '8x8 rounded 2', - category: '8x8', - author: 'syuilo', - data: [ - ' ---- ', - ' ------ ', - '--------', - '---wb---', - '---bw---', - '--------', - ' ------ ', - ' ---- ' - ] -}; - -export const roundedEighteight3: Map = { - name: '8x8 rounded 3', - category: '8x8', - author: 'syuilo', - data: [ - ' -- ', - ' ---- ', - ' ------ ', - '---wb---', - '---bw---', - ' ------ ', - ' ---- ', - ' -- ' - ] -}; - -export const eighteightWithNotch: Map = { - name: '8x8 with notch', - category: '8x8', - author: 'syuilo', - data: [ - '--- ---', - '--------', - '--------', - ' --wb-- ', - ' --bw-- ', - '--------', - '--------', - '--- ---' - ] -}; - -export const eighteightWithSomeHoles: Map = { - name: '8x8 with some holes', - category: '8x8', - author: 'syuilo', - data: [ - '--- ----', - '----- --', - '-- -----', - '---wb---', - '---bw- -', - ' -------', - '--- ----', - '--------' - ] -}; - -export const circle: Map = { - name: 'Circle', - category: '8x8', - author: 'syuilo', - data: [ - ' -- ', - ' ------ ', - ' ------ ', - '---wb---', - '---bw---', - ' ------ ', - ' ------ ', - ' -- ' - ] -}; - -export const smile: Map = { - name: 'Smile', - category: '8x8', - author: 'syuilo', - data: [ - ' ------ ', - '--------', - '-- -- --', - '---wb---', - '-- bw --', - '--- ---', - '--------', - ' ------ ' - ] -}; - -export const window: Map = { - name: 'Window', - category: '8x8', - author: 'syuilo', - data: [ - '--------', - '- -- -', - '- -- -', - '---wb---', - '---bw---', - '- -- -', - '- -- -', - '--------' - ] -}; - -export const reserved: Map = { - name: 'Reserved', - category: '8x8', - author: 'Aya', - data: [ - 'w------b', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - 'b------w' - ] -}; - -export const x: Map = { - name: 'X', - category: '8x8', - author: 'Aya', - data: [ - 'w------b', - '-w----b-', - '--w--b--', - '---wb---', - '---bw---', - '--b--w--', - '-b----w-', - 'b------w' - ] -}; - -export const parallel: Map = { - name: 'Parallel', - category: '8x8', - author: 'Aya', - data: [ - '--------', - '--------', - '--------', - '---bb---', - '---ww---', - '--------', - '--------', - '--------' - ] -}; - -export const lackOfBlack: Map = { - name: 'Lack of Black', - category: '8x8', - data: [ - '--------', - '--------', - '--------', - '---w----', - '---bw---', - '--------', - '--------', - '--------' - ] -}; - -export const squareParty: Map = { - name: 'Square Party', - category: '8x8', - author: 'syuilo', - data: [ - '--------', - '-wwwbbb-', - '-w-wb-b-', - '-wwwbbb-', - '-bbbwww-', - '-b-bw-w-', - '-bbbwww-', - '--------' - ] -}; - -export const minesweeper: Map = { - name: 'Minesweeper', - category: '8x8', - author: 'syuilo', - data: [ - 'b-b--w-w', - '-w-wb-b-', - 'w-b--w-b', - '-b-wb-w-', - '-w-bw-b-', - 'b-w--b-w', - '-b-bw-w-', - 'w-w--b-b' - ] -}; - -export const tenthtenth: Map = { - name: '10x10', - category: '10x10', - data: [ - '----------', - '----------', - '----------', - '----------', - '----wb----', - '----bw----', - '----------', - '----------', - '----------', - '----------' - ] -}; - -export const hole: Map = { - name: 'The Hole', - category: '10x10', - author: 'syuilo', - data: [ - '----------', - '----------', - '--wb--wb--', - '--bw--bw--', - '---- ----', - '---- ----', - '--wb--wb--', - '--bw--bw--', - '----------', - '----------' - ] -}; - -export const grid: Map = { - name: 'Grid', - category: '10x10', - author: 'syuilo', - data: [ - '----------', - '- - -- - -', - '----------', - '- - -- - -', - '----wb----', - '----bw----', - '- - -- - -', - '----------', - '- - -- - -', - '----------' - ] -}; - -export const cross: Map = { - name: 'Cross', - category: '10x10', - author: 'Aya', - data: [ - ' ---- ', - ' ---- ', - ' ---- ', - '----------', - '----wb----', - '----bw----', - '----------', - ' ---- ', - ' ---- ', - ' ---- ' - ] -}; - -export const charX: Map = { - name: 'Char X', - category: '10x10', - author: 'syuilo', - data: [ - '--- ---', - '---- ----', - '----------', - ' -------- ', - ' --wb-- ', - ' --bw-- ', - ' -------- ', - '----------', - '---- ----', - '--- ---' - ] -}; - -export const charY: Map = { - name: 'Char Y', - category: '10x10', - author: 'syuilo', - data: [ - '--- ---', - '---- ----', - '----------', - ' -------- ', - ' --wb-- ', - ' --bw-- ', - ' ------ ', - ' ------ ', - ' ------ ', - ' ------ ' - ] -}; - -export const walls: Map = { - name: 'Walls', - category: '10x10', - author: 'Aya', - data: [ - ' bbbbbbbb ', - 'w--------w', - 'w--------w', - 'w--------w', - 'w---wb---w', - 'w---bw---w', - 'w--------w', - 'w--------w', - 'w--------w', - ' bbbbbbbb ' - ] -}; - -export const cpu: Map = { - name: 'CPU', - category: '10x10', - author: 'syuilo', - data: [ - ' b b b b ', - 'w--------w', - ' -------- ', - 'w--------w', - ' ---wb--- ', - ' ---bw--- ', - 'w--------w', - ' -------- ', - 'w--------w', - ' b b b b ' - ] -}; - -export const checker: Map = { - name: 'Checker', - category: '10x10', - author: 'Aya', - data: [ - '----------', - '----------', - '----------', - '---wbwb---', - '---bwbw---', - '---wbwb---', - '---bwbw---', - '----------', - '----------', - '----------' - ] -}; - -export const japaneseCurry: Map = { - name: 'Japanese curry', - category: '10x10', - author: 'syuilo', - data: [ - 'w-b-b-b-b-', - '-w-b-b-b-b', - 'w-w-b-b-b-', - '-w-w-b-b-b', - 'w-w-wwb-b-', - '-w-wbb-b-b', - 'w-w-w-b-b-', - '-w-w-w-b-b', - 'w-w-w-w-b-', - '-w-w-w-w-b' - ] -}; - -export const mosaic: Map = { - name: 'Mosaic', - category: '10x10', - author: 'syuilo', - data: [ - '- - - - - ', - ' - - - - -', - '- - - - - ', - ' - w w - -', - '- - b b - ', - ' - w w - -', - '- - b b - ', - ' - - - - -', - '- - - - - ', - ' - - - - -', - ] -}; - -export const arena: Map = { - name: 'Arena', - category: '10x10', - author: 'syuilo', - data: [ - '- - -- - -', - ' - - - - ', - '- ------ -', - ' -------- ', - '- --wb-- -', - '- --bw-- -', - ' -------- ', - '- ------ -', - ' - - - - ', - '- - -- - -' - ] -}; - -export const reactor: Map = { - name: 'Reactor', - category: '10x10', - author: 'syuilo', - data: [ - '-w------b-', - 'b- - - -w', - '- --wb-- -', - '---b w---', - '- b wb w -', - '- w bw b -', - '---w b---', - '- --bw-- -', - 'w- - - -b', - '-b------w-' - ] -}; - -export const sixeight: Map = { - name: '6x8', - category: 'Special', - data: [ - '------', - '------', - '------', - '--wb--', - '--bw--', - '------', - '------', - '------' - ] -}; - -export const spark: Map = { - name: 'Spark', - category: 'Special', - author: 'syuilo', - data: [ - ' - - ', - '----------', - ' -------- ', - ' -------- ', - ' ---wb--- ', - ' ---bw--- ', - ' -------- ', - ' -------- ', - '----------', - ' - - ' - ] -}; - -export const islands: Map = { - name: 'Islands', - category: 'Special', - author: 'syuilo', - data: [ - '-------- ', - '---wb--- ', - '---bw--- ', - '-------- ', - ' - - ', - ' - - ', - ' --------', - ' --------', - ' --------', - ' --------' - ] -}; - -export const galaxy: Map = { - name: 'Galaxy', - category: 'Special', - author: 'syuilo', - data: [ - ' ------ ', - ' --www--- ', - ' ------w--- ', - '---bbb--w---', - '--b---b-w-b-', - '-b--wwb-w-b-', - '-b-w-bww--b-', - '-b-w-b---b--', - '---w--bbb---', - ' ---w------ ', - ' ---www-- ', - ' ------ ' - ] -}; - -export const triangle: Map = { - name: 'Triangle', - category: 'Special', - author: 'syuilo', - data: [ - ' -- ', - ' -- ', - ' ---- ', - ' ---- ', - ' --wb-- ', - ' --bw-- ', - ' -------- ', - ' -------- ', - '----------', - '----------' - ] -}; - -export const iphonex: Map = { - name: 'iPhone X', - category: 'Special', - author: 'syuilo', - data: [ - ' -- -- ', - '--------', - '--------', - '--------', - '--------', - '---wb---', - '---bw---', - '--------', - '--------', - '--------', - '--------', - ' ------ ' - ] -}; - -export const dealWithIt: Map = { - name: 'Deal with it!', - category: 'Special', - author: 'syuilo', - data: [ - '------------', - '--w-b-------', - ' --b-w------', - ' --w-b---- ', - ' ------- ' - ] -}; - -export const experiment: Map = { - name: 'Let\'s experiment', - category: 'Special', - author: 'syuilo', - data: [ - ' ------------ ', - '------wb------', - '------bw------', - '--------------', - ' - - ', - '------ ------', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'bbbbbb wwwwww', - 'wwwwww bbbbbb' - ] -}; - -export const bigBoard: Map = { - name: 'Big board', - category: 'Special', - data: [ - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '-------wb-------', - '-------bw-------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------', - '----------------' - ] -}; - -export const twoBoard: Map = { - name: 'Two board', - category: 'Special', - author: 'Aya', - data: [ - '-------- --------', - '-------- --------', - '-------- --------', - '---wb--- ---wb---', - '---bw--- ---bw---', - '-------- --------', - '-------- --------', - '-------- --------' - ] -}; - -export const test1: Map = { - name: 'Test1', - category: 'Test', - data: [ - '--------', - '---wb---', - '---bw---', - '--------' - ] -}; - -export const test2: Map = { - name: 'Test2', - category: 'Test', - data: [ - '------', - '------', - '-b--w-', - '-w--b-', - '-w--b-' - ] -}; - -export const test3: Map = { - name: 'Test3', - category: 'Test', - data: [ - '-w-', - '--w', - 'w--', - '-w-', - '--w', - 'w--', - '-w-', - '--w', - 'w--', - '-w-', - '---', - 'b--', - ] -}; - -export const test4: Map = { - name: 'Test4', - category: 'Test', - data: [ - '-w--b-', - '-w--b-', - '------', - '-w--b-', - '-w--b-' - ] -}; - -// https://misskey.xyz/othello/5aaabf7fe126e10b5216ea09 64 -export const test5: Map = { - name: 'Test5', - category: 'Test', - data: [ - '--wwwwww--', - '--wwwbwwww', - '-bwwbwbwww', - '-bwwwbwbww', - '-bwwbwbwbw', - '-bwbwbwb-w', - 'bwbwwbbb-w', - 'w-wbbbbb--', - '--w-b-w---', - '----------' - ] -}; diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts index 58a6ef49aa..b573b65a65 100644 --- a/src/publishers/stream.ts +++ b/src/publishers/stream.ts @@ -37,12 +37,12 @@ class MisskeyEvent { this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishOthelloStream(userId: ID, type: string, value?: any): void { - this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value); + public publishReversiStream(userId: ID, type: string, value?: any): void { + this.publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishOthelloGameStream(gameId: ID, type: string, value?: any): void { - this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value); + public publishReversiGameStream(gameId: ID, type: string, value?: any): void { + this.publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value); } public publishLocalTimelineStream(note: any): void { @@ -73,5 +73,5 @@ export const publishUserListStream = ev.publishUserListStream.bind(ev); export const publishNoteStream = ev.publishNoteStream.bind(ev); export const publishMessagingStream = ev.publishMessagingStream.bind(ev); export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); -export const publishOthelloStream = ev.publishOthelloStream.bind(ev); -export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev); +export const publishReversiStream = ev.publishReversiStream.bind(ev); +export const publishReversiGameStream = ev.publishReversiGameStream.bind(ev); diff --git a/src/reversi/ai/back.ts b/src/reversi/ai/back.ts new file mode 100644 index 0000000000..42f1a0f4b8 --- /dev/null +++ b/src/reversi/ai/back.ts @@ -0,0 +1,377 @@ +/** + * -AI- + * Botのバックエンド(思考を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as request from 'request-promise-native'; +import Reversi, { Color } from '../core'; +import conf from '../../config'; +import getUserName from '../../renderers/get-user-name'; + +let game; +let form; + +/** + * BotアカウントのユーザーID + */ +const id = conf.reversi_ai.id; + +/** + * BotアカウントのAPIキー + */ +const i = conf.reversi_ai.i; + +let note; + +process.on('message', async msg => { + // 親プロセスからデータをもらう + if (msg.type == '_init_') { + game = msg.game; + form = msg.form; + } + + // フォームが更新されたとき + if (msg.type == 'update-form') { + form.find(i => i.id == msg.body.id).value = msg.body.value; + } + + // ゲームが始まったとき + if (msg.type == 'started') { + onGameStarted(msg.body); + + //#region TLに投稿する + const game = msg.body; + const url = `${conf.url}/reversi/${game.id}`; + const user = game.user1Id == id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!` + : `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; + + const res = await request.post(`${conf.api_url}/notes/create`, { + json: { i, + text: `${text}\n→[観戦する](${url})` + } + }); + + note = res.createdNote; + //#endregion + } + + // ゲームが終了したとき + if (msg.type == 'ended') { + // ストリームから切断 + process.send({ + type: 'close' + }); + + //#region TLに投稿する + const user = game.user1Id == id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? msg.body.winnerId === null + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...` + : msg.body.winnerId == id + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` + : msg.body.winnerId === null + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~` + : msg.body.winnerId == id + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`; + + await request.post(`${conf.api_url}/notes/create`, { + json: { i, + renoteId: note.id, + text: text + } + }); + //#endregion + + process.exit(); + } + + // 打たれたとき + if (msg.type == 'set') { + onSet(msg.body); + } +}); + +let o: Reversi; +let botColor: Color; + +// 各マスの強さ +let cellWeights; + +/** + * ゲーム開始時 + * @param g ゲーム情報 + */ +function onGameStarted(g) { + game = g; + + // リバーシエンジン初期化 + o = new Reversi(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + // 各マスの価値を計算しておく + cellWeights = o.map.map((pix, i) => { + if (pix == 'null') return 0; + const [x, y] = o.transformPosToXy(i); + let count = 0; + const get = (x, y) => { + if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null'; + return o.mapDataGet(o.transformXyToPos(x, y)); + }; + + if (get(x , y - 1) == 'null') count++; + if (get(x + 1, y - 1) == 'null') count++; + if (get(x + 1, y ) == 'null') count++; + if (get(x + 1, y + 1) == 'null') count++; + if (get(x , y + 1) == 'null') count++; + if (get(x - 1, y + 1) == 'null') count++; + if (get(x - 1, y ) == 'null') count++; + if (get(x - 1, y - 1) == 'null') count++; + //return Math.pow(count, 3); + return count >= 4 ? 1 : 0; + }); + + botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2; + + if (botColor) { + think(); + } +} + +function onSet(x) { + o.put(x.color, x.pos); + + if (x.next === botColor) { + think(); + } +} + +const db = {}; + +function think() { + console.log('Thinking...'); + console.time('think'); + + const isSettai = form[0].value === 0; + + // 接待モードのときは、全力(5手先読みくらい)で負けるようにする + const maxDepth = isSettai ? 5 : form[0].value; + + /** + * Botにとってある局面がどれだけ有利か取得する + */ + function staticEval() { + let score = o.canPutSomewhere(botColor).length; + + cellWeights.forEach((weight, i) => { + // 係数 + const coefficient = 30; + weight = weight * coefficient; + + const stone = o.board[i]; + if (stone === botColor) { + // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する + score += weight; + } else if (stone !== null) { + score -= weight; + } + }); + + // ロセオならスコアを反転 + if (game.settings.isLlotheo) score = -score; + + // 接待ならスコアを反転 + if (isSettai) score = -score; + + return score; + } + + /** + * αβ法での探索 + */ + const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const key = o.board.toString(); + let cache = db[key]; + if (cache) { + if (alpha >= cache.upper) { + o.undo(); + return cache.upper; + } + if (beta <= cache.lower) { + o.undo(); + return cache.lower; + } + alpha = Math.max(alpha, cache.lower); + beta = Math.min(beta, cache.upper); + } else { + cache = { + upper: Infinity, + lower: -Infinity + }; + } + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + let value = isBotTurn ? -Infinity : Infinity; + let a = alpha; + let b = beta; + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + const score = dive(p, a, beta, depth + 1); + value = Math.max(value, score); + a = Math.max(a, value); + if (value >= beta) break; + } else { + const score = dive(p, alpha, b, depth + 1); + value = Math.min(value, score); + b = Math.min(b, value); + if (value <= alpha) break; + } + } + + // 巻き戻し + o.undo(); + + if (value <= alpha) { + cache.upper = value; + } else if (value >= beta) { + cache.lower = value; + } else { + cache.upper = value; + cache.lower = value; + } + + db[key] = cache; + + return value; + } + }; + + /** + * αβ法での探索(キャッシュ無し)(デバッグ用) + */ + const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1)); + } else { + beta = Math.min(beta, dive2(p, alpha, beta, depth + 1)); + } + if (alpha >= beta) break; + } + + // 巻き戻し + o.undo(); + + return isBotTurn ? alpha : beta; + } + }; + + const cans = o.canPutSomewhere(botColor); + const scores = cans.map(p => dive(p)); + const pos = cans[scores.indexOf(Math.max(...scores))]; + + console.log('Thinked:', pos); + console.timeEnd('think'); + + process.send({ + type: 'put', + pos + }); +} diff --git a/src/reversi/ai/front.ts b/src/reversi/ai/front.ts new file mode 100644 index 0000000000..25ee43225a --- /dev/null +++ b/src/reversi/ai/front.ts @@ -0,0 +1,233 @@ +/** + * -AI- + * Botのフロントエンド(ストリームとの対話を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as childProcess from 'child_process'; +const WebSocket = require('ws'); +import * as ReconnectingWebSocket from 'reconnecting-websocket'; +import * as request from 'request-promise-native'; +import conf from '../../config'; + +// 設定 //////////////////////////////////////////////////////// + +/** + * BotアカウントのAPIキー + */ +const i = conf.reversi_ai.i; + +/** + * BotアカウントのユーザーID + */ +const id = conf.reversi_ai.id; + +//////////////////////////////////////////////////////////////// + +/** + * ホームストリーム + */ +const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, { + constructor: WebSocket +}); + +homeStream.on('open', () => { + console.log('home stream opened'); +}); + +homeStream.on('close', () => { + console.log('home stream closed'); +}); + +homeStream.on('message', message => { + const msg = JSON.parse(message.toString()); + + // タイムライン上でなんか言われたまたは返信されたとき + if (msg.type == 'mention' || msg.type == 'reply') { + const note = msg.body; + + if (note.userId == id) return; + + // リアクションする + request.post(`${conf.api_url}/notes/reactions/create`, { + json: { i, + noteId: note.id, + reaction: 'love' + } + }); + + if (note.text) { + if (note.text.indexOf('リバーシ') > -1) { + request.post(`${conf.api_url}/notes/create`, { + json: { i, + replyId: note.id, + text: '良いですよ~' + } + }); + + invite(note.userId); + } + } + } + + // メッセージでなんか言われたとき + if (msg.type == 'messaging_message') { + const message = msg.body; + if (message.text) { + if (message.text.indexOf('リバーシ') > -1) { + request.post(`${conf.api_url}/messaging/messages/create`, { + json: { i, + userId: message.userId, + text: '良いですよ~' + } + }); + + invite(message.userId); + } + } + } +}); + +// ユーザーを対局に誘う +function invite(userId) { + request.post(`${conf.api_url}/reversi/match`, { + json: { i, + userId: userId + } + }); +} + +/** + * リバーシストリーム + */ +const reversiStream = new ReconnectingWebSocket(`${conf.ws_url}/reversi?i=${i}`, undefined, { + constructor: WebSocket +}); + +reversiStream.on('open', () => { + console.log('reversi stream opened'); +}); + +reversiStream.on('close', () => { + console.log('reversi stream closed'); +}); + +reversiStream.on('message', message => { + const msg = JSON.parse(message.toString()); + + // 招待されたとき + if (msg.type == 'invited') { + onInviteMe(msg.body.parent); + } + + // マッチしたとき + if (msg.type == 'matched') { + gameStart(msg.body); + } +}); + +/** + * ゲーム開始 + * @param game ゲーム情報 + */ +function gameStart(game) { + // ゲームストリームに接続 + const gw = new ReconnectingWebSocket(`${conf.ws_url}/reversi-game?i=${i}&game=${game.id}`, undefined, { + constructor: WebSocket + }); + + gw.on('open', () => { + console.log('reversi game stream opened'); + + // フォーム + const form = [{ + id: 'strength', + type: 'radio', + label: '強さ', + value: 2, + items: [{ + label: '接待', + value: 0 + }, { + label: '弱', + value: 1 + }, { + label: '中', + value: 2 + }, { + label: '強', + value: 3 + }, { + label: '最強', + value: 5 + }] + }]; + + //#region バックエンドプロセス開始 + const ai = childProcess.fork(__dirname + '/back.js'); + + // バックエンドプロセスに情報を渡す + ai.send({ + type: '_init_', + game, + form + }); + + ai.on('message', msg => { + if (msg.type == 'put') { + gw.send(JSON.stringify({ + type: 'set', + pos: msg.pos + })); + } else if (msg.type == 'close') { + gw.close(); + } + }); + + // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える + gw.on('message', message => { + const msg = JSON.parse(message.toString()); + ai.send(msg); + }); + //#endregion + + // フォーム初期化 + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'init-form', + body: form + })); + }, 1000); + + // どんな設定内容の対局でも受け入れる + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'accept' + })); + }, 2000); + }); + + gw.on('close', () => { + console.log('reversi game stream closed'); + }); +} + +/** + * リバーシの対局に招待されたとき + * @param inviter 誘ってきたユーザー + */ +async function onInviteMe(inviter) { + console.log(`Someone invited me: @${inviter.username}`); + + // 承認 + const game = await request.post(`${conf.api_url}/reversi/match`, { + json: { + i, + userId: inviter.id + } + }); + + gameStart(game); +} diff --git a/src/reversi/ai/index.ts b/src/reversi/ai/index.ts new file mode 100644 index 0000000000..5cd1db82da --- /dev/null +++ b/src/reversi/ai/index.ts @@ -0,0 +1 @@ +require('./front'); diff --git a/src/reversi/core.ts b/src/reversi/core.ts new file mode 100644 index 0000000000..f1f9de1486 --- /dev/null +++ b/src/reversi/core.ts @@ -0,0 +1,340 @@ +/** + * true ... 黒 + * false ... 白 + */ +export type Color = boolean; +const BLACK = true; +const WHITE = false; + +export type MapPixel = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; +}; + +export type Undo = { + /** + * 色 + */ + color: Color, + + /** + * どこに打ったか + */ + pos: number; + + /** + * 反転した石の位置の配列 + */ + effects: number[]; + + /** + * ターン + */ + turn: Color; +}; + +/** + * リバーシエンジン + */ +export default class Reversi { + public map: MapPixel[]; + public mapWidth: number; + public mapHeight: number; + public board: Color[]; + public turn: Color = BLACK; + public opts: Options; + + public prevPos = -1; + public prevColor: Color = null; + + private logs: Undo[] = []; + + /** + * ゲームを初期化します + */ + constructor(map: string[], opts: Options) { + //#region binds + this.put = this.put.bind(this); + //#endregion + + //#region Options + this.opts = opts; + if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; + if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; + if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + //#endregion + + //#region Parse map data + this.mapWidth = map[0].length; + this.mapHeight = map.length; + const mapData = map.join(''); + + this.board = mapData.split('').map(d => { + if (d == '-') return null; + if (d == 'b') return BLACK; + if (d == 'w') return WHITE; + return undefined; + }); + + this.map = mapData.split('').map(d => { + if (d == '-' || d == 'b' || d == 'w') return 'empty'; + return 'null'; + }); + //#endregion + + // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある + if (this.canPutSomewhere(BLACK).length == 0) { + if (this.canPutSomewhere(WHITE).length == 0) { + this.turn = null; + } else { + this.turn = WHITE; + } + } + } + + /** + * 黒石の数 + */ + public get blackCount() { + return this.board.filter(x => x === BLACK).length; + } + + /** + * 白石の数 + */ + public get whiteCount() { + return this.board.filter(x => x === WHITE).length; + } + + /** + * 黒石の比率 + */ + public get blackP() { + if (this.blackCount == 0 && this.whiteCount == 0) return 0; + return this.blackCount / (this.blackCount + this.whiteCount); + } + + /** + * 白石の比率 + */ + public get whiteP() { + if (this.blackCount == 0 && this.whiteCount == 0) return 0; + return this.whiteCount / (this.blackCount + this.whiteCount); + } + + public transformPosToXy(pos: number): number[] { + const x = pos % this.mapWidth; + const y = Math.floor(pos / this.mapWidth); + return [x, y]; + } + + public transformXyToPos(x: number, y: number): number { + return x + (y * this.mapWidth); + } + + /** + * 指定のマスに石を打ちます + * @param color 石の色 + * @param pos 位置 + */ + public put(color: Color, pos: number) { + this.prevPos = pos; + this.prevColor = color; + + this.board[pos] = color; + + // 反転させられる石を取得 + const effects = this.effects(color, pos); + + // 反転させる + for (const pos of effects) { + this.board[pos] = color; + } + + const turn = this.turn; + + this.logs.push({ + color, + pos, + effects, + turn + }); + + this.calcTurn(); + } + + private calcTurn() { + // ターン計算 + if (this.canPutSomewhere(!this.prevColor).length > 0) { + this.turn = !this.prevColor; + } else if (this.canPutSomewhere(this.prevColor).length > 0) { + this.turn = this.prevColor; + } else { + this.turn = null; + } + } + + public undo() { + const undo = this.logs.pop(); + this.prevColor = undo.color; + this.prevPos = undo.pos; + this.board[undo.pos] = null; + for (const pos of undo.effects) { + const color = this.board[pos]; + this.board[pos] = !color; + } + this.turn = undo.turn; + } + + /** + * 指定した位置のマップデータのマスを取得します + * @param pos 位置 + */ + public mapDataGet(pos: number): MapPixel { + const [x, y] = this.transformPosToXy(pos); + if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null'; + return this.map[pos]; + } + + /** + * 打つことができる場所を取得します + */ + public canPutSomewhere(color: Color): number[] { + const result = []; + + this.board.forEach((x, i) => { + if (this.canPut(color, i)) result.push(i); + }); + + return result; + } + + /** + * 指定のマスに石を打つことができるかどうかを取得します + * @param color 自分の色 + * @param pos 位置 + */ + public canPut(color: Color, pos: number): boolean { + // 既に石が置いてある場所には打てない + if (this.board[pos] !== null) return false; + + if (this.opts.canPutEverywhere) { + // 挟んでなくても置けるモード + return this.mapDataGet(pos) == 'empty'; + } else { + // 相手の石を1つでも反転させられるか + return this.effects(color, pos).length !== 0; + } + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param pos 位置 + */ + public effects(color: Color, pos: number): number[] { + const enemyColor = !color; + + // ひっくり返せる石(の位置)リスト + let stones = []; + + const initPos = pos; + + // 走査 + const iterate = (fn: (i: number) => number[]) => { + let i = 1; + const found = []; + + while (true) { + let [x, y] = fn(i); + + // 座標が指し示す位置がボード外に出たとき + if (this.opts.loopedBoard) { + if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth); + if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight); + if (x >= this.mapWidth ) x = x % this.mapWidth; + if (y >= this.mapHeight) y = y % this.mapHeight; + + // for debug + //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) { + // console.log(x, y); + //} + + // 一周して自分に帰ってきたら + if (this.transformXyToPos(x, y) == initPos) { + // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、 + // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります) + // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます + // (あと無効な方がゲームとしておもしろそうだった) + stones = stones.concat(found); + break; + } + } else { + if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break; + } + + const pos = this.transformXyToPos(x, y); + + //#region 「配置不能」マスに当たった場合走査終了 + const pixel = this.mapDataGet(pos); + if (pixel == 'null') break; + //#endregion + + // 石取得 + const stone = this.board[pos]; + + // 石が置かれていないマスなら走査終了 + if (stone === null) break; + + // 相手の石なら「ひっくり返せるかもリスト」に入れておく + if (stone === enemyColor) found.push(pos); + + // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了 + if (stone === color) { + stones = stones.concat(found); + break; + } + + i++; + } + }; + + const [x, y] = this.transformPosToXy(pos); + + iterate(i => [x , y - i]); // 上 + iterate(i => [x + i, y - i]); // 右上 + iterate(i => [x + i, y ]); // 右 + iterate(i => [x + i, y + i]); // 右下 + iterate(i => [x , y + i]); // 下 + iterate(i => [x - i, y + i]); // 左下 + iterate(i => [x - i, y ]); // 左 + iterate(i => [x - i, y - i]); // 左上 + + return stones; + } + + /** + * ゲームが終了したか否か + */ + public get isEnded(): boolean { + return this.turn === null; + } + + /** + * ゲームの勝者 (null = 引き分け) + */ + public get winner(): Color { + if (!this.isEnded) return undefined; + + if (this.blackCount == this.whiteCount) return null; + + if (this.opts.isLlotheo) { + return this.blackCount > this.whiteCount ? WHITE : BLACK; + } else { + return this.blackCount > this.whiteCount ? BLACK : WHITE; + } + } +} diff --git a/src/reversi/maps.ts b/src/reversi/maps.ts new file mode 100644 index 0000000000..f55cb1d516 --- /dev/null +++ b/src/reversi/maps.ts @@ -0,0 +1,911 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + category?: string; + author?: string; + data: string[]; +}; + +export const fourfour: Map = { + name: '4x4', + category: '4x4', + data: [ + '----', + '-wb-', + '-bw-', + '----' + ] +}; + +export const sixsix: Map = { + name: '6x6', + category: '6x6', + data: [ + '------', + '------', + '--wb--', + '--bw--', + '------', + '------' + ] +}; + +export const roundedSixsix: Map = { + name: '6x6 rounded', + category: '6x6', + author: 'syuilo', + data: [ + ' ---- ', + '------', + '--wb--', + '--bw--', + '------', + ' ---- ' + ] +}; + +export const roundedSixsix2: Map = { + name: '6x6 rounded 2', + category: '6x6', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + '--wb--', + '--bw--', + ' ---- ', + ' -- ' + ] +}; + +export const eighteight: Map = { + name: '8x8', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH1: Map = { + name: '8x8 handicap 1', + category: '8x8', + data: [ + 'b-------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH2: Map = { + name: '8x8 handicap 2', + category: '8x8', + data: [ + 'b-------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '-------b' + ] +}; + +export const eighteightH3: Map = { + name: '8x8 handicap 3', + category: '8x8', + data: [ + 'b------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '-------b' + ] +}; + +export const eighteightH4: Map = { + name: '8x8 handicap 4', + category: '8x8', + data: [ + 'b------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------b' + ] +}; + +export const eighteightH12: Map = { + name: '8x8 handicap 12', + category: '8x8', + data: [ + 'bb----bb', + 'b------b', + '--------', + '---wb---', + '---bw---', + '--------', + 'b------b', + 'bb----bb' + ] +}; + +export const eighteightH16: Map = { + name: '8x8 handicap 16', + category: '8x8', + data: [ + 'bbb---bb', + 'b------b', + '-------b', + '---wb---', + '---bw---', + 'b-------', + 'b------b', + 'bb---bbb' + ] +}; + +export const eighteightH20: Map = { + name: '8x8 handicap 20', + category: '8x8', + data: [ + 'bbb--bbb', + 'b------b', + 'b------b', + '---wb---', + '---bw---', + 'b------b', + 'b------b', + 'bbb---bb' + ] +}; + +export const eighteightH28: Map = { + name: '8x8 handicap 28', + category: '8x8', + data: [ + 'bbbbbbbb', + 'b------b', + 'b------b', + 'b--wb--b', + 'b--bw--b', + 'b------b', + 'b------b', + 'bbbbbbbb' + ] +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + ' ------ ' + ] +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + category: '8x8', + author: 'syuilo', + data: [ + ' ---- ', + ' ------ ', + '--------', + '---wb---', + '---bw---', + '--------', + ' ------ ', + ' ---- ' + ] +}; + +export const roundedEighteight3: Map = { + name: '8x8 rounded 3', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ---- ', + ' -- ' + ] +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + category: '8x8', + author: 'syuilo', + data: [ + '--- ---', + '--------', + '--------', + ' --wb-- ', + ' --bw-- ', + '--------', + '--------', + '--- ---' + ] +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + category: '8x8', + author: 'syuilo', + data: [ + '--- ----', + '----- --', + '-- -----', + '---wb---', + '---bw- -', + ' -------', + '--- ----', + '--------' + ] +}; + +export const circle: Map = { + name: 'Circle', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ------ ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ------ ', + ' -- ' + ] +}; + +export const smile: Map = { + name: 'Smile', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '-- -- --', + '---wb---', + '-- bw --', + '--- ---', + '--------', + ' ------ ' + ] +}; + +export const window: Map = { + name: 'Window', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '- -- -', + '- -- -', + '---wb---', + '---bw---', + '- -- -', + '- -- -', + '--------' + ] +}; + +export const reserved: Map = { + name: 'Reserved', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------w' + ] +}; + +export const x: Map = { + name: 'X', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '-w----b-', + '--w--b--', + '---wb---', + '---bw---', + '--b--w--', + '-b----w-', + 'b------w' + ] +}; + +export const parallel: Map = { + name: 'Parallel', + category: '8x8', + author: 'Aya', + data: [ + '--------', + '--------', + '--------', + '---bb---', + '---ww---', + '--------', + '--------', + '--------' + ] +}; + +export const lackOfBlack: Map = { + name: 'Lack of Black', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---w----', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const squareParty: Map = { + name: 'Square Party', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '-wwwbbb-', + '-w-wb-b-', + '-wwwbbb-', + '-bbbwww-', + '-b-bw-w-', + '-bbbwww-', + '--------' + ] +}; + +export const minesweeper: Map = { + name: 'Minesweeper', + category: '8x8', + author: 'syuilo', + data: [ + 'b-b--w-w', + '-w-wb-b-', + 'w-b--w-b', + '-b-wb-w-', + '-w-bw-b-', + 'b-w--b-w', + '-b-bw-w-', + 'w-w--b-b' + ] +}; + +export const tenthtenth: Map = { + name: '10x10', + category: '10x10', + data: [ + '----------', + '----------', + '----------', + '----------', + '----wb----', + '----bw----', + '----------', + '----------', + '----------', + '----------' + ] +}; + +export const hole: Map = { + name: 'The Hole', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '----------', + '--wb--wb--', + '--bw--bw--', + '---- ----', + '---- ----', + '--wb--wb--', + '--bw--bw--', + '----------', + '----------' + ] +}; + +export const grid: Map = { + name: 'Grid', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '- - -- - -', + '----------', + '- - -- - -', + '----wb----', + '----bw----', + '- - -- - -', + '----------', + '- - -- - -', + '----------' + ] +}; + +export const cross: Map = { + name: 'Cross', + category: '10x10', + author: 'Aya', + data: [ + ' ---- ', + ' ---- ', + ' ---- ', + '----------', + '----wb----', + '----bw----', + '----------', + ' ---- ', + ' ---- ', + ' ---- ' + ] +}; + +export const charX: Map = { + name: 'Char X', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + '----------', + '---- ----', + '--- ---' + ] +}; + +export const charY: Map = { + name: 'Char Y', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' ------ ', + ' ------ ', + ' ------ ', + ' ------ ' + ] +}; + +export const walls: Map = { + name: 'Walls', + category: '10x10', + author: 'Aya', + data: [ + ' bbbbbbbb ', + 'w--------w', + 'w--------w', + 'w--------w', + 'w---wb---w', + 'w---bw---w', + 'w--------w', + 'w--------w', + 'w--------w', + ' bbbbbbbb ' + ] +}; + +export const cpu: Map = { + name: 'CPU', + category: '10x10', + author: 'syuilo', + data: [ + ' b b b b ', + 'w--------w', + ' -------- ', + 'w--------w', + ' ---wb--- ', + ' ---bw--- ', + 'w--------w', + ' -------- ', + 'w--------w', + ' b b b b ' + ] +}; + +export const checker: Map = { + name: 'Checker', + category: '10x10', + author: 'Aya', + data: [ + '----------', + '----------', + '----------', + '---wbwb---', + '---bwbw---', + '---wbwb---', + '---bwbw---', + '----------', + '----------', + '----------' + ] +}; + +export const japaneseCurry: Map = { + name: 'Japanese curry', + category: '10x10', + author: 'syuilo', + data: [ + 'w-b-b-b-b-', + '-w-b-b-b-b', + 'w-w-b-b-b-', + '-w-w-b-b-b', + 'w-w-wwb-b-', + '-w-wbb-b-b', + 'w-w-w-b-b-', + '-w-w-w-b-b', + 'w-w-w-w-b-', + '-w-w-w-w-b' + ] +}; + +export const mosaic: Map = { + name: 'Mosaic', + category: '10x10', + author: 'syuilo', + data: [ + '- - - - - ', + ' - - - - -', + '- - - - - ', + ' - w w - -', + '- - b b - ', + ' - w w - -', + '- - b b - ', + ' - - - - -', + '- - - - - ', + ' - - - - -', + ] +}; + +export const arena: Map = { + name: 'Arena', + category: '10x10', + author: 'syuilo', + data: [ + '- - -- - -', + ' - - - - ', + '- ------ -', + ' -------- ', + '- --wb-- -', + '- --bw-- -', + ' -------- ', + '- ------ -', + ' - - - - ', + '- - -- - -' + ] +}; + +export const reactor: Map = { + name: 'Reactor', + category: '10x10', + author: 'syuilo', + data: [ + '-w------b-', + 'b- - - -w', + '- --wb-- -', + '---b w---', + '- b wb w -', + '- w bw b -', + '---w b---', + '- --bw-- -', + 'w- - - -b', + '-b------w-' + ] +}; + +export const sixeight: Map = { + name: '6x8', + category: 'Special', + data: [ + '------', + '------', + '------', + '--wb--', + '--bw--', + '------', + '------', + '------' + ] +}; + +export const spark: Map = { + name: 'Spark', + category: 'Special', + author: 'syuilo', + data: [ + ' - - ', + '----------', + ' -------- ', + ' -------- ', + ' ---wb--- ', + ' ---bw--- ', + ' -------- ', + ' -------- ', + '----------', + ' - - ' + ] +}; + +export const islands: Map = { + name: 'Islands', + category: 'Special', + author: 'syuilo', + data: [ + '-------- ', + '---wb--- ', + '---bw--- ', + '-------- ', + ' - - ', + ' - - ', + ' --------', + ' --------', + ' --------', + ' --------' + ] +}; + +export const galaxy: Map = { + name: 'Galaxy', + category: 'Special', + author: 'syuilo', + data: [ + ' ------ ', + ' --www--- ', + ' ------w--- ', + '---bbb--w---', + '--b---b-w-b-', + '-b--wwb-w-b-', + '-b-w-bww--b-', + '-b-w-b---b--', + '---w--bbb---', + ' ---w------ ', + ' ---www-- ', + ' ------ ' + ] +}; + +export const triangle: Map = { + name: 'Triangle', + category: 'Special', + author: 'syuilo', + data: [ + ' -- ', + ' -- ', + ' ---- ', + ' ---- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + ' -------- ', + '----------', + '----------' + ] +}; + +export const iphonex: Map = { + name: 'iPhone X', + category: 'Special', + author: 'syuilo', + data: [ + ' -- -- ', + '--------', + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------', + '--------', + ' ------ ' + ] +}; + +export const dealWithIt: Map = { + name: 'Deal with it!', + category: 'Special', + author: 'syuilo', + data: [ + '------------', + '--w-b-------', + ' --b-w------', + ' --w-b---- ', + ' ------- ' + ] +}; + +export const experiment: Map = { + name: 'Let\'s experiment', + category: 'Special', + author: 'syuilo', + data: [ + ' ------------ ', + '------wb------', + '------bw------', + '--------------', + ' - - ', + '------ ------', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'bbbbbb wwwwww', + 'wwwwww bbbbbb' + ] +}; + +export const bigBoard: Map = { + name: 'Big board', + category: 'Special', + data: [ + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '-------wb-------', + '-------bw-------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------' + ] +}; + +export const twoBoard: Map = { + name: 'Two board', + category: 'Special', + author: 'Aya', + data: [ + '-------- --------', + '-------- --------', + '-------- --------', + '---wb--- ---wb---', + '---bw--- ---bw---', + '-------- --------', + '-------- --------', + '-------- --------' + ] +}; + +export const test1: Map = { + name: 'Test1', + category: 'Test', + data: [ + '--------', + '---wb---', + '---bw---', + '--------' + ] +}; + +export const test2: Map = { + name: 'Test2', + category: 'Test', + data: [ + '------', + '------', + '-b--w-', + '-w--b-', + '-w--b-' + ] +}; + +export const test3: Map = { + name: 'Test3', + category: 'Test', + data: [ + '-w-', + '--w', + 'w--', + '-w-', + '--w', + 'w--', + '-w-', + '--w', + 'w--', + '-w-', + '---', + 'b--', + ] +}; + +export const test4: Map = { + name: 'Test4', + category: 'Test', + data: [ + '-w--b-', + '-w--b-', + '------', + '-w--b-', + '-w--b-' + ] +}; + +// https://misskey.xyz/reversi/5aaabf7fe126e10b5216ea09 64 +export const test5: Map = { + name: 'Test5', + category: 'Test', + data: [ + '--wwwwww--', + '--wwwbwwww', + '-bwwbwbwww', + '-bwwwbwbww', + '-bwwbwbwbw', + '-bwbwbwb-w', + 'bwbwwbbb-w', + 'w-wbbbbb--', + '--w-b-w---', + '----------' + ] +}; diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 5f0a020d6f..a5d13b0237 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -245,27 +245,27 @@ const endpoints: Endpoint[] = [ }, { - name: 'othello/match', + name: 'reversi/match', withCredential: true }, { - name: 'othello/match/cancel', + name: 'reversi/match/cancel', withCredential: true }, { - name: 'othello/invitations', + name: 'reversi/invitations', withCredential: true }, { - name: 'othello/games', + name: 'reversi/games', withCredential: true }, { - name: 'othello/games/show' + name: 'reversi/games/show' }, { diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts deleted file mode 100644 index 2320a34b04..0000000000 --- a/src/server/api/endpoints/othello/games.ts +++ /dev/null @@ -1,62 +0,0 @@ -import $ from 'cafy'; import ID from '../../../../cafy-id'; -import OthelloGame, { pack } from '../../../../models/othello-game'; - -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'my' parameter - const [my = false, myErr] = $.bool.optional().get(params.my); - if (myErr) return rej('invalid my param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional().range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional().get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional().get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); - - // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { - return rej('cannot set sinceId and untilId'); - } - - const q: any = my ? { - isStarted: true, - $or: [{ - user1Id: user._id - }, { - user2Id: user._id - }] - } : { - isStarted: true - }; - - const sort = { - _id: -1 - }; - - if (sinceId) { - sort._id = 1; - q._id = { - $gt: sinceId - }; - } else if (untilId) { - q._id = { - $lt: untilId - }; - } - - // Fetch games - const games = await OthelloGame.find(q, { - sort, - limit - }); - - // Reponse - res(Promise.all(games.map(async (g) => await pack(g, user, { - detail: false - })))); -}); diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts deleted file mode 100644 index 6b2f5ce137..0000000000 --- a/src/server/api/endpoints/othello/games/show.ts +++ /dev/null @@ -1,32 +0,0 @@ -import $ from 'cafy'; import ID from '../../../../../cafy-id'; -import OthelloGame, { pack } from '../../../../../models/othello-game'; -import Othello from '../../../../../othello/core'; - -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'gameId' parameter - const [gameId, gameIdErr] = $.type(ID).get(params.gameId); - if (gameIdErr) return rej('invalid gameId param'); - - const game = await OthelloGame.findOne({ _id: gameId }); - - if (game == null) { - return rej('game not found'); - } - - const o = new Othello(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard - }); - - game.logs.forEach(log => { - o.put(log.color, log.pos); - }); - - const packed = await pack(game, user); - - res(Object.assign({ - board: o.board, - turn: o.turn - }, packed)); -}); diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts deleted file mode 100644 index 4761537614..0000000000 --- a/src/server/api/endpoints/othello/invitations.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Matching, { pack as packMatching } from '../../../../models/othello-matching'; - -module.exports = (params, user) => new Promise(async (res, rej) => { - // Find session - const invitations = await Matching.find({ - childId: user._id - }, { - sort: { - _id: -1 - } - }); - - // Reponse - res(Promise.all(invitations.map(async (i) => await packMatching(i, user)))); -}); diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts deleted file mode 100644 index e70e579755..0000000000 --- a/src/server/api/endpoints/othello/match.ts +++ /dev/null @@ -1,95 +0,0 @@ -import $ from 'cafy'; import ID from '../../../../cafy-id'; -import Matching, { pack as packMatching } from '../../../../models/othello-matching'; -import OthelloGame, { pack as packGame } from '../../../../models/othello-game'; -import User from '../../../../models/user'; -import publishUserStream, { publishOthelloStream } from '../../../../publishers/stream'; -import { eighteight } from '../../../../othello/maps'; - -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'userId' parameter - const [childId, childIdErr] = $.type(ID).get(params.userId); - if (childIdErr) return rej('invalid userId param'); - - // Myself - if (childId.equals(user._id)) { - return rej('invalid userId param'); - } - - // Find session - const exist = await Matching.findOne({ - parentId: childId, - childId: user._id - }); - - if (exist) { - // Destroy session - Matching.remove({ - _id: exist._id - }); - - // Create game - const game = await OthelloGame.insert({ - createdAt: new Date(), - user1Id: exist.parentId, - user2Id: user._id, - user1Accepted: false, - user2Accepted: false, - isStarted: false, - isEnded: false, - logs: [], - settings: { - map: eighteight.data, - bw: 'random', - isLlotheo: false - } - }); - - // Reponse - res(await packGame(game, user)); - - publishOthelloStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); - - const other = await Matching.count({ - childId: user._id - }); - - if (other == 0) { - publishUserStream(user._id, 'othello_no_invites'); - } - } else { - // Fetch child - const child = await User.findOne({ - _id: childId - }, { - fields: { - _id: true - } - }); - - if (child === null) { - return rej('user not found'); - } - - // 以前のセッションはすべて削除しておく - await Matching.remove({ - parentId: user._id - }); - - // セッションを作成 - const matching = await Matching.insert({ - createdAt: new Date(), - parentId: user._id, - childId: child._id - }); - - // Reponse - res(); - - const packed = await packMatching(matching, child); - - // 招待 - publishOthelloStream(child._id, 'invited', packed); - - publishUserStream(child._id, 'othello_invited', packed); - } -}); diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts deleted file mode 100644 index 562e691061..0000000000 --- a/src/server/api/endpoints/othello/match/cancel.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Matching from '../../../../../models/othello-matching'; - -module.exports = (params, user) => new Promise(async (res, rej) => { - await Matching.remove({ - parentId: user._id - }); - - res(); -}); diff --git a/src/server/api/endpoints/reversi/games.ts b/src/server/api/endpoints/reversi/games.ts new file mode 100644 index 0000000000..9d879ecf23 --- /dev/null +++ b/src/server/api/endpoints/reversi/games.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; import ID from '../../../../cafy-id'; +import ReversiGame, { pack } from '../../../../models/reversi-game'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'my' parameter + const [my = false, myErr] = $.bool.optional().get(params.my); + if (myErr) return rej('invalid my param'); + + // Get 'limit' parameter + const [limit = 10, limitErr] = $.num.optional().range(1, 100).get(params.limit); + if (limitErr) return rej('invalid limit param'); + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $.type(ID).optional().get(params.sinceId); + if (sinceIdErr) return rej('invalid sinceId param'); + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $.type(ID).optional().get(params.untilId); + if (untilIdErr) return rej('invalid untilId param'); + + // Check if both of sinceId and untilId is specified + if (sinceId && untilId) { + return rej('cannot set sinceId and untilId'); + } + + const q: any = my ? { + isStarted: true, + $or: [{ + user1Id: user._id + }, { + user2Id: user._id + }] + } : { + isStarted: true + }; + + const sort = { + _id: -1 + }; + + if (sinceId) { + sort._id = 1; + q._id = { + $gt: sinceId + }; + } else if (untilId) { + q._id = { + $lt: untilId + }; + } + + // Fetch games + const games = await ReversiGame.find(q, { + sort, + limit + }); + + // Reponse + res(Promise.all(games.map(async (g) => await pack(g, user, { + detail: false + })))); +}); diff --git a/src/server/api/endpoints/reversi/games/show.ts b/src/server/api/endpoints/reversi/games/show.ts new file mode 100644 index 0000000000..f32eb23513 --- /dev/null +++ b/src/server/api/endpoints/reversi/games/show.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; import ID from '../../../../../cafy-id'; +import ReversiGame, { pack } from '../../../../../models/reversi-game'; +import Reversi from '../../../../../reversi/core'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'gameId' parameter + const [gameId, gameIdErr] = $.type(ID).get(params.gameId); + if (gameIdErr) return rej('invalid gameId param'); + + const game = await ReversiGame.findOne({ _id: gameId }); + + if (game == null) { + return rej('game not found'); + } + + const o = new Reversi(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const packed = await pack(game, user); + + res(Object.assign({ + board: o.board, + turn: o.turn + }, packed)); +}); diff --git a/src/server/api/endpoints/reversi/invitations.ts b/src/server/api/endpoints/reversi/invitations.ts new file mode 100644 index 0000000000..fc487205a9 --- /dev/null +++ b/src/server/api/endpoints/reversi/invitations.ts @@ -0,0 +1,15 @@ +import Matching, { pack as packMatching } from '../../../../models/reversi-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Find session + const invitations = await Matching.find({ + childId: user._id + }, { + sort: { + _id: -1 + } + }); + + // Reponse + res(Promise.all(invitations.map(async (i) => await packMatching(i, user)))); +}); diff --git a/src/server/api/endpoints/reversi/match.ts b/src/server/api/endpoints/reversi/match.ts new file mode 100644 index 0000000000..5a699ddbae --- /dev/null +++ b/src/server/api/endpoints/reversi/match.ts @@ -0,0 +1,95 @@ +import $ from 'cafy'; import ID from '../../../../cafy-id'; +import Matching, { pack as packMatching } from '../../../../models/reversi-matching'; +import ReversiGame, { pack as packGame } from '../../../../models/reversi-game'; +import User from '../../../../models/user'; +import publishUserStream, { publishReversiStream } from '../../../../publishers/stream'; +import { eighteight } from '../../../../reversi/maps'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + // Get 'userId' parameter + const [childId, childIdErr] = $.type(ID).get(params.userId); + if (childIdErr) return rej('invalid userId param'); + + // Myself + if (childId.equals(user._id)) { + return rej('invalid userId param'); + } + + // Find session + const exist = await Matching.findOne({ + parentId: childId, + childId: user._id + }); + + if (exist) { + // Destroy session + Matching.remove({ + _id: exist._id + }); + + // Create game + const game = await ReversiGame.insert({ + createdAt: new Date(), + user1Id: exist.parentId, + user2Id: user._id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, + logs: [], + settings: { + map: eighteight.data, + bw: 'random', + isLlotheo: false + } + }); + + // Reponse + res(await packGame(game, user)); + + publishReversiStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); + + const other = await Matching.count({ + childId: user._id + }); + + if (other == 0) { + publishUserStream(user._id, 'reversi_no_invites'); + } + } else { + // Fetch child + const child = await User.findOne({ + _id: childId + }, { + fields: { + _id: true + } + }); + + if (child === null) { + return rej('user not found'); + } + + // 以前のセッションはすべて削除しておく + await Matching.remove({ + parentId: user._id + }); + + // セッションを作成 + const matching = await Matching.insert({ + createdAt: new Date(), + parentId: user._id, + childId: child._id + }); + + // Reponse + res(); + + const packed = await packMatching(matching, child); + + // 招待 + publishReversiStream(child._id, 'invited', packed); + + publishUserStream(child._id, 'reversi_invited', packed); + } +}); diff --git a/src/server/api/endpoints/reversi/match/cancel.ts b/src/server/api/endpoints/reversi/match/cancel.ts new file mode 100644 index 0000000000..bc8a4cd640 --- /dev/null +++ b/src/server/api/endpoints/reversi/match/cancel.ts @@ -0,0 +1,9 @@ +import Matching from '../../../../../models/reversi-matching'; + +module.exports = (params, user) => new Promise(async (res, rej) => { + await Matching.remove({ + parentId: user._id + }); + + res(); +}); diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts deleted file mode 100644 index 841e542610..0000000000 --- a/src/server/api/stream/othello-game.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import * as CRC32 from 'crc-32'; -import OthelloGame, { pack } from '../../../models/othello-game'; -import { publishOthelloGameStream } from '../../../publishers/stream'; -import Othello from '../../../othello/core'; -import * as maps from '../../../othello/maps'; -import { ParsedUrlQuery } from 'querystring'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void { - const q = request.resourceURL.query as ParsedUrlQuery; - const gameId = q.game; - - // Subscribe game stream - subscriber.subscribe(`misskey:othello-game-stream:${gameId}`); - subscriber.on('message', (_, data) => { - connection.send(data); - }); - - connection.on('message', async (data) => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'accept': - accept(true); - break; - - case 'cancel-accept': - accept(false); - break; - - case 'update-settings': - if (msg.settings == null) return; - updateSettings(msg.settings); - break; - - case 'init-form': - if (msg.body == null) return; - initForm(msg.body); - break; - - case 'update-form': - if (msg.id == null || msg.value === undefined) return; - updateForm(msg.id, msg.value); - break; - - case 'message': - if (msg.body == null) return; - message(msg.body); - break; - - case 'set': - if (msg.pos == null) return; - set(msg.pos); - break; - - case 'check': - if (msg.crc32 == null) return; - check(msg.crc32); - break; - } - }); - - async function updateSettings(settings) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - if (game.user1Id.equals(user._id) && game.user1Accepted) return; - if (game.user2Id.equals(user._id) && game.user2Accepted) return; - - await OthelloGame.update({ _id: gameId }, { - $set: { - settings - } - }); - - publishOthelloGameStream(gameId, 'update-settings', settings); - } - - async function initForm(form) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const set = game.user1Id.equals(user._id) ? { - form1: form - } : { - form2: form - }; - - await OthelloGame.update({ _id: gameId }, { - $set: set - }); - - publishOthelloGameStream(gameId, 'init-form', { - userId: user._id, - form - }); - } - - async function updateForm(id, value) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; - - const item = form.find(i => i.id == id); - - if (item == null) return; - - item.value = value; - - const set = game.user1Id.equals(user._id) ? { - form2: form - } : { - form1: form - }; - - await OthelloGame.update({ _id: gameId }, { - $set: set - }); - - publishOthelloGameStream(gameId, 'update-form', { - userId: user._id, - id, - value - }); - } - - async function message(message) { - message.id = Math.random(); - publishOthelloGameStream(gameId, 'message', { - userId: user._id, - message - }); - } - - async function accept(accept: boolean) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (game.isStarted) return; - - let bothAccepted = false; - - if (game.user1Id.equals(user._id)) { - await OthelloGame.update({ _id: gameId }, { - $set: { - user1Accepted: accept - } - }); - - publishOthelloGameStream(gameId, 'change-accepts', { - user1: accept, - user2: game.user2Accepted - }); - - if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id.equals(user._id)) { - await OthelloGame.update({ _id: gameId }, { - $set: { - user2Accepted: accept - } - }); - - publishOthelloGameStream(gameId, 'change-accepts', { - user1: game.user1Accepted, - user2: accept - }); - - if (accept && game.user1Accepted) bothAccepted = true; - } else { - return; - } - - if (bothAccepted) { - // 3秒後、まだacceptされていたらゲーム開始 - setTimeout(async () => { - const freshGame = await OthelloGame.findOne({ _id: gameId }); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; - - let bw: number; - if (freshGame.settings.bw == 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = freshGame.settings.bw as number; - } - - function getRandomMap() { - const mapCount = Object.entries(maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.entries(maps).find((x, i) => i == rnd)[1].data; - } - - const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); - - await OthelloGame.update({ _id: gameId }, { - $set: { - startedAt: new Date(), - isStarted: true, - black: bw, - 'settings.map': map - } - }); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const o = new Othello(map, { - isLlotheo: freshGame.settings.isLlotheo, - canPutEverywhere: freshGame.settings.canPutEverywhere, - loopedBoard: freshGame.settings.loopedBoard - }); - - if (o.isEnded) { - let winner; - if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; - } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; - } else { - winner = null; - } - - await OthelloGame.update({ - _id: gameId - }, { - $set: { - isEnded: true, - winnerId: winner - } - }); - - publishOthelloGameStream(gameId, 'ended', { - winnerId: winner, - game: await pack(gameId, user) - }); - } - //#endregion - - publishOthelloGameStream(gameId, 'started', await pack(gameId, user)); - }, 3000); - } - } - - // 石を打つ - async function set(pos) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (!game.isStarted) return; - if (game.isEnded) return; - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; - - const o = new Othello(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard - }); - - game.logs.forEach(log => { - o.put(log.color, log.pos); - }); - - const myColor = - (game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) - ? true - : false; - - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); - - let winner; - if (o.isEnded) { - if (o.winner === true) { - winner = game.black == 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { - winner = game.black == 1 ? game.user2Id : game.user1Id; - } else { - winner = null; - } - } - - const log = { - at: new Date(), - color: myColor, - pos - }; - - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); - - await OthelloGame.update({ - _id: gameId - }, { - $set: { - crc32, - isEnded: o.isEnded, - winnerId: winner - }, - $push: { - logs: log - } - }); - - publishOthelloGameStream(gameId, 'set', Object.assign(log, { - next: o.turn - })); - - if (o.isEnded) { - publishOthelloGameStream(gameId, 'ended', { - winnerId: winner, - game: await pack(gameId, user) - }); - } - } - - async function check(crc32) { - const game = await OthelloGame.findOne({ _id: gameId }); - - if (!game.isStarted) return; - - // 互換性のため - if (game.crc32 == null) return; - - if (crc32 !== game.crc32) { - connection.send(JSON.stringify({ - type: 'rescue', - body: await pack(game, user) - })); - } - } -} diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts deleted file mode 100644 index fa62b05836..0000000000 --- a/src/server/api/stream/othello.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as mongo from 'mongodb'; -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import Matching, { pack } from '../../../models/othello-matching'; -import publishUserStream from '../../../publishers/stream'; - -export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { - // Subscribe othello stream - subscriber.subscribe(`misskey:othello-stream:${user._id}`); - subscriber.on('message', (_, data) => { - connection.send(data); - }); - - connection.on('message', async (data) => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'ping': - if (msg.id == null) return; - const matching = await Matching.findOne({ - parentId: user._id, - childId: new mongo.ObjectID(msg.id) - }); - if (matching == null) return; - publishUserStream(matching.childId, 'othello_invited', await pack(matching, matching.childId)); - break; - } - }); -} diff --git a/src/server/api/stream/reversi-game.ts b/src/server/api/stream/reversi-game.ts new file mode 100644 index 0000000000..63d9b220b0 --- /dev/null +++ b/src/server/api/stream/reversi-game.ts @@ -0,0 +1,333 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import * as CRC32 from 'crc-32'; +import ReversiGame, { pack } from '../../../models/reversi-game'; +import { publishReversiGameStream } from '../../../publishers/stream'; +import Reversi from '../../../reversi/core'; +import * as maps from '../../../reversi/maps'; +import { ParsedUrlQuery } from 'querystring'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void { + const q = request.resourceURL.query as ParsedUrlQuery; + const gameId = q.game; + + // Subscribe game stream + subscriber.subscribe(`misskey:reversi-game-stream:${gameId}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'accept': + accept(true); + break; + + case 'cancel-accept': + accept(false); + break; + + case 'update-settings': + if (msg.settings == null) return; + updateSettings(msg.settings); + break; + + case 'init-form': + if (msg.body == null) return; + initForm(msg.body); + break; + + case 'update-form': + if (msg.id == null || msg.value === undefined) return; + updateForm(msg.id, msg.value); + break; + + case 'message': + if (msg.body == null) return; + message(msg.body); + break; + + case 'set': + if (msg.pos == null) return; + set(msg.pos); + break; + + case 'check': + if (msg.crc32 == null) return; + check(msg.crc32); + break; + } + }); + + async function updateSettings(settings) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + if (game.user1Id.equals(user._id) && game.user1Accepted) return; + if (game.user2Id.equals(user._id) && game.user2Accepted) return; + + await ReversiGame.update({ _id: gameId }, { + $set: { + settings + } + }); + + publishReversiGameStream(gameId, 'update-settings', settings); + } + + async function initForm(form) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const set = game.user1Id.equals(user._id) ? { + form1: form + } : { + form2: form + }; + + await ReversiGame.update({ _id: gameId }, { + $set: set + }); + + publishReversiGameStream(gameId, 'init-form', { + userId: user._id, + form + }); + } + + async function updateForm(id, value) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const form = game.user1Id.equals(user._id) ? game.form2 : game.form1; + + const item = form.find(i => i.id == id); + + if (item == null) return; + + item.value = value; + + const set = game.user1Id.equals(user._id) ? { + form2: form + } : { + form1: form + }; + + await ReversiGame.update({ _id: gameId }, { + $set: set + }); + + publishReversiGameStream(gameId, 'update-form', { + userId: user._id, + id, + value + }); + } + + async function message(message) { + message.id = Math.random(); + publishReversiGameStream(gameId, 'message', { + userId: user._id, + message + }); + } + + async function accept(accept: boolean) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id.equals(user._id)) { + await ReversiGame.update({ _id: gameId }, { + $set: { + user1Accepted: accept + } + }); + + publishReversiGameStream(gameId, 'change-accepts', { + user1: accept, + user2: game.user2Accepted + }); + + if (accept && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id.equals(user._id)) { + await ReversiGame.update({ _id: gameId }, { + $set: { + user2Accepted: accept + } + }); + + publishReversiGameStream(gameId, 'change-accepts', { + user1: game.user1Accepted, + user2: accept + }); + + if (accept && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await ReversiGame.findOne({ _id: gameId }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.settings.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = freshGame.settings.bw as number; + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.entries(maps).find((x, i) => i == rnd)[1].data; + } + + const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + + await ReversiGame.update({ _id: gameId }, { + $set: { + startedAt: new Date(), + isStarted: true, + black: bw, + 'settings.map': map + } + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Reversi(map, { + isLlotheo: freshGame.settings.isLlotheo, + canPutEverywhere: freshGame.settings.canPutEverywhere, + loopedBoard: freshGame.settings.loopedBoard + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await ReversiGame.update({ + _id: gameId + }, { + $set: { + isEnded: true, + winnerId: winner + } + }); + + publishReversiGameStream(gameId, 'ended', { + winnerId: winner, + game: await pack(gameId, user) + }); + } + //#endregion + + publishReversiGameStream(gameId, 'started', await pack(gameId, user)); + }, 3000); + } + } + + // 石を打つ + async function set(pos) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (!game.isStarted) return; + if (game.isEnded) return; + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return; + + const o = new Reversi(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); + + const myColor = + (game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2) + ? true + : false; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black == 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black == 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + + await ReversiGame.update({ + _id: gameId + }, { + $set: { + crc32, + isEnded: o.isEnded, + winnerId: winner + }, + $push: { + logs: log + } + }); + + publishReversiGameStream(gameId, 'set', Object.assign(log, { + next: o.turn + })); + + if (o.isEnded) { + publishReversiGameStream(gameId, 'ended', { + winnerId: winner, + game: await pack(gameId, user) + }); + } + } + + async function check(crc32) { + const game = await ReversiGame.findOne({ _id: gameId }); + + if (!game.isStarted) return; + + // 互換性のため + if (game.crc32 == null) return; + + if (crc32 !== game.crc32) { + connection.send(JSON.stringify({ + type: 'rescue', + body: await pack(game, user) + })); + } + } +} diff --git a/src/server/api/stream/reversi.ts b/src/server/api/stream/reversi.ts new file mode 100644 index 0000000000..35c6167364 --- /dev/null +++ b/src/server/api/stream/reversi.ts @@ -0,0 +1,29 @@ +import * as mongo from 'mongodb'; +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import Matching, { pack } from '../../../models/reversi-matching'; +import publishUserStream from '../../../publishers/stream'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + // Subscribe reversi stream + subscriber.subscribe(`misskey:reversi-stream:${user._id}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); + + connection.on('message', async (data) => { + const msg = JSON.parse(data.utf8Data); + + switch (msg.type) { + case 'ping': + if (msg.id == null) return; + const matching = await Matching.findOne({ + parentId: user._id, + childId: new mongo.ObjectID(msg.id) + }); + if (matching == null) return; + publishUserStream(matching.childId, 'reversi_invited', await pack(matching, matching.childId)); + break; + } + }); +} diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index 2d4cfc108f..a3e4ed6f85 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -10,8 +10,8 @@ import userListStream from './stream/user-list'; import driveStream from './stream/drive'; import messagingStream from './stream/messaging'; import messagingIndexStream from './stream/messaging-index'; -import othelloGameStream from './stream/othello-game'; -import othelloStream from './stream/othello'; +import reversiGameStream from './stream/reversi-game'; +import reversiStream from './stream/reversi'; import serverStatsStream from './stream/server-stats'; import notesStatsStream from './stream/notes-stats'; import requestsStream from './stream/requests'; @@ -56,8 +56,8 @@ module.exports = (server: http.Server) => { const q = request.resourceURL.query as ParsedUrlQuery; const [user, app] = await authenticate(q.i as string); - if (request.resourceURL.pathname === '/othello-game') { - othelloGameStream(request, connection, subscriber, user); + if (request.resourceURL.pathname === '/reversi-game') { + reversiGameStream(request, connection, subscriber, user); return; } @@ -75,7 +75,7 @@ module.exports = (server: http.Server) => { request.resourceURL.pathname === '/drive' ? driveStream : request.resourceURL.pathname === '/messaging' ? messagingStream : request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : - request.resourceURL.pathname === '/othello' ? othelloStream : + request.resourceURL.pathname === '/reversi' ? reversiStream : null; if (channel !== null) { -- cgit v1.2.3-freya