प्रौद्योगिकी साझेदारी

हस्तेन टेट्रिस् क्रीडनम् (3) - क्रीडायाः कोरमॉड्यूल् डिजाइनम्

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

हस्तेन टेट्रिस् क्रीडनम् - क्रीडायाः कोरमॉड्यूल् डिजाइनम्

क्रीडां आरभत

पूर्वस्य डिजाइनस्य अनुसारं अस्माकं कृते क्रीडायाः आरम्भार्थं क्रीडायाः आवश्यकाः तत्त्वानि आवश्यकानि सन्ति ।

import { ConsoleCanvas, ConsoleController, ConsoleColorTheme, Color } from '@shushanfx/tetris-console';
import { Dimension, ColorFactory, Game } from '@shushanfx/tetris-core';

const theme = new ConsoleColorTheme();
const canvas = new ConsoleCanvas(theme);
const controller = new ConsoleController();
const dimension = new Dimension(10, 20);
const factory = new ColorFactory(dimension, [
  Color.red,
  Color.green,
  Color.yellow,
  Color.blue,
  Color.magenta,
  Color.cyan,
]);
const game = new Game({ dimension, canvas, factory, controller });
game.start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

पङ्क्तिपङ्क्तिविश्लेषणं कुर्मः : १.

  • संकुलस्य परिचयः tetris-core तथा tetris-console इति विभक्तः अस्ति एतत् संकुलस्य विभाजनम् अस्ति कोरसङ्कुलस्य घटकाः tetris-core इत्यत्र स्थापिताः सन्ति, विशिष्टं कार्यान्वयनञ्च tetris-console इत्यत्र स्थापितं भवति ।

  • विषयस्य, कैनवासस्य, नियन्त्रकस्य च आरम्भः;

  • कारखानस्य आरम्भः तथा आयामः;

  • क्रीडायाः आरम्भार्थं पूर्वं आरभितानां कैनवास, कारखाना, कैनवास, आयामवस्तूनाम् उपयोगं कुर्वन्तु;

  • क्रीडा आरम्भविधिं आह्वयति।

तदनन्तरं पश्यामः यत् आरम्भः किं करोति ?

Game.start तर्कः


class Game {
  start() {
    const { status } = this;
    if (status === GameStatus.RUNNING) {
      return ;
    }
    if (status === GameStatus.OVER
      || status === GameStatus.STOP) {
      this.stage.reset();
      this.canvas.render();
    } else if (status === GameStatus.READY) {
      this.controller?.bind();
      this.canvas.render();
    }
    this.status = GameStatus.RUNNING;
    this.tickCount = 0;
    this.canvas.update();
    // @ts-ignore
    this.tickTimer = setInterval(() => {
      if (this.tickCount == 0) {
        // 处理向下
        this.stage.tick();
        this.checkIsOver();
      }
      this.canvas.update();
      this.tickCount++;
      if (this.tickCount >= this.tickMaxCount) {
        this.tickCount = 0;
      }
    }, this.speed);
  }
}

  • 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

पङ्क्तिपङ्क्तिविश्लेषणं कुर्मः : १.

  1. प्राप्नोतुstatusचरः, २.statusकृतेGameक्रीडावस्थायाः आन्तरिकप्रतिनिधित्वं क्रमशः准备就绪(READY)游戏中(RUNNING)暂停(PAUSE)停止(STOP)游戏结束(OVER) . स्टॉप-क्रीडा-अन्तयोः भेदः अस्ति यत् पूर्वः सक्रियरूपेण क्रीडां स्थगयति, उत्तरं तु क्रीडायाः अन्त्य-तर्कं प्रेरयति, क्रीडायाः समाप्तिम् अपि जनयति

  2. यदि क्रीडा प्रचलति तर्हि प्रत्यक्षतया प्रत्यागच्छन्तु;

  3. यदि क्रीडा निवृत्ता क्रीडा समाप्तं च भवति तर्हिStageएकं रीसेटं कुर्वन्तु तथा चcanvasसमग्रं पुनः आकर्षणं कुर्वन्तु।

  4. यदि सज्जतायां क्रीडा निरन्तरं भवति तर्हि क्रीडा अधुना एव आरम्भीकरणं सम्पन्नवती, कदापि न आरब्धा इति अर्थः । इवेण्ट् बाइण्ड् कर्तुं प्रथमवारं च कैनवासं आकर्षयितुं नियन्त्रकं आह्वयन्तु;

  5. क्रीडावस्थां इति सेट् कुर्वन्तु 游戏中(RUNNING), आन्तरिक अवस्था tickCount = 0;

  6. स्थानांतरणcanvasतत्क्षणमेव आंशिकं अद्यतनं कुर्वन्तु अत्र मुख्यं अद्यतनं यत् स्थितिः परिवर्तिता अस्ति, येन क्रीडायाः स्थितिः पुनः प्रस्तुतीकरणस्य आवश्यकता वर्तते;

  7. समयनिर्धारकं आरभत, समयनिर्धारकसमयः this.speed, .speedभविष्ये वयं तस्य क्रीडास्तरेन (अद्यापि समर्थितं न) सह मेलयितुम् विचारयिष्यामः ।

  • यदि tickCount == 0, तर्हि कस्यचित् चरणस्य टिक-क्रियाम् उद्दीपयन्तु, तथा च ट्रिगर करणस्य अनन्तरं तत्क्षणमेव समाप्तं भवति वा इति पश्यन्तु;
  • कैनवासस्य अद्यतनसञ्चालनं ट्रिगर कुर्वन्तु
  • tickCount स्वयमेव वर्धते तथा च पुनः सेट् भवति यदि >= tickMaxCount सन्तुष्टः भवति;

tickCount तन्त्रस्य प्रवर्तनस्य कारणं मुख्यतया कैनवासस्य अद्यतन-आवृत्तिः सुनिश्चित्य भवति ।

स्टेज टिक

यथा उपरिष्टात् कोडात् दृश्यते, क्रीडायाः मूलतर्कः अस्तिstage.tick, तस्य आन्तरिकं कार्यान्वयनम् अस्ति ।

class Stage {
  tick(): void {
    if (this.isOver || this.clearTimers.length > 0) {
      return;
    }
    // 首次加载,current为空
    if (!this.current) {
      this.next = this.factory.randomBlock();
      this.toTop(this.next);
      this.current = this.factory.randomBlock();
      this.toTop(this.current);
      return ;
    }
    const isOver = this.current.points.some((point) => {
      return !this.points[point.y][point.x].isEmpty;
    });
    if (isOver) {
      this.isOver = true;
      return;
    }
    const canMove = this.current.canMove('down', this.points);
    if (canMove) {
      this.current.move('down');
    } else {
      this.handleClear();
    }
  }
}
  • 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
  • प्रथमं निर्धारयन्तु यत् क्रीडा समाप्तम् अस्ति वा स्वच्छताक्रिया क्रियते वा इति।

  • यदिcurrentयदि रिक्तं भवति तर्हि प्रथमवारं क्रीडा लोड् कृत्वा पृथक् आरभ्यते इति अर्थः ।currentतथाnext

  • क्रीडा अन्त्यस्थितिं प्राप्नोति वा इति निर्धारयतु अर्थात्currentतथाpoints तत्र आच्छादनम् । यदि आच्छादनं भवति तर्हि क्रीडा समाप्तम् इति चिह्निता भवति ।

  • वर्तमानधारा अधः गन्तुं शक्नोति वा इति निर्धारयतु यदि अधः गन्तुं शक्नोति तर्हि एकं स्थानं अधः गन्तुं शक्नोति वा इति ।

तदनन्तरं वयं पश्यामः यत् कथं निष्कासनं ज्ञातुं शक्यते अर्थात्handleClearसाक्षात्कारः ।

class Stage {
  private handleClear() {
    if (!this.current) {
      return;
    }
    // 1. 复制新的points
    const pointsClone: Point[][] = this.points.map((row) => row.map((point) => point.clone()));
    this.current.points.forEach((point) => {
      pointsClone[point.y][point.x] = point.clone();
    });
    // 2. 检查是否有消除的行
    const cleanRows: number[] = [];
    for(let i = 0; i < pointsClone.length; i ++) {
      const row = pointsClone[i];
      const isFull = row.every((point) => {
        return !point.isEmpty
      });
      if (isFull) {
        cleanRows.push(i);
      }
    }
    // 3. 对行进行消除
    if (cleanRows.length > 0) {
      this.startClear(pointsClone, cleanRows, () => {
        // 处理计算分数
        this.score += this.getScore(cleanRows.length);
        // 处理消除和下落
        cleanRows.forEach((rowIndex) => {
          for(let i = rowIndex; i >= 0; i--) {
            if (i === 0) {
              pointsClone[0] = Array.from({ length: this.dimension.xSize }, () => new Point(-1, -1));
            } else {
              pointsClone[i] = pointsClone[i - 1];
            }
          }
        });
        // 4. 扫尾工作,变量赋值
        this.points = pointsClone;
        this.current = this.next;
        this.next = this.factory.randomBlock();
        this.toTop(this.next);
      });
    } else {
      // 4. 扫尾工作,变量赋值
      this.points = pointsClone;
      this.current = this.next;
      this.next = this.factory.randomBlock();
      this.toTop(this.next);
    }
  }
}
  • 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

यथा उपर्युक्तसङ्केतात् दृश्यते, सम्पूर्णा प्रक्रिया चतुर्धा विभक्ता अस्ति ।

  1. वर्तमानं वर्तमानं च बिन्दून् समाविष्टं नूतनं pointsClone प्रतिलिपिं कुर्वन्तु ।

  2. points ज्ञापयन्तुClone रेखां रेखा, तथा च चिह्नं कुर्वन्तु यत् यदि सम्पूर्णा रेखा पूरिता अस्ति;

  3. 2 इत्यस्मिन् उत्पन्नस्य चिह्नितसामग्रीनुसारं पङ्क्तिं पङ्क्तिं विलोपयन्तु। ध्यानं कुर्वन्तु यत् पङ्क्तिं विलोपनं कुर्वन् उपरितः अधः यावत् विलोपनक्रिया क्रियते ।

  4. कार्यस्य स्वच्छता।एतत् पदं क्लियरिंग् ऑपरेशनं Assign pointsClone to इति न कृत्वा अपि कर्तव्यम्this.points, एकस्मिन् समये सम्पूर्णम्currentतथाnextनुदति।

परिभ्रमति

खण्डानां परिभ्रमणं किं प्रचलति ?

सर्वे परिभ्रमणव्यवहाराः game.rotate पद्धतिं आह्वयित्वा प्रवर्तन्ते, यत्र नियन्त्रकेन परिभाषिताः घटनाः, बाह्य-आह्वानाः इत्यादयः सन्ति;

Game इत्यस्मिन् कार्यान्वितः तर्कः निम्नलिखितरूपेण अस्ति ।

class Game {
  rotate() {
    this.stage.rotate();  
    this.canvas.update();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

अग्रे पश्यन्तुStageसाक्षात्कारः

class Stage {
  rotate(): boolean {
    if (!this.current) {
      return false;
    }
    const canChange = this.current.canRotate(this.points);
    if (canChange) {
      this.current.rotate();
    }
    return false;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • प्रथमं न्यायं कुरुतcurrentअस्ति वा, यदि नास्ति तर्हि साक्षात् प्रत्यागमिष्यति;

  • स्थानांतरणcurrentइत्यस्यcanRotateवर्तमानस्थानं परिभ्रमितुं शक्यते वा इति परीक्षितुं विधिः यदि तत् चयनं कर्तुं शक्यते तर्हि परिभ्रमणविधिं परिभ्रमितुं आह्वयन्तु ।

अग्रे गत्वा पश्यामःBlockइत्यस्यcanRotateतथाrotateप्रक्रिया।

class Block {
  canRotate(points: Point[][]): boolean {
    const centerIndex = this.getCenterIndex();
    if (centerIndex === -1) {
      return false;
    }
    const changes = this.getChanges();
    if (changes.length === 0) {
      return false;
    }
    const nextChange = changes[(this.currentChangeIndex + 1) % changes.length];
    const newPoints = this.changePoints(this.points, this.points[centerIndex], nextChange);
    const isValid = Block.isValid(newPoints, this.dimension);
    if (isValid) {
      return newPoints.every((point) => {
        return points[point.y][point.x].isEmpty;
      });
    }
    return isValid;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

प्रथमं पश्यामःcanRotateसाक्षात्कारः ।

  • centerIndex प्राप्नुवन्तु, centerIndex इति परिभ्रमणस्य केन्द्रबिन्दुस्य अनुक्रमणिका अस्ति । प्रत्येकं चित्रं भिन्नं भवति, यथा IBlock, यत् निम्नलिखितरूपेण परिभाषितम् अस्ति ।

    class IBlock extends Block {
      getCenterIndex(): number {
        return 1;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    अर्थात् परिभ्रमणकेन्द्रबिन्दुः द्वितीयः नोडः ।इव口口口口, द्वितीयः केन्द्रबिन्दुः口田口口

    तदतिरिक्तं अस्य खण्डस्य डिजाइनं कुर्वन्तः अस्माभिः इदमपि विचारितम् यत् केचन खण्डाः परिभ्रमितुं न शक्यन्ते, यथा OBlock, येषां चयनं कर्तुं न शक्यते ।किन्तुgetCenterIndexनिर्वतनम्-1

  • परिवर्तनानां सरणीं प्राप्नुवन्तु सरणी वर्तमानघूर्णनस्य कोणः इति परिभाषिता अस्ति सरणीयाः सामग्रीः अन्तिमपरिभ्रमणस्य सापेक्षतया अस्य परिभ्रमणस्य कोणं प्रतिनिधियति ।इवIBlockइति विवक्षितम् ।

    class IBlock extends Block {
      currentChangeIndex: number = -1;
      getChanges(): number[] {
        return [
          Math.PI / 2,
          0 - Math.PI / 2
        ];
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    अर्थात् प्रथमः परिभ्रमणं प्रारम्भिकस्थितिः Math.PI / 2 (अर्थात् 90 डिग्री), द्वितीयं च परिभ्रमणं प्रथमस्य परिभ्रमणस्य (अर्थात् -90 डिग्री) -Math.PI / 2 भवति यथा- १.

    // 初始状态
    // 口田口口
    
    // 第一次旋转
    //  口
    //  田
    //  口
    //  口
    
    // 第二次旋转
    // 口田口口
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    पुनश्च: अत्र कृपया ज्ञातव्यं यत् निर्देशांक-अक्षः वामतः दक्षिणतः, उपरितः अधः च भवति ।

  • परिभ्रमणनिर्णयस्य कृते निर्णयमापदण्डाः सन्ति- १.

    1. परिभ्रमितनिर्देशाङ्कबिन्दवः सम्पूर्णस्य क्रीडायाः सीमां अतिक्रमितुं न शक्नुवन्ति;
    2. परिभ्रमितनिर्देशाङ्कबिन्दवः पूरितवर्गस्य बिन्दून् आकर्षयितुं न शक्नुवन्ति ।

    अतः सन्ति इति पश्यामःisValidतथाnewPoints.everyन्यायः ।

अग्रे पश्यामःBlock.rotate,यथा : १.

class Block {
  rotate() {
    const centerIndex = this.getCenterIndex();
    if (centerIndex === -1) {
      return false;
    }
    const changes = this.getChanges();
    if (changes.length === 0) {
      return false;
    }
    const nextChange = changes[(this.currentChangeIndex + 1) % changes.length];
    const newPoints = this.changePoints(this.points, this.points[centerIndex], nextChange);
    const isValid = Block.isValid(newPoints, this.dimension);
    if (isValid) {
      this.currentChangeIndex = (this.currentChangeIndex + 1) % changes.length;
      this.points = newPoints;
    }
    return isValid;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

उपर्युक्तवर्णनद्वारा, २.rotateतर्कः सुलभः अस्ति ।

  • प्राप्नोतुcenterIndexतथाchanges,भविष्यतिcurrentChangeIndexचक्रीयवृद्धिं कृत्वा Block इत्येतत् नूतननिर्देशाङ्कं प्रति सूचयन्तु ।

  • इत्यस्मिन्‌currentChangeIndex प्रारम्भिकं मूल्यं -1 अस्ति, यस्य अर्थः अस्ति यत् वर्तमानं परिभ्रमणं प्रचलति, यदि च 0 इत्यस्मात् अधिकं वा समं वा भवति तर्हि अनुक्रमणिका + 1 चयनितम् इति अर्थः । (कृपया अत्र सम्यक् चिन्तयन्तु, यतः सरणीयाः अनुक्रमणिका 0 तः आरभ्यते)

चलनम्

गतिः इति अर्थः खण्डस्य चतुर्दिक्षु चालनम् ।तस्य कार्यान्वयनम् पश्यामः

class Game {
  move(direction: Direction) {
    this.stage.move(direction);
    this.canvas.update();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

तेषु दिग्विवक्षिता यथा ।

type Direction = 'up' | 'down' | 'left' | 'right'
  • 1

अधिकं पश्यन्तुStageकार्यान्वयनम् : १.

class Stage {
  move(direction: Direction) {
    if (!this.current) {
      return false;
    }
    const canMove = this.current.canMove(direction, this.points);
    if (canMove) {
      this.current.move(direction);
    }
    return canMove;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

अधिकं पश्यन्तुcanMoveतथाmoveसाक्षात्कारः ।

class Block {
  canMove(direction: Direction, points: Point[][]): boolean {
    return this.points.every((point) => {
      switch (direction) {
        case 'up':
          return point.y > 0 && points[point.y - 1][point.x].isEmpty;
        case 'down':
          return point.y < this.dimension.ySize - 1 && points[point.y + 1][point.x].isEmpty;
        case 'left':
          return point.x > 0 && points[point.y][point.x - 1].isEmpty;
        case 'right':
          return point.x < this.dimension.xSize - 1 && points[point.y][point.x + 1].isEmpty;
      }
    });
  };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

संक्षेपेण तस्य अनुवादः यथा भवति ।

  • उपरि गन्तुं सर्वे y-अक्षबिन्दवः 0 इत्यस्मात् अधिकाः (अर्थात् 1 इत्यस्मात् अधिकाः वा समानाः वा) भवेयुः, गमनात् परं बिन्दुः रिक्तबिन्दवः भवितुमर्हन्ति;

  • वामभागे शिफ्ट् कुर्वन्तु, सर्वे x-अक्षबिन्दवः 0 इत्यस्मात् अधिकाः भवेयुः (अर्थात् 1 इत्यस्मात् अधिकाः वा समानाः वा), तथा च चालनस्य अनन्तरं बिन्दवः रिक्तबिन्दवः भवेयुः;

  • दक्षिणदिशि स्थानान्तरणं कुर्वन्तु, सर्वे x-अक्षबिन्दवः x-निर्देशाङ्क-अक्षस्य -1 (अर्थात् xSize - 2 इत्यस्मात् न्यूनाः वा समानाः वा) दीर्घतायाः अपेक्षया न्यूनाः भवितुमर्हन्ति, तथा च चालनस्य अनन्तरं बिन्दवः रिक्तबिन्दवः भवेयुः

  • अधः गन्तुं सर्वे y-अक्षबिन्दवः y-समन्वय-अक्षस्य -1 (अर्थात् ySize - 2 इत्यस्मात् न्यूनाः वा समानाः वा) दीर्घतायाः अपेक्षया न्यूनाः भवितुमर्हन्ति, तथा च गमनात् परं बिन्दवः रिक्तबिन्दवः भवेयुः

गतिशर्ताः पूरयित्वा पश्यामःmoveसाक्षात्कारः ।

class Block {
  move(direction: Direction): boolean {
    switch (direction) {
      case 'up':
        this.points.forEach((point) => { point.y = point.y - 1})
        break;
      case 'down':
        this.points.forEach((point) => { point.y = point.y + 1})
        break;
      case 'left':
        this.points.forEach((point) => { point.x = point.x - 1})
        break;
      case 'right':
        this.points.forEach((point) => { point.x = point.x + 1})
        break;
    }
    return true;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

निर्देशांकबिन्दुस्य मूल्यं प्रत्यक्षतया परिवर्तयन्तु ।

संक्षेपः

अस्मिन् अध्याये क्रीडायाः त्रयः महत्त्वपूर्णाः व्यवहाराः वर्णिताः सन्ति - स्वच्छता, परिभ्रमणं, चलनं च । त्रयः परस्परं सहकार्यं कृत्वा क्रीडां सम्पन्नं कुर्वन्ति । अग्रिमे अध्याये वयं क्रीडायाः interface rendering तथा operation control इत्येतत् साझां कुर्मः ।