なんかもうめっちゃ変えた

This commit is contained in:
syuilo 2018-03-09 18:11:10 +09:00
parent caea9c91b9
commit 910ccf1804
7 changed files with 425 additions and 307 deletions

View file

@ -6,6 +6,23 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [my = false, myErr] = $(params.my).optional.boolean().$; const [my = false, myErr] = $(params.my).optional.boolean().$;
if (myErr) return rej('invalid my param'); if (myErr) return rej('invalid my param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'since_id' parameter
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
// Get 'until_id' parameter
const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
if (untilIdErr) return rej('invalid until_id param');
// Check if both of since_id and until_id is specified
if (sinceId && untilId) {
return rej('cannot set since_id and until_id');
}
const q = my ? { const q = my ? {
is_started: true, is_started: true,
$or: [{ $or: [{
@ -17,13 +34,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
is_started: true is_started: true
}; };
const sort = {
_id: -1
};
if (sinceId) {
sort._id = 1;
q._id = {
$gt: sinceId
};
} else if (untilId) {
q._id = {
$lt: untilId
};
}
// Fetch games // Fetch games
const games = await Game.find(q, { const games = await Game.find(q, {
sort: { sort
_id: -1
}
}); });
// Reponse // Reponse
res(Promise.all(games.map(async (g) => await pack(g, user)))); res(Promise.all(games.map(async (g) => await pack(g, user, {
detail: false
}))));
}); });

View file

@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
is_ended: false, is_ended: false,
logs: [], logs: [],
settings: { settings: {
map: eighteight, map: eighteight.data,
bw: 'random', bw: 'random',
is_llotheo: false is_llotheo: false
} }

View file

@ -28,7 +28,7 @@ export interface IGame {
winner_id: mongo.ObjectID; winner_id: mongo.ObjectID;
logs: any[]; logs: any[];
settings: { settings: {
map: Map; map: string[];
bw: string | number; bw: string | number;
is_llotheo: boolean; is_llotheo: boolean;
}; };
@ -39,8 +39,15 @@ export interface IGame {
*/ */
export const pack = ( export const pack = (
game: any, game: any,
me?: string | mongo.ObjectID | IUser me?: string | mongo.ObjectID | IUser,
options?: {
detail?: boolean
}
) => new Promise<any>(async (resolve, reject) => { ) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: true
}, options);
let _game: any; let _game: any;
// Populate the game if 'game' is ID // Populate the game if 'game' is ID
@ -69,6 +76,11 @@ export const pack = (
_game.id = _game._id; _game.id = _game._id;
delete _game._id; delete _game._id;
if (opts.detail === false) {
delete _game.logs;
delete _game.settings.map;
}
// Populate user // Populate user
_game.user1 = await packUser(_game.user1_id, meId); _game.user1 = await packUser(_game.user1_id, meId);
_game.user2 = await packUser(_game.user2_id, meId); _game.user2 = await packUser(_game.user2_id, meId);

View file

@ -1,5 +1,3 @@
import { Map } from './maps';
export type Color = 'black' | 'white'; export type Color = 'black' | 'white';
export type MapPixel = 'null' | 'empty'; export type MapPixel = 'null' | 'empty';
@ -11,12 +9,14 @@ export type Options = {
* *
*/ */
export default class Othello { export default class Othello {
public map: Map; public map: MapPixel[];
public mapData: MapPixel[]; public mapWidth: number;
public mapHeight: number;
public board: Color[]; public board: Color[];
public turn: Color = 'black'; public turn: Color = 'black';
public opts: Options; public opts: Options;
public prevPos = -1;
public stats: Array<{ public stats: Array<{
b: number; b: number;
w: number; w: number;
@ -25,18 +25,21 @@ export default class Othello {
/** /**
* *
*/ */
constructor(map: Map, opts: Options) { constructor(map: string[], opts: Options) {
this.map = map;
this.opts = opts; this.opts = opts;
this.mapWidth = map[0].length;
this.mapHeight = map.length;
const mapData = map.join('');
// Parse map data // Parse map data
this.board = this.map.data.split('').map(d => { this.board = mapData.split('').map(d => {
if (d == '-') return null; if (d == '-') return null;
if (d == 'b') return 'black'; if (d == 'b') return 'black';
if (d == 'w') return 'white'; if (d == 'w') return 'white';
return undefined; return undefined;
}); });
this.mapData = this.map.data.split('').map(d => { this.map = mapData.split('').map(d => {
if (d == '-' || d == 'b' || d == 'w') return 'empty'; if (d == '-' || d == 'b' || d == 'w') return 'empty';
return 'null'; return 'null';
}); });
@ -48,8 +51,6 @@ export default class Othello {
}]; }];
} }
public prevPos = -1;
/** /**
* *
*/ */
@ -79,13 +80,13 @@ export default class Othello {
} }
public transformPosToXy(pos: number): number[] { public transformPosToXy(pos: number): number[] {
const x = pos % this.map.size; const x = pos % this.mapWidth;
const y = Math.floor(pos / this.map.size); const y = Math.floor(pos / this.mapHeight);
return [x, y]; return [x, y];
} }
public transformXyToPos(x: number, y: number): number { public transformXyToPos(x: number, y: number): number {
return x + (y * this.map.size); return x + (y * this.mapHeight);
} }
/** /**
@ -145,8 +146,8 @@ export default class Othello {
* @param pos * @param pos
*/ */
public mapDataGet(pos: number): MapPixel { public mapDataGet(pos: number): MapPixel {
if (pos < 0 || pos >= this.mapData.length) return 'null'; if (pos < 0 || pos >= this.map.length) return 'null';
return this.mapData[pos]; return this.map[pos];
} }
/** /**
@ -188,7 +189,7 @@ export default class Othello {
const found = []; const found = [];
while (true) { while (true) {
const [x, y] = fn(i); const [x, y] = fn(i);
if (x < 0 || y < 0 || x >= this.map.size || y >= this.map.size) break; if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) break;
const pos = this.transformXyToPos(x, y); const pos = this.transformXyToPos(x, y);
const pixel = this.mapDataGet(pos); const pixel = this.mapDataGet(pos);
if (pixel == 'null') break; if (pixel == 'null') break;

View file

@ -11,438 +11,502 @@
export type Map = { export type Map = {
name?: string; name?: string;
category?: string; category?: string;
size: number; author?: string;
data: string; data: string[];
}; };
export const fourfour: Map = { export const fourfour: Map = {
name: '4x4', name: '4x4',
category: '4x4', category: '4x4',
size: 4, data: [
data: '----',
'----' + '-wb-',
'-wb-' + '-bw-',
'-bw-' +
'----' '----'
]
}; };
export const sixsix: Map = { export const sixsix: Map = {
name: '6x6', name: '6x6',
category: '6x6', category: '6x6',
size: 6, data: [
data: '------',
'------' + '------',
'------' + '--wb--',
'--wb--' + '--bw--',
'--bw--' + '------',
'------' +
'------' '------'
]
}; };
export const roundedSixsix: Map = { export const roundedSixsix: Map = {
name: '6x6 rounded', name: '6x6 rounded',
category: '6x6', category: '6x6',
size: 6, author: 'syuilo',
data: data: [
' ---- ' + ' ---- ',
'------' + '------',
'--wb--' + '--wb--',
'--bw--' + '--bw--',
'------' + '------',
' ---- ' ' ---- '
]
}; };
export const roundedSixsix2: Map = { export const roundedSixsix2: Map = {
name: '6x6 rounded 2', name: '6x6 rounded 2',
category: '6x6', category: '6x6',
size: 6, author: 'syuilo',
data: data: [
' -- ' + ' -- ',
' ---- ' + ' ---- ',
'--wb--' + '--wb--',
'--bw--' + '--bw--',
' ---- ' + ' ---- ',
' -- ' ' -- '
]
}; };
export const eighteight: Map = { export const eighteight: Map = {
name: '8x8', name: '8x8',
category: '8x8', category: '8x8',
size: 8, data: [
data: '--------',
'--------' + '--------',
'--------' + '--------',
'--------' + '---wb---',
'---wb---' + '---bw---',
'---bw---' + '--------',
'--------' + '--------',
'--------' +
'--------' '--------'
]
}; };
export const roundedEighteight: Map = { export const roundedEighteight: Map = {
name: '8x8 rounded', name: '8x8 rounded',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
' ------ ' + ' ------ ',
'--------' + '--------',
'--------' + '--------',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
'--------' + '--------',
'--------' + '--------',
' ------ ' ' ------ '
]
}; };
export const roundedEighteight2: Map = { export const roundedEighteight2: Map = {
name: '8x8 rounded 2', name: '8x8 rounded 2',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
' ---- ' + ' ---- ',
' ------ ' + ' ------ ',
'--------' + '--------',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
'--------' + '--------',
' ------ ' + ' ------ ',
' ---- ' ' ---- '
]
}; };
export const roundedEighteight3: Map = { export const roundedEighteight3: Map = {
name: '8x8 rounded 3', name: '8x8 rounded 3',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
' -- ' + ' -- ',
' ---- ' + ' ---- ',
' ------ ' + ' ------ ',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
' ------ ' + ' ------ ',
' ---- ' + ' ---- ',
' -- ' ' -- '
]
}; };
export const eighteightWithNotch: Map = { export const eighteightWithNotch: Map = {
name: '8x8 with notch', name: '8x8 with notch',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
'--- ---' + '--- ---',
'--------' + '--------',
'--------' + '--------',
' --wb-- ' + ' --wb-- ',
' --bw-- ' + ' --bw-- ',
'--------' + '--------',
'--------' + '--------',
'--- ---' '--- ---'
]
}; };
export const eighteightWithSomeHoles: Map = { export const eighteightWithSomeHoles: Map = {
name: '8x8 with some holes', name: '8x8 with some holes',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
'--- ----' + '--- ----',
'----- --' + '----- --',
'-- -----' + '-- -----',
'---wb---' + '---wb---',
'---bw- -' + '---bw- -',
' -------' + ' -------',
'--- ----' + '--- ----',
'--------' '--------'
]
}; };
export const circle: Map = { export const circle: Map = {
name: 'Circle', name: 'Circle',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
' -- ' + ' -- ',
' ------ ' + ' ------ ',
' ------ ' + ' ------ ',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
' ------ ' + ' ------ ',
' ------ ' + ' ------ ',
' -- ' ' -- '
]
}; };
export const face: Map = { export const smile: Map = {
name: 'Face', name: 'Smile',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
' ------ ' + ' ------ ',
'--------' + '--------',
'-- -- --' + '-- -- --',
'---wb---' + '---wb---',
'-- bw --' + '-- bw --',
'--- ---' + '--- ---',
'--------' + '--------',
' ------ ' ' ------ '
]
}; };
export const window: Map = { export const window: Map = {
name: 'Window', name: 'Window',
category: '8x8', category: '8x8',
size: 8, author: 'syuilo',
data: data: [
'--------' + '--------',
'- -- -' + '- -- -',
'- -- -' + '- -- -',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
'- -- -' + '- -- -',
'- -- -' + '- -- -',
'--------' '--------'
]
}; };
export const reserved: Map = { export const reserved: Map = {
name: 'Reserved', name: 'Reserved',
category: '8x8', category: '8x8',
size: 8, author: 'Aya',
data: data: [
'w------b' + 'w------b',
'--------' + '--------',
'--------' + '--------',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
'--------' + '--------',
'--------' + '--------',
'b------w' 'b------w'
]
}; };
export const x: Map = { export const x: Map = {
name: 'X', name: 'X',
category: '8x8', category: '8x8',
size: 8, author: 'Aya',
data: data: [
'w------b' + 'w------b',
'-w----b-' + '-w----b-',
'--w--b--' + '--w--b--',
'---wb---' + '---wb---',
'---bw---' + '---bw---',
'--b--w--' + '--b--w--',
'-b----w-' + '-b----w-',
'b------w' 'b------w'
]
};
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 = { export const tenthtenth: Map = {
name: '10x10', name: '10x10',
category: '10x10', category: '10x10',
size: 10, data: [
data: '----------',
'----------' + '----------',
'----------' + '----------',
'----------' + '----------',
'----------' + '----wb----',
'----wb----' + '----bw----',
'----bw----' + '----------',
'----------' + '----------',
'----------' + '----------',
'----------' +
'----------' '----------'
]
}; };
export const hole: Map = { export const hole: Map = {
name: 'The Hole', name: 'The Hole',
category: '10x10', category: '10x10',
size: 10, author: 'syuilo',
data: data: [
'----------' + '----------',
'----------' + '----------',
'--wb--wb--' + '--wb--wb--',
'--bw--bw--' + '--bw--bw--',
'---- ----' + '---- ----',
'---- ----' + '---- ----',
'--wb--wb--' + '--wb--wb--',
'--bw--bw--' + '--bw--bw--',
'----------' + '----------',
'----------' '----------'
]
}; };
export const grid: Map = { export const grid: Map = {
name: 'Grid', name: 'Grid',
category: '10x10', category: '10x10',
size: 10, author: 'syuilo',
data: data: [
'----------' + '----------',
'- - -- - -' + '- - -- - -',
'----------' + '----------',
'- - -- - -' + '- - -- - -',
'----wb----' + '----wb----',
'----bw----' + '----bw----',
'- - -- - -' + '- - -- - -',
'----------' + '----------',
'- - -- - -' + '- - -- - -',
'----------' '----------'
]
}; };
export const cross: Map = { export const cross: Map = {
name: 'Cross', name: 'Cross',
category: '10x10', category: '10x10',
size: 10, author: 'Aya',
data: data: [
' ---- ' + ' ---- ',
' ---- ' + ' ---- ',
' ---- ' + ' ---- ',
'----------' + '----------',
'----wb----' + '----wb----',
'----bw----' + '----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 = { export const walls: Map = {
name: 'Walls', name: 'Walls',
category: '10x10', category: '10x10',
size: 10, author: 'Aya',
data: data: [
' bbbbbbbb ' + ' bbbbbbbb ',
'w--------w' + 'w--------w',
'w--------w' + 'w--------w',
'w--------w' + 'w--------w',
'w---wb---w' + 'w---wb---w',
'w---bw---w' + 'w---bw---w',
'w--------w' + 'w--------w',
'w--------w' + 'w--------w',
'w--------w' + 'w--------w',
' bbbbbbbb ' ' bbbbbbbb '
]
}; };
export const checker: Map = { export const checker: Map = {
name: 'Checker', name: 'Checker',
category: '10x10', category: '10x10',
size: 10, author: 'Aya',
data: data: [
'----------' + '----------',
'----------' + '----------',
'----------' + '----------',
'---wbwb---' + '---wbwb---',
'---bwbw---' + '---bwbw---',
'---wbwb---' + '---wbwb---',
'---bwbw---' + '---bwbw---',
'----------' + '----------',
'----------' + '----------',
'----------' '----------'
]
}; };
export const sixeight: Map = { export const sixeight: Map = {
name: '6x8', name: '6x8',
category: 'special', category: 'special',
size: 8, data: [
data: '------',
' ------ ' + '------',
' ------ ' + '------',
' ------ ' + '--wb--',
' --wb-- ' + '--bw--',
' --bw-- ' + '------',
' ------ ' + '------',
' ------ ' + '------'
' ------ ' ]
}; };
export const spark: Map = { export const spark: Map = {
name: 'Spark', name: 'Spark',
category: 'special', category: 'special',
size: 10, author: 'syuilo',
data: data: [
' - - ' + ' - - ',
'----------' + '----------',
' -------- ' + ' -------- ',
' -------- ' + ' -------- ',
' ---wb--- ' + ' ---wb--- ',
' ---bw--- ' + ' ---bw--- ',
' -------- ' + ' -------- ',
' -------- ' + ' -------- ',
'----------' + '----------',
' - - ' ' - - '
]
}; };
export const islands: Map = { export const islands: Map = {
name: 'Islands', name: 'Islands',
category: 'special', category: 'special',
size: 10, author: 'syuilo',
data: data: [
'-------- ' + '-------- ',
'---wb--- ' + '---wb--- ',
'---bw--- ' + '---bw--- ',
'-------- ' + '-------- ',
' - - ' + ' - - ',
' - - ' + ' - - ',
' --------' + ' --------',
' --------' + ' --------',
' --------' + ' --------',
' --------' ' --------'
]
}; };
export const iphonex: Map = { export const iphonex: Map = {
name: 'iPhone X', name: 'iPhone X',
category: 'special', category: 'special',
size: 12, author: 'syuilo',
data: data: [
' -- -- ' + ' -- -- ',
' -------- ' + '--------',
' -------- ' + '--------',
' -------- ' + '--------',
' -------- ' + '--------',
' ---wb--- ' + '---wb---',
' ---bw--- ' + '---bw---',
' -------- ' + '--------',
' -------- ' + '--------',
' -------- ' + '--------',
' -------- ' + '--------',
' ------ ' ' ------ '
]
}; };
export const bigBoard: Map = { export const bigBoard: Map = {
name: 'Big board', name: 'Big board',
category: 'special', category: 'special',
size: 16, data: [
data: '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '-------wb-------',
'-------wb-------' + '-------bw-------',
'-------bw-------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' + '----------------',
'----------------' +
'----------------' '----------------'
]
}; };
export const twoBoard: Map = { export const twoBoard: Map = {
name: 'Two board', name: 'Two board',
category: 'special', category: 'special',
size: 17, author: 'Aya',
data: data: [
'-------- --------' + '-------- --------',
'-------- --------' + '-------- --------',
'-------- --------' + '-------- --------',
'---wb--- ---wb---' + '---wb--- ---wb---',
'---bw--- ---bw---' + '---bw--- ---bw---',
'-------- --------' + '-------- --------',
'-------- --------' + '-------- --------',
'-------- --------' + '-------- --------'
' ' + ]
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' '
}; };

View file

@ -10,9 +10,9 @@
<template v-else>引き分け</template> <template v-else>引き分け</template>
</p> </p>
<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }"> <div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
<div v-for="(stone, i) in o.board" <div v-for="(stone, i) in o.board"
:class="{ empty: stone == null, none: o.map.data[i] == ' ', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }" :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
@click="set(i)" @click="set(i)"
> >
<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt=""> <img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
@ -106,6 +106,8 @@ export default Vue.extend({
this.o.put(log.color, log.pos); this.o.put(log.color, log.pos);
}); });
console.log(this.o);
this.logs = this.game.logs; this.logs = this.game.logs;
this.logPos = this.logs.length; this.logPos = this.logs.length;
}, },

View file

@ -4,14 +4,17 @@
<p>ゲームの設定</p> <p>ゲームの設定</p>
<el-select v-model="mapName" placeholder="マップを選択" @change="onMapChange"> <el-select class="map" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
<el-option-group v-for="c in mapCategories" :key="c" :label="c"> <el-option-group v-for="c in mapCategories" :key="c" :label="c">
<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"/> <el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
<span style="float: left">{{ m.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
</el-option>
</el-option-group> </el-option-group>
</el-select> </el-select>
<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }"> <div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
<div v-for="(x, i) in game.settings.map.data" <div v-for="(x, i) in game.settings.map.join('')"
:class="{ none: x == ' ' }" :class="{ none: x == ' ' }"
> >
<template v-if="x == 'b'">%fa:circle%</template> <template v-if="x == 'b'">%fa:circle%</template>
@ -112,7 +115,7 @@ export default Vue.extend({
}, },
onMapChange(v) { onMapChange(v) {
this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1]; this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data;
this.connection.send({ this.connection.send({
type: 'update-settings', type: 'update-settings',
settings: this.game.settings settings: this.game.settings
@ -141,6 +144,9 @@ export default Vue.extend({
padding 8px padding 8px
border-bottom dashed 1px #c4cdd4 border-bottom dashed 1px #c4cdd4
> .map
width 300px
> .board > .board
display grid display grid
grid-gap 4px grid-gap 4px