코드스피츠 강의 정리록
생소한 도메인으로 배우는게 좋다. 익숙한 도메인들은 익숙한 처리방법으로 처리하기때문에 객체지향을 배우기 어렵다. 때문에 80기는 게임을 통해서 진행할 예정.
객체지향 프로그래밍을 배우기 전,프로시저 프로그래밍 형태의 코드를 알아본다.
2. 게임 개요 및 규칙
2.1 앤티티 파악 아키텍트나 디자인패턴을 만드는 설계자들은 현상을 보고, 현상으로부터 프로그램 엔티티를 도출해서 설계를 들어간다.
2.2 규칙
Entity
블럭타입: 5가지 (0,1,2,3,4)
cell size: 8*8
Core Action
한번에 3개 이상 같은 색으로 인접한 블록이 선택되면
삭제됨
삭제되면 위의 블록이 내려옴.
내려온 뒤
공간의 블록이 생성되어 채워짐
cf__1 게임이야기 ~ 모바일시대에서는 복잡한 core action이 많이 들어간 게임은 상품성이 없다. (안좋은 예: 와우) core action이 단순한 게임이 상품성이 좋다. 최근 rpg게임들은 단순하게 변했다. 한붓그리기도 이미 오래전에 누가 찾아낸 코어액션중에 하나이다. 재밌는 반복거리를 찾아내면 벗어나려고 하지 않는다.
3. 어디서부터 시작할까? 프로그램의 핵심은 데이터 사람은 표면적인 것만 파악하려고 한다.
표면적인 것과 시각적인 것
경험에 의존에 의해서 파악하기 때문에 경험한 것만 보인다.
프로그램의 핵심은 데이터이다.
거의 대부분의 프로그램들은 다 예쁜 데이터 view.
개발자의 눈으로 보면 프로그램의 핵심은 데이터이다.
entity의 종류를 미리 파악하자. 👀 1 2 3 4 5 6 7 8 column = 8 ; row = 8 ; blockTypes = [0 , 1 , 2 , 3 , 4 ]; Block = class { constructor ( ){}; isNext ( ){}; isValid ( ){}; }
현실세계 entity에는 이 3가지 이외에는 발견하기 힘들다.
스칼라 값
설정에 따라 자유롭게 크기를 변경할 수 있음.
단일 값
가변적
(한번에 하나의 값만 보유할 수 있는 원자량 from wiki )
정의값
미리 정해진 타입이 존재한다.
bolckTypes는 콜렉션 개념이 아니다. 집합을 정의한 개념이다. 하나의 값인것처럼.
객체값
생성되거나 삭제되고, 연결되거나 조건을 파악한다.
블록을 객체로 보는 이유는 블록이 자신만의 책임을 위임할 수 있기 때문 이다.
다음번에 올 수 있는 블록이니?
올바른 위치인 상태이니?
자신의 상태를 은닉하고 캡슐화
블록에는 자신만의 책임을 가질 수 있기 때문에 객체로 평가하게 된다.
UML을 깊이 공부하면 5가지 종류의 여러가지 entity과 관계가 나온다.
cf__2. 클래스이냐? 싱글톤 객체이냐?를 항상 고민한다.
싱글톤 객체 애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴. 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 반환한다. 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이다.
객체는 하나만 있거나 여러개 있다. 하나만 있는 것인지 여러개 있는 것인지 파악해야한다.
하나만 있는데 클래스로 정의하거나 여러개 있는 것인데 객체로 만들면 반드시 고우투헬..
여러개 있는 것을 개별적으로 만들면, 여러개 있는 애들은 보통 변화를 한꺼번에 일으켜야하는데 개별적으로 만들었기 때문에 한꺼번에 수정할 수 없게 된다.
하나만 있어야 하는데 클래스로 만들면 인스턴스가 여러개 생길 수 있다.
3.1 [객체] Block
클래스? 혹은 싱글톤 객체?
책임
1 2 3 4 5 6 7 8 const Block = class { constructor (type ){ this ._type = type; } get image (){return `url('img/${this ._type} .png')` ;} get type (){return this ._type ;} } Block .GET = (type = parseInt (Math .random()*5 ) ) => new Block (type);
자신만의 타입 을 갖는다.
parseInt(Math.random()*5)
: 0 ~ 4 사이의 타입을 얻게됨
인자에 초기화 코드를 명시해주면 선언과 코드의 내용을 분리할 수 있어서 가독성이 좋아진다.
타입에 따른 이미지 경로 를 반환한다. (타입에 따른 블록 이미지가 다르니까.. )
왜 factory 함수로? (Block.GET
)
Block 클래스는 받아온 type을 자기 타입으로 기억하는데 만 관심 있다.
타입을 전달하지 않았을 때 어떻게 만드는지는엄밀하게 따지면 블럭의 지식은 아니다.
블럭을 만드는 쪽이 관심 있다.
만드는 쪽이 관심있는 것과 만들어 지는 쪽이 관심있는 것이 다르다.
랜덤함수는 팩토리 함수가 가져야할 지식이다.
팩토리 함수는 클래스 내부로 가면 안된다.
변화율 때문에!
타입은 stage마다 바뀔꺼고, 아이템 추가되면 더 바뀔 것이다.
타입은 변화율이 높은 개념이므로 클래스와 분리하여 명확한 의도를 작성하게 한다.
변화율을 평가하는 가장 쉬운 방법 변화율을 잘 디자인 했으면 책임이 없다고 생각됬던 객체는 수정되지 않아야한다. 우리의 목표는 보다 더 많은 파일을 안건드리고 일부파일만 건드려야한다.
cf__3. Factory 함수 객체지향할 때 클래스를 사용하는 경우 생성자를 봉인하자.factory함수로 클래스를 얻어가게 하자.
static
키워드 써서 불러오게 해도됨.
factory 함수의 명칭은 일관되게 모든 클래스에 생성자를 대신하는 스태틱 함수를 만들자. (예제에서는 GET
)
팩토리 함수 : 객체를 반환하는 함수
cf__4. 단일 책임원칙 훈련하기 단일 책임원칙은 엄밀하게 지키려면 굉장히 어렵고, 섬세하게 바라보는 눈으로 언제나 의심해서 관리하지 않으면 실력..키워지지 않는다. (빠르면 3년 늦으면 5년..ㅎ)
3.2 [객체] Game 게임이라는 객체는 블록들을 소유하는 마스터 객체
클래스이냐? 싱글톤 객체이냐? 게임은 하나의 객체만 있으면 된다. (바둑판 하나.) 구지 클래스로 선언하지 않고, 싱글톤 객체로 만든다. => 싱글톤 객체
책임 2번과 3번은 내부에서만 알면된다.
초기화 필요한 정보를 바탕으로 게임 본체를 생성
렌더링 그림 갱신
이벤트 걸기 각 블록에서 이벤트를 처리
외부에서 게임의 어떤 상태만 알면 될까?
1 2 3 4 5 6 const Game = (_ => { const init = ... ... return init; })()
4. Game 시나리오
테이블 형태 정의
테이블 생성
테이블 내에 블록 데이터 채우기 (data) 2차원배열을 만들어야하기때문에 row와 column을 돌면서 생성한다.
렌더링
data는 inMemory 객체
render를 따로 호출하는 것은 네이티브 객체이기때문에(dom의 세상..)
렌더링 전 table에 이벤트 걸기.
1. + 2. + 3. [객체, 스칼라값, 정의값] 테이블 데이터 정의
테이블 형태 정의
테이블 생성
테이블 내에 블록 데이터 채우기 (data)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const Game = (_ => { const column = 8 , row = 8 , blockSize = 80 ; const data = []; let table; const init = tid => { table = document .querySelector (tid); for (let i = 0 ; i < row; i++){ const r = []; data.push (r); for (let j = 0 ; j < column; j++) r[j] = Block .GET (); } render (); }; const render = _ => {...} })
4. 렌더링 데이터를 소비해서 표를 다시 그려주는 로직.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const Game = (_ => { const column = 8 , row = 8 , blockSize = 80 ; const data = []; let table; const init = tid => { ... render (); }; 👇👇👇 const el = tag => document .createElement (tag); const render = _ => { table.innerHTML = '' ; data.forEach (row => table.appendChild ( row.reduce ((tr, block ) => { tr.appendChild (el ('td' )).style .cssText = ` ${block? `background: ${block.image} ;` : '' } width: ${blockSize} px; height: ${blockSize} px cursor: pointer` ; return tr; }, el ('tr' )) )); }; 👆👆👆 })
배열을 루프돌아서 각각의 줄을 td를 갖는 tr로 바꿔서 table에 넣고 싶음.
td를 갖는 tr로
각각의 줄을 (td를 갖는) tr로 바꿔서
cf__5. 배열의 고차함수 배열의 고차함수를 쓰는 원리는 간단하다. - 루프돌고 싶으면 forEach
- 배열의 원소를 바꾼 배열을 얻고 싶으면 map
- 하나의 값으로 뭉치고 싶으면 reduce
> 여러개의 집합을 하나의 스칼라값으로 바꾸는 것
5. table에 이벤트 걸기. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const Game = (_ => { const column = 8 , row = 8 , blockSize = 80 ; const data = []; let table; const init = tid => { table = document .querySelector (tid); for (let i = 0 ; i < row; i++){ const r = []; data.push (r); for (let j = 0 ; j < column; j++) r[j] = Block .GET (); } 👇👇👇 table.addEventListener ('mousedown' , down); table.addEventListener ('mouseup' , up); table.addEventListener ('mouseleave' , up); table.addEventListener ('mousemove' , move); 👆👆👆 render (); }; const el = tag => document .createElement (tag); const render = _ => {...} })
mousedown
마우스를 누르는 그 순간. (click과 다르게 press하는 순간)
mouseup
mouseleave
바인딩된 요소에만 이벤트가 발생하며, 해당 엘리먼트의 영역에서 마우스가 벗어날 때 발생한다.
mousemove
5.1 이벤트 > 블럭을 누르는 순간: down
move는 mousedown인 상태여야 의미가 있다. down의 상태값 필요
현재 눌러진 블록의 위치값 알아야함. event로부터 x,y값 전체좌표를 받아와서 x, y좌표를 이용해서 테이블 내에서 몇번째 블록인지, 데이터로 치환하게 된다.
현재 눌러진 블록이 첫 시작? 아니면 중간? 1. 시작 블록 - 지금 선택이 시작되는 블록 - 라이언타입이면 계속 라이언 타입 - 시작값은 왜 알아야하지? - 돌아가기도 해야함. 2. 현재 블록 - move할때마다 변함 (cursor와 같은)
1 2 3 4 5 const down = e => { }
down의 상태값
어떤 블록이 선택되어있는지.
x,y값을 넣으면 몇번째인지 알아낼 수 있는 함수
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const column = 8 , row = 8 , blockSize = 80 ;const data = [];let table;👇👇👇 let startBlock, currBlock, isDown;const selected = [], getBlock = (x, y ) => {...}const down = ({pageX: x, pageY: y} ) => { if (isDown) return ; const curr = getBlock (x,y); if (!curr) return ; isDown = true ; selected.length = 0 ; selected[0 ] = startBlock = currBlock = curr; render (); } 👆👆👆
getBlock이외의 밑에나오는 로직은 전부 인메모리 로직. 네이티브 레이어를 다루는 방법은 즉시 인메모리 객체로 변환한다.
cf__6. 네이티브 객체를 다룰때 가장 중요한 요령
네이티브 레이어를 최대한 줄이는 것.
네이티브의 정보 중에 필요한 핵심정보만 이용해서 즉시 인메모리 객체로 바꿈.
네이티브의 코드가 많이 퍼져있으면 퍼져있을수록 더욱더 다루기 어렵고 더욱더 컨버팅하기 어렵게 된다.
네이티브 코드중에 필요한 부분만 추출해서 => 즉시 인메모리 객체로 바꾸고
나머지 로직은 인메모리에서 수용하도록 함.
좋은 개발자 말하면 코드로 옮기는 것이 리얼타임이 될때까지.. 훈련하자. 좋은 개발자는 제어문을 잘쓰거나, 코드를 한국어로 번역하거나 한국어를 코드로 번역하는 능력에 달려있다. 숙련을 많이 해서 실시간으로.
5.cf 네이티브 코드 => 인메모리 객체로 변환 1 2 3 4 5 6 7 8 9 const getBlock = (x : number , y : number ): Block => { const { top : T, left : L } = table.getBoundingClientRect (); if (x < L || x > L + blockSize * row || y < T || y > T + blockSize * column) return null ; return data[parseInt (y - T) / blockSize][parseInt (x - L) / blockSize]; };
getBoundingClientRect
Element.getBoundingClientRect() 메서드는 요소의 크기와 요소의 viewport에서의 상대적인 위치를 반환합니다.1 2 3 4 5 6 7 8 9 10 DOMRect { x: 0, y: 0, width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 }
5.2 이벤트 > 블럭을 떼는 순간: up 코어액션이 여기서 다 일어난다.
down을 해제
선택목록이 3이상이면 삭제 실시
2이하면 리셋
5.2.0 reset 선택항목이 2이하면 모두 초기화시킨다.1 2 3 4 5 6 7 8 const up = _ => selected.length > 2 ? remove () : reset ();const reset = _ => { startBlock = currBlock = null ; selected.length = 0 ; isDown = false ; render (); }
cf__7. 쉬운것부터 짜자! 쉬운 것은 의존성이 없다. 고칠 일이 적다. 복잡한 것부터 짜면 의존성이 많은 것부터 짜게 되고, 나중에 깨달은게 많을수록 더 많이 고치게 된다.
선택된 블록들을 지워주고 => remove
떨어뜨린 다음에 => drop
새로 생성하고 => readyToFill
다시 내려와주면서, 합쳐줘야함. => fill
5.2.1 remove 😫😫 1 2 3 4 5 6 7 8 9 10 11 12 13 const remove = ( ) => { data.forEach (r => { selected.forEach (v => { let i; if ((i = r.indexOf (v)) !== -1 ) r[i] = null ; }); }); render (); setTimeout (drop, 300 ); };
forEach는 for문보다 대화가됨.
column을 돌면서 row를 계산해서 떨어뜨리는 일을 한다. 한턴에는 한칸씩 떨어진다. 중력에 의해 떨어지는 방향은 column기준이기때문에 column을 먼저 loop돌린다. row는 맨 아랫줄부터 떨어지는 작업을 해야하므로.. (전체row -1) 부터 시작
isNext: 윗줄에도 block이 있는 상황이라 다음 row도 검사해야하는지.
isEmpty: 떨어질 블럭이 하나의 column row들에 있는지 없는지.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const drop = _ => { let isNext = false ; for (let j = 0 ; j < column; j++) { for (let i = row - 1 ; i > -1 ; i--) { if (!data[i][j] && i) { let k = i, isEmpty = true ; while (k--) if (data[k][j]) { isEmpty = false ; break ; } if (isEmpty) break ; isNext = true ; while (i--) { data[i + 1 ][j] = data[i][j]; data[i][j] = null ; } break ; } } } render (); isNext ? setTmeout (drop, 300 ) : readyToFill (); };
ㅋㅋㅋ ㅠㅠ 대댜냐댜.. 머리가 좋아지는수밖에 없다. 잘짜고, 쉽게 짤때까지 머리로 훈련하는 수밖에 없다. 알고리즘, 코딩인터뷰, 자료구조, 트리구조 보다 도메인 해석 능력을 키우자. 어떠한 도메인에 대해서 알고리즘을 짜는 능력은 훈련밖에 없다. 복잡한 일이 일어나는 것을 눈으로 관찰해서 어떤 일인지 파악한 다음에 코드로 차근차근 푸는건 훈련밖에 없다. 이거 안되면 아키텍처고 디자인패턴이고 필요없다..
채울준비를 하는 것은 밖에부터 채워야하는 애들이 예쁘게 내려오게 하기 위해서.
지워진 모양 그대로의 형태가 위에 형성되고 => readyToFill
해당 형태가 떨어지는 상황 => fill
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const fills = [];let fillCnt = 0 ;const readyToFill = _ => { fills.length = 0 ; data.some (row => { if (row.indexOf (null ) === -1 ) return ture; const r = [...row].fill (null ); fills.push (r); row.forEach ((v, i ) => !v && (r[i] = Block .GET ())); }); fillCnt = 0 ; setTimeout (fill, 300 ); }; const fill = _ => { if (fillCnt > fills.length ) { isDown = false ; return ; } for (let i = 0 ; i < fillCnt; i++) { fills[fills.length - i - 1 ].forEach ((v, j ) => { if (v) data[fillCnt - i - 1 ][j] = v; }); } fillCnt++; render (); setTimeout (fill, 300 ); };
5.3 이벤트 > 블럭을 down하면서 움직이는 상태: move 누른상태에서 다음(혹은 이전) 블럭으로 움직이는 상태
1 2 3 4 5 6 7 const move = e => { }
1 2 3 4 5 6 7 8 9 const move = ({ pageX: x, pageY: y } ) => { if (!isDown) return ; const curr = getBlock (x, y); if (!crr || curr.type !== startBlock.type || !isNext (curr)) return ; if (selected.indexOf (curr) == -1 ) selected.push (curr); else if (selected[selected.length - 1 ] == curr) selected.pop (); currBlock = curr; render (); };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const isNext = curr => { let r0, c0, r1, c1, cnt = 0 ; data.some ((row, i ) => { let j; if ((j = row.indexOf (currBlock)) !== -1 ) (r0 = i), (c0 = j), cnt++; if ((j = row.indexOf (curr)) !== -1 ) (r1 = i), (c1 = j), cnt++; return cnt === 2 ; }); return ( curr !== currBlock && (Math .abs (r0 - r1) === 1 || Math .abs (c0 - c1) === 1 ) ); };
자바스크립트에서 배열의 고차함수로 여러 제어문을 제거할 수 있다.
최종 코드 및 정리 프로시저 함수: 반환값이 없는 함수 sideEffect가 엄청 많다. 현재 로직들은 모두 프로시저 함수. 앞으로 객체지향으로 리팩토링 예정.
최종코드 (TS ver.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 interface IGame { column : number ; row : number ; blockSize : number ; data : (Block | null )[][]; table : HTMLElement ; startBlock : Block ; currBlock : Block ; isDown : boolean ; selected : Block []; isNext : (curr : Block ) => boolean ; getBlock : (x : number , y : number ) => Block ; down : ({ pageX: x, pageY: y }: { pageX: number ; pageY: number } ) => void ; move : ({ pageX: x, pageY: y }: { pageX: number ; pageY: number } ) => void ; up : () => void ; reset : () => void ; remove : () => void ; drop : () => void ; fills : (Block | null )[][]; fillCnt : number ; readyToFill : () => void ; fill : () => void ; el : (tag : HTMLElementTagNameMap ) => HTMLElement ; render : () => void ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 const Game = (() => { class Block { static GET (type = Math .random() * 5 ) { return new Block (type ); } _type : number ; constructor (type ) { this ._type = type ; } get image () { return `url('img/block${this ._type} .png')` ; } get type () { return this ._type ; } } const column = 8 , row = 8 , blockSize = 80 ; const data : (Block | null )[][] = []; let table : HTMLElement ; let startBlock : Block , currBlock : Block , isDown : boolean ; const selected : Block [] = []; const getBlock = (x : number , y : number ): Block => { const { top : T, left : L } = table.getBoundingClientRect (); if (x < L || x > L + blockSize * row || y < T || y > T + blockSize * column) return null ; return data[(y - T) / blockSize][(x - L) / blockSize]; }; const isNext = (curr : Block ): boolean => { let r0 : number , c0 : number , r1 : number , c1 : number , cnt = 0 ; data.some ((row : (Block | null )[], i : number ) => { let j : number ; if ((j = row.indexOf (currBlock)) != -1 ) (r0 = i), (c0 = j), cnt++; if ((j = row.indexOf (curr)) != -1 ) (r1 = i), (c1 = j), cnt++; return cnt == 2 ; }); return ( (curr != currBlock && Math .abs (r0 - r1) == 1 ) || Math .abs (c0 - c1) == 1 ); }; const reset = ( ) => { startBlock = currBlock = null ; selected.length = 0 ; isDown = false ; render (); }; const remove = ( ) => { data.forEach ((r : (Block | null )[] ) => { selected.forEach ((v : Block ) => { let i : number ; if ((i = r.indexOf (v)) != -1 ) r[i] = null ; }); }); render (); setTimeout (drop, 300 ); }; const drop = ( ) => { let isNext = false ; for (let j = 0 ; j < column; j++) { for (let i = row - 1 ; i > -1 ; i--) { if (!data[i][j] && i) { let k = i, isEmpty = true ; while (k--) if (data[k][j]) { isEmpty = false ; break ; } if (isEmpty) break ; isNext = true ; while (i--) { data[i + 1 ][j] = data[i][j]; data[i][j] = null ; } break ; } } } render (); isNext ? setTimeout (drop, 300 ) : readyToFill (); }; const fills : (Block | null )[][] = []; let fillCnt = 0 ; const readyToFill = ( ) => { fills.length = 0 ; data.some ((row : (Block | null )[] ) => { if (row.indexOf (null ) == -1 ) return true ; const r : (Block | null )[] = [...row].fill (null ); fills.push (r); row.forEach ((v : Block | null , i ) => !v && (r[i] = Block .GET ())); }); fillCnt = 0 ; setTimeout (fill, 300 ); }; const fill = ( ) => { if (fillCnt > fills.length ) { isDown = false ; return ; } for (let i = 0 ; i < fillCnt; i++) { fills[fills.length - i - 1 ].forEach ((v, j ) => { if (v) data[fillCnt - i - 1 ][j] = v; }); } fillCnt++; render (); setTimeout (fill, 300 ); }; const down = ({ pageX: x, pageY: y }: { pageX: number ; pageY: number } ) => { if (isDown) return ; const curr = getBlock (x, y); if (!curr) return ; isDown = true ; selected.length = 0 ; selected[0 ] = startBlock = currBlock = curr; render (); }; const move = ({ pageX: x, pageY: y } ) => { if (!isDown) return ; const curr = getBlock (x, y); if (!curr || curr.type != startBlock.type || !isNext (curr)) return ; if (selected.indexOf (curr) == -1 ) selected.push (curr); else if (selected[selected.length - 2 ] == curr) selected.pop (); currBlock = curr; render (); }; const up = ( ) => (selected.length > 2 ? remove () : reset ()); const el = (tag : keyof HTMLElementTagNameMap ) => document .createElement (tag); const render = ( ) => { table.innerHTML = "" ; data.forEach (row => table.appendChild ( row.reduce ((tr, block ) => { tr.appendChild (el ("td" )).style .cssText = ` ${block ? `background:${block.image} ;` : "" } width:${blockSize} px; height:${blockSize} px; cursor:pointer` ; return tr; }, el ("tr" )) ) ); }; return (tid : string ) => { table = document .querySelector (tid); for (let i = 0 ; i < row; i++) { const r = []; data.push (r); for (let j = 0 ; j < column; j++) r[j] = Block .GET (); } table.addEventListener ("mousedown" , down); table.addEventListener ("mouseup" , up); table.addEventListener ("mouseleave" , up); table.addEventListener ("mousemove" , move); render (); }; })(); Game ("#stage" );