Game.tsx 8.42 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * MIT License
 *
 * Copyright (c) 2020, Concordant and contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

Yannick Li's avatar
Yannick Li committed
25
import React from "react";
Yannick Li's avatar
Yannick Li committed
26
import assert from "assert";
27
28
import Grid from "./Grid";
import { GRIDS } from "../constants";
Yannick Li's avatar
Yannick Li committed
29
import { client } from "@concordant/c-client";
Yannick Li's avatar
Yannick Li committed
30
import Submit1Input from "./Submit1Input";
Yannick Li's avatar
Yannick Li committed
31

Yannick Li's avatar
Yannick Li committed
32
import CONFIG from "../config.json";
Yannick Li's avatar
Yannick Li committed
33

34
35
const TIMEOUTGET = 60000;

Yannick Li's avatar
Yannick Li committed
36
37
38
39
40
41
42
const session = client.Session.Companion.connect(
  CONFIG.dbName,
  CONFIG.serviceUrl,
  CONFIG.credentials
);
const collection = session.openCollection("sudoku", false);

Yannick Li's avatar
Yannick Li committed
43
44
45
46
47
/**
 * Interface for the state of a Game.
 * Keep a reference to the opened session and opened MVMap.
 */
interface IGameState {
Yannick Li's avatar
Yannick Li committed
48
49
  gridNum: string;
  mvmap: any; // eslint-disable-line @typescript-eslint/no-explicit-any
50
  cells: { value: string; modifiable: boolean }[];
Yannick Li's avatar
Yannick Li committed
51
  isConnected: boolean;
Yannick Li's avatar
Yannick Li committed
52
}
53
54
55
56

/**
 * This class represent the Game that glues all components together.
 */
Yannick Li's avatar
Yannick Li committed
57
class Game extends React.Component<Record<string, unknown>, IGameState> {
58
  timeoutGet!: NodeJS.Timeout;
Yannick Li's avatar
Yannick Li committed
59
60
  modifiedCells: string[];

Yannick Li's avatar
Yannick Li committed
61
62
  constructor(props: Record<string, unknown>) {
    super(props);
Yannick Li's avatar
Yannick Li committed
63
64
    const cells = new Array(81)
      .fill(null)
65
      .map(() => ({ value: "", modifiable: false }));
Yannick Li's avatar
Yannick Li committed
66
67
68
69
70
71
    this.modifiedCells = new Array(81).fill(null);
    const gridNum = "1";
    const mvmap = collection.open(
      "grid" + gridNum,
      "MVMap",
      false,
Yannick Li's avatar
Yannick Li committed
72
      this.handler.bind(this)
Yannick Li's avatar
Yannick Li committed
73
74
    );
    this.state = {
Yannick Li's avatar
Yannick Li committed
75
76
77
78
      gridNum: gridNum,
      mvmap: mvmap,
      cells: cells,
      isConnected: true,
Yannick Li's avatar
Yannick Li committed
79
80
    };
  }
81

Yannick Li's avatar
Yannick Li committed
82
83
84
85
  /**
   * Called after the component is rendered.
   * It set a timer to refresh cells values.
   */
Yannick Li's avatar
Yannick Li committed
86
  componentDidMount(): void {
Yannick Li's avatar
Yannick Li committed
87
    this.initFrom(generateStaticGrid(this.state.gridNum));
88
    this.setGetTimeout();
Yannick Li's avatar
Yannick Li committed
89
90
91
92
93
94
95
  }

  /**
   * Called when the compenent is about to be removed from the DOM.
   * It remove the timer set in componentDidMount().
   */
  componentWillUnmount(): void {
96
97
98
99
100
101
102
103
104
105
106
    clearInterval(this.timeoutGet);
  }

  /**
   * Get remote changes
   */
  private setGetTimeout() {
    this.timeoutGet = setTimeout(() => {
      collection.forceGet(this.state.mvmap);
      this.setGetTimeout();
    }, TIMEOUTGET);
Yannick Li's avatar
Yannick Li committed
107
108
  }

Yannick Li's avatar
Yannick Li committed
109
110
111
112
113
114
115
  /**
   * Handles update
   */
  private handler() {
    this.pullGrid();
  }

Yannick Li's avatar
Yannick Li committed
116
117
118
  /**
   * Update cells values from C-Client.
   */
119
  pullGrid(): void {
Yannick Li's avatar
Yannick Li committed
120
121
122
    if (!this.state.isConnected) {
      return;
    }
123
    clearTimeout(this.timeoutGet);
124
125
    const cells = this.state.cells;
    collection.pull(client.utils.ConsistencyLevel.None);
Yannick Li's avatar
Yannick Li committed
126
127
128
129
130
131
132
133
134
135
136
137
    for (let index = 0; index < 81; index++) {
      if (cells[index].modifiable) {
        cells[index].value = "";
      }
    }
    session.transaction(client.utils.ConsistencyLevel.None, () => {
      const itString = this.state.mvmap.iteratorString();
      while (itString.hasNext()) {
        const val = itString.next();
        cells[val.first].value = hashSetToString(val.second);
      }
    });
138
    this.setState({ cells: cells });
139
    this.setGetTimeout();
Yannick Li's avatar
Yannick Li committed
140
141
142
143
144
145
  }

  /**
   * This function is used to simulate the offline mode.
   */
  switchConnection(): void {
Yannick Li's avatar
Yannick Li committed
146
147
148
149
150
151
152
153
154
155
    this.setState({ isConnected: !this.state.isConnected }, () => {
      if (this.state.isConnected) {
        session.transaction(client.utils.ConsistencyLevel.None, () => {
          for (let index = 0; index < 81; index++) {
            if (
              this.state.cells[index].modifiable &&
              this.modifiedCells[index] !== null
            ) {
              this.state.mvmap.setString(index, this.modifiedCells[index]);
            }
Yannick Li's avatar
Yannick Li committed
156
          }
Yannick Li's avatar
Yannick Li committed
157
158
159
160
161
162
163
        });
        this.pullGrid();
      } else {
        clearInterval(this.timeoutGet);
        this.modifiedCells = new Array(81).fill(null);
      }
    });
Yannick Li's avatar
Yannick Li committed
164
165
166
167
168
169
170
171
172
173
174
175
176
  }

  /**
   * Initialize the grid with the given values.
   * @param values values to be set in the grid.
   */
  initFrom(values: string): void {
    assert.ok(values.length === 81);
    const cells = this.state.cells;
    for (let index = 0; index < 81; index++) {
      cells[index].value = values[index] === "." ? "" : values[index];
      cells[index].modifiable = values[index] === "." ? true : false;
    }
177
    this.setState({ cells: cells });
Yannick Li's avatar
Yannick Li committed
178
179
180
181
182
183
184
  }

  /**
   * Reset the value of all modifiable cells.
   */
  reset(): void {
    const cells = this.state.cells;
Yannick Li's avatar
Yannick Li committed
185
186
187
188
189
    session.transaction(client.utils.ConsistencyLevel.None, () => {
      for (let index = 0; index < 81; index++) {
        if (cells[index].modifiable) {
          cells[index].value = "";
          if (this.state.isConnected) {
Yannick Li's avatar
Yannick Li committed
190
            this.state.mvmap.setString(index, cells[index].value);
Yannick Li's avatar
Yannick Li committed
191
192
193
          } else {
            this.modifiedCells[index] = "";
          }
Yannick Li's avatar
Yannick Li committed
194
195
        }
      }
Yannick Li's avatar
Yannick Li committed
196
    });
197
    this.setState({ cells: cells });
Yannick Li's avatar
Yannick Li committed
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
  }

  /**
   * This handler is called when the value of a cell is changed.
   * @param index The index of the cell changed.
   * @param value The new value of the cell.
   */
  handleChange(index: number, value: string): void {
    assert.ok(value === "" || (Number(value) >= 1 && Number(value) <= 9));
    assert.ok(index >= 0 && index < 81);
    if (!this.state.cells[index].modifiable) {
      console.error(
        "Trying to change an non modifiable cell. Should not happend"
      );
    }

    const cells = this.state.cells;
    cells[index].value = value;
216
    this.setState({ cells: cells });
Yannick Li's avatar
Yannick Li committed
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

    if (this.state.isConnected) {
      session.transaction(client.utils.ConsistencyLevel.None, () => {
        this.state.mvmap.setString(index, value);
      });
    } else {
      this.modifiedCells[index] = value;
    }
  }

  /**
   * This handler is called when a new grid number is submit.
   * @param gridNum Desired grid number.
   */
  handleSubmit(gridNum: string): void {
    if (
      Number(gridNum) < 1 ||
      Number(gridNum) > 100 ||
      gridNum === this.state.gridNum
    ) {
      return;
    }
    const mvmap = collection.open(
      "grid" + gridNum,
      "MVMap",
      false,
Yannick Li's avatar
Yannick Li committed
243
      this.handler.bind(this)
Yannick Li's avatar
Yannick Li committed
244
245
246
247
    );
    this.setState({ gridNum: gridNum, mvmap: mvmap });
    this.initFrom(generateStaticGrid(gridNum));
  }
Yannick Li's avatar
Yannick Li committed
248

Yannick Li's avatar
Yannick Li committed
249
250
  render(): JSX.Element {
    return (
Yannick Li's avatar
Yannick Li committed
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
      <div className="sudoku">
        <div>Current grid : {this.state.gridNum}</div>
        <Submit1Input
          inputName="Grid"
          onSubmit={this.handleSubmit.bind(this)}
        />
        <div>
          Difficulty levels: easy (1-20), medium (21-40), hard (41-60),
          very-hard (61-80), insane (81-100)
        </div>
        <br />
        <div>
          <button onClick={this.reset.bind(this)}>Reset</button>
        </div>
        <br />
        <div>
          <button onClick={() => this.switchConnection()}>
            {this.state.isConnected ? "Disconnect" : "Connect"}
          </button>
        </div>
        <br />
Yannick Li's avatar
Yannick Li committed
272
273
274
275
276
277
        <Grid
          cells={this.state.cells}
          onChange={(index: number, value: string) =>
            this.handleChange(index, value)
          }
        />
Yannick Li's avatar
Yannick Li committed
278
      </div>
Yannick Li's avatar
Yannick Li committed
279
280
    );
  }
Yannick Li's avatar
Yannick Li committed
281
282
283
284
285
286
}

/**
 * Return a predefined Sudoku grid as a string.
 * @param gridNum Desired grid number
 */
Yannick Li's avatar
Yannick Li committed
287
function generateStaticGrid(gridNum: string) {
Yannick Li's avatar
Yannick Li committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
  return GRIDS[gridNum];
}

/**
 * Concatenates all values of a HashSet as a String.
 * @param set HashSet to be concatenated.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hashSetToString(set: any): string {
  const res = new Set();
  const it = set.iterator();
  while (it.hasNext()) {
    const val = it.next();
    if (val !== "") {
      res.add(val);
    }
  }
  return Array.from(res).sort().join(" ");
Yannick Li's avatar
Yannick Li committed
306
}
307

Yannick Li's avatar
Yannick Li committed
308
export default Game;