🎮 JavaScriptで作る！WEB制作会社経営シミュレーションゲーム開発入門 💻

# ブラウザで動く経営シミュレーションゲームの作り方 🎮

こんにちは、@YushiYamamotoです！今回は「ブラウザで動作する経営シミュレーションゲーム」の作り方について、初心者の方にも分かりやすく解説していきます。JavaScriptを使って、シンプルながらも奥深いゲーム開発の世界に飛び込んでみましょう！

<p class="codepen" data-height="1388" data-default-tab="result" data-slug-hash="azbwgKo" data-pen-title="ラク子のWEB制作会社" data-editable="true" data-user="yamamotoyushi" style="height: 1388px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yamamotoyushi/pen/azbwgKo">
  ラク子のWEB制作会社</a> by Yushi Yamamoto (<a href="https://codepen.io/yamamotoyushi">@yamamotoyushi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## 🌟 なぜ経営シミュレーションゲーム？

経営シミュレーションゲームは、プレイヤーが経営者となって会社やお店を運営するゲームです。資金管理、スタッフ雇用、設備投資など、現実の経営要素を取り入れつつ、ゲームとして楽しめる要素が満載です。

また、技術的な観点からも：
- オブジェクト指向プログラミングの練習に最適
- 状態管理の仕組みを学べる
- UIとロジックの分離を実践できる

## 📋 開発の流れ

ゲーム開発の基本的な流れは以下のようになります：

```mermaid
graph TD
    A[企画・コンセプト決定] --> B[仕様書作成]
    B --> C[開発環境準備]
    C --> D[基本システム実装]
    D --> E[UI実装]
    E --> F[ゲームバランス調整]
    F --> G[テスト]
    G --> H[公開]
```

今回は、「WEB制作会社経営シミュレーション」というテーマで進めていきます。プレイヤーはWEB制作会社の社長となり、クライアントから依頼を受けてWebサイトを制作し、会社を成長させていくゲームです。

## 💻 開発環境の準備

ブラウザゲーム開発に必要なものは非常にシンプルです：

- テキストエディタ（VSCode推奨）
- Webブラウザ（Chrome推奨）
- 基本的なHTML/CSS/JavaScript知識

特別なゲームエンジンは使わず、純粋なJavaScriptで実装していきます。これにより、基本的なプログラミングの概念を深く理解できます。

## 🏗️ ゲームの基本設計

まずは、ゲームの基本構造を考えましょう。経営シミュレーションゲームの核となる要素は以下の通りです：

1. **会社の状態管理**：資金、評判、所有設備など
2. **時間の経過**：日付進行システム
3. **プロジェクト管理**：依頼の受注、進行、完了
4. **スタッフ管理**：雇用、スキル成長、給与支払い
5. **設備投資**：機材購入、オフィス拡張

これらの要素をクラス図で表すと次のようになります：

```
+------------------+       +------------------+
|      Game        |       |     Project      |
+------------------+       +------------------+
| - company        || - id             |
| - gameTime       |       | - name           |
| - advanceDay()   |       | - budget         |
| - saveGame()     |       | - deadline       |
+------------------+       | - assignedStaff  |
         |                 +------------------+
         |
         v
+------------------+       +------------------+
|     Company      ||      Staff       |
+------------------+       +------------------+
| - name           |       | - id             |
| - funds          |       | - name           |
| - reputation     |       | - skills         |
| - staff          |       | - salary         |
| - projects       |       | - motivation     |
| - equipment      |       +------------------+
+------------------+
```

## 🚀 基本システムの実装

それでは、実際にコードを書いていきましょう。まずは、ゲームの基本クラスを作成します。


<details><summary>JS</summary>

```javascript
// ゲームのメインクラス
class WebDevTycoon {
  constructor() {
    // 会社の初期状態
    this.company = {
      name: "マイWEB制作会社",
      funds: 500000,  // 初期資金：50万円
      reputation: 50, // 評判：0-100
      staff: [],      // スタッフリスト
      projects: [],   // プロジェクトリスト
      equipment: [],  // 所有機材リスト
      office: {size: "small", rent: 50000} // オフィス情報
    };
    
    // ゲーム内時間
    this.gameTime = {
      day: 1,
      month: 4,
      year: 2025
    };
    
    // 初期スタッフ（プレイヤー自身）を追加
    this.company.staff.push({
      id: 1,
      name: "あなた",
      role: "社長",
      salary: 0,  // 社長なので給料なし
      skills: {
        design: 50,
        coding: 50,
        management: 50,
        communication: 60
      },
      motivation: 100
    });
  }
  
  // 日付を進める
  advanceDay() {
    this.gameTime.day++;
    
    // 月末処理
    if (this.gameTime.day > 30) {
      this.gameTime.day = 1;
      this.gameTime.month++;
      this.payMonthlyExpenses(); // 月次経費支払い
    }
    
    // 年末処理
    if (this.gameTime.month > 12) {
      this.gameTime.month = 1;
      this.gameTime.year++;
      this.annualReport(); // 年次レポート
    }
    
    // プロジェクトの進行
    this.updateProjects();
    
    // ランダムイベント発生
    this.generateRandomEvents();
  }
  
  // 月次経費の支払い
  payMonthlyExpenses() {
    // オフィス賃料
    this.company.funds -= this.company.office.rent;
    
    // スタッフの給料
    for (const staff of this.company.staff) {
      if (staff.salary > 0) { // プレイヤー以外のスタッフ
        this.company.funds -= staff.salary;
      }
    }
    
    // 設備維持費
    const equipmentMaintenance = this.company.equipment.length * 5000;
    this.company.funds -= equipmentMaintenance;
    
    return {
      rent: this.company.office.rent,
      salaries: this.company.staff.reduce((sum, staff) => sum + staff.salary, 0),
      maintenance: equipmentMaintenance
    };
  }
  
  // プロジェクトの更新
  updateProjects() {
    for (const project of this.company.projects) {
      if (!project.completed) {
        project.daysLeft--;
        
        // プロジェクトが期限切れになった場合
        if (project.daysLeft  p.id === projectId);
    if (!project) return { success: false, message: "プロジェクトが見つかりません。" };
    
    // 品質計算
    const quality = this.calculateProjectQuality(project);
    
    // 報酬と評判の更新
    this.company.funds += project.remainingPayment;
    
    // 品質に応じた評判変動
    let reputationChange = 0;
    if (quality > 80) {
      reputationChange = 5;
    } else if (quality > 50) {
      reputationChange = 2;
    } else {
      reputationChange = -3;
    }
    
    this.company.reputation = Math.max(0, Math.min(100, this.company.reputation + reputationChange));
    
    // プロジェクト完了フラグを立てる
    project.completed = true;
    
    // スタッフのスキルアップ
    for (const staffId of project.assignedStaff) {
      const staff = this.company.staff.find(s => s.id === staffId);
      if (staff) {
        // プロジェクトの種類に応じたスキルアップ
        for (const skill of project.requiredSkills) {
          if (staff.skills[skill]) {
            staff.skills[skill] = Math.min(100, staff.skills[skill] + 1);
          }
        }
      }
    }
    
    return {
      success: true,
      message: `${project.name}が完了しました！品質: ${quality}点、報酬: ${project.remainingPayment.toLocaleString()}円`,
      quality: quality,
      payment: project.remainingPayment,
      reputationChange: reputationChange
    };
  }
  
  // プロジェクトの品質を計算
  calculateProjectQuality(project) {
    // 基本品質
    let quality = 50;
    
    // スタッフのスキル影響
    const assignedStaff = project.assignedStaff || [];
    for (const staffId of assignedStaff) {
      const staff = this.company.staff.find(s => s.id === staffId);
      if (staff) {
        // プロジェクトに必要なスキルの平均値を計算
        let skillSum = 0;
        let skillCount = 0;
        
        for (const skill of project.requiredSkills) {
          if (staff.skills[skill]) {
            skillSum += staff.skills[skill];
            skillCount++;
          }
        }
        
        if (skillCount > 0) {
          const avgSkill = skillSum / skillCount;
          quality += avgSkill / assignedStaff.length;
        }
        
        // モチベーションの影響
        quality += (staff.motivation - 50) / 10;
      }
    }
    
    // 納期影響
    if (project.daysLeft > 5) {
      quality += 10; // 余裕があると品質アップ
    } else if (project.daysLeft 
```

</details>

<details><summary>HTML</summary>

### HTML
```html
  <div class="game-container">
    <div class="game-header">
      <div class="company-info">
        <h1 class="company-name" id="company-name">ラク子のWEB制作会社</h1>
        <div class="company-stats">
          <div class="stat-item">
            <span class="stat-label">資金</span>
            <span class="stat-value" id="funds">500,000円</span>
          </div>
          <div class="stat-item">
            <span class="stat-label">評判</span>
            <span class="stat-value" id="reputation">50</span>
          </div>
        </div>
      </div>
      <div class="game-date" id="date">2025年4月1日</div>
    </div>
    
    <div class="rakuko-area">
      <img src="https://rakuraku-site.prodouga.com/img/RakuRaku.avif" alt="ラク子" class="rakuko-image normal" id="rakuko-image">
      <div class="rakuko-message" id="rakuko-message">今日も頑張りましょう！</div>
      <button class="next-day-button" id="next-day-button">次の日へ</button>
    </div>
    
    <div class="game-main">
      <div class="action-buttons">
        <button class="action-button" id="hire-staff-button">スタッフを雇用</button>
        <button class="action-button" id="buy-equipment-button">機材を購入</button>
        <button class="action-button" id="upgrade-office-button">オフィスをアップグレード</button>
      </div>
      
      <div class="game-content">
        <div class="content-section projects-section">
          <h2 class="section-title">プロジェクト</h2>
          <ul class="projects-list" id="projects-list">
            <li class="empty-list">現在進行中のプロジェクトはありません</li>
          </ul>
        </div>
        
        <div class="content-section staff-section">
          <h2 class="section-title">スタッフ</h2>
          <ul class="staff-list" id="staff-list"></ul>
        </div>
        
        <div class="content-section equipment-section">
          <h2 class="section-title">機材</h2>
          <ul class="equipment-list" id="equipment-list"></ul>
        </div>
      </div>
      
      <div class="message-log" id="message-log"></div>
    </div>
  </div>
```

</details>

<details><summary>CSS</summary>

### CSS
```CSS
    /* ゲーム全体のスタイル */
    body {
      font-family: 'Hiragino Kaku Gothic Pro', 'メイリオ', sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f5f5f5;
      color: #333;
    }
    
    .game-container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
      display: grid;
      grid-template-columns: 300px 1fr;
      grid-template-rows: auto 1fr;
      gap: 20px;
      height: 100vh;
    }
    
    /* ヘッダー部分 */
    .game-header {
      grid-column: 1 / 3;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
    
    .company-info {
      display: flex;
      flex-direction: column;
    }
    
    .company-name {
      font-size: 24px;
      font-weight: bold;
      margin: 0;
    }
    
    .company-stats {
      display: flex;
      gap: 20px;
    }
    
    .stat-item {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .stat-label {
      font-size: 12px;
      color: #666;
    }
    
    .stat-value {
      font-size: 18px;
      font-weight: bold;
    }
    
    .game-date {
      padding: 5px 10px;
      background-color: #f0f0f0;
      border-radius: 4px;
      font-size: 14px;
    }
    
    /* ラク子エリア */
    .rakuko-area {
      grid-column: 1;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    .rakuko-image {
      width: 200px;
      height: 200px;
      object-fit: cover;
      border-radius: 50%;
      margin-bottom: 15px;
      transition: all 0.3s ease;
    }
    
    .rakuko-image.happy {
      border: 5px solid #4CAF50;
    }
    
    .rakuko-image.normal {
      border: 5px solid #FFC107;
    }
    
    .rakuko-image.sad {
      border: 5px solid #F44336;
    }
    
    .rakuko-message {
      font-size: 16px;
      text-align: center;
      margin-bottom: 20px;
      min-height: 48px;
    }
    
    .next-day-button {
      padding: 10px 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .next-day-button:hover {
      background-color: #45a049;
    }
    
    /* ゲームメインエリア */
    .game-main {
      grid-column: 2;
      display: grid;
      grid-template-rows: auto 1fr;
      gap: 20px;
    }
    
    .action-buttons {
      display: flex;
      gap: 10px;
    }
    
    .action-button {
      padding: 10px 15px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 14px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .action-button:hover {
      background-color: #0b7dda;
    }
    
    .game-content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      grid-template-rows: 1fr 1fr;
      gap: 20px;
    }
    
    .content-section {
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      overflow-y: auto;
    }
    
    .section-title {
      font-size: 18px;
      font-weight: bold;
      margin-top: 0;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #eee;
    }
    
    /* プロジェクトリスト */
    .projects-section {
      grid-column: 1 / 3;
      grid-row: 1;
    }
    
    .projects-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .project-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .project-header {
      padding: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .project-header.normal {
      background-color: #E3F2FD;
    }
    
    .project-header.urgent {
      background-color: #FFF3E0;
    }
    
    .project-header.overdue {
      background-color: #FFEBEE;
    }
    
    .project-header.completed {
      background-color: #E8F5E9;
    }
    
    .project-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .project-client {
      font-size: 12px;
      color: #666;
    }
    
    .project-details {
      padding: 10px;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
    }
    
    .project-details p {
      margin: 0;
      font-size: 14px;
    }
    
    .project-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .project-actions button {
      padding: 5px 10px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .project-actions button:hover {
      background-color: #0b7dda;
    }
    
    .status-badge {
      padding: 5px 10px;
      border-radius: 4px;
      font-size: 12px;
    }
    
    .status-badge.completed {
      background-color: #4CAF50;
      color: white;
    }
    
    .empty-list {
      padding: 20px;
      text-align: center;
      color: #999;
    }
    
    /* スタッフリスト */
    .staff-section {
      grid-column: 1;
      grid-row: 2;
    }
    
    .staff-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .staff-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .staff-header {
      padding: 10px;
      background-color: #E8EAF6;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .staff-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .staff-role {
      font-size: 12px;
      color: #666;
    }
    
    .staff-details {
      padding: 10px;
    }
    
    .staff-details p {
      margin: 5px 0;
      font-size: 14px;
    }
    
    .staff-skills {
      margin: 10px 0;
    }
    
    .skill-bar {
      display: flex;
      align-items: center;
      margin-bottom: 5px;
    }
    
    .skill-name {
      width: 100px;
      font-size: 12px;
    }
    
    .skill-value-bar {
      flex-grow: 1;
      height: 8px;
      background-color: #eee;
      border-radius: 4px;
      overflow: hidden;
      margin: 0 10px;
    }
    
    .skill-value {
      height: 100%;
      background-color: #2196F3;
    }
    
    .skill-value-text {
      font-size: 12px;
      width: 30px;
      text-align: right;
    }
    
    .motivation {
      font-weight: bold;
    }
    
    .motivation-high {
      color: #4CAF50;
    }
    
    .motivation-medium {
      color: #FFC107;
    }
    
    .motivation-low {
      color: #F44336;
    }
    
    .staff-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .staff-actions button {
      padding: 5px 10px;
      background-color: #673AB7;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .staff-actions button:hover {
      background-color: #5e35b1;
    }
    
    /* 機材リスト */
    .equipment-section {
      grid-column: 2;
      grid-row: 2;
    }
    
    .equipment-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .equipment-item {
      margin-bottom: 15px;
      border: 1px solid #eee;
      border-radius: 4px;
      overflow: hidden;
    }
    
    .equipment-header {
      padding: 10px;
      background-color: #E0F7FA;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .equipment-header h3 {
      margin: 0;
      font-size: 16px;
    }
    
    .equipment-details {
      padding: 10px;
    }
    
    .equipment-details p {
      margin: 5px 0;
      font-size: 14px;
    }
    
    .condition {
      font-weight: bold;
    }
    
    .condition-good {
      color: #4CAF50;
    }
    
    .condition-fair {
      color: #FFC107;
    }
    
    .condition-poor {
      color: #F44336;
    }
    
    .equipment-actions {
      padding: 10px;
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      background-color: #f9f9f9;
    }
    
    .equipment-actions button {
      padding: 5px 10px;
      background-color: #00BCD4;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .equipment-actions button:hover {
      background-color: #00acc1;
    }
    
    /* メッセージログ */
    .message-log {
      grid-column: 1 / 3;
      background-color: #fff;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      max-height: 200px;
      overflow-y: auto;
    }
    
    .message {
      padding: 5px 10px;
      margin-bottom: 5px;
      border-radius: 4px;
      font-size: 14px;
    }
    
    .message.info {
      background-color: #E3F2FD;
    }
    
    .message.warning {
      background-color: #FFF3E0;
    }
    
    .message.error {
      background-color: #FFEBEE;
    }
    
    /* ダイアログ */
    .dialog {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }
    
    .dialog-content {
      background-color: #fff;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 5px 15px rgba(0,0,0,0.3);
      max-width: 80%;
      max-height: 80%;
      overflow-y: auto;
    }
    
    .dialog-content h2 {
      margin-top: 0;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #eee;
    }
    
    .dialog-staff-list,
    .dialog-skill-list,
    .dialog-hire-list,
    .dialog-equipment-list,
    .dialog-office-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .dialog-staff-item,
    .dialog-skill-item,
    .dialog-hire-item,
    .dialog-equipment-item,
    .dialog-office-item {
      margin-bottom: 15px;
      padding: 10px;
      border: 1px solid #eee;
      border-radius: 4px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .dialog-staff-item.assigned {
      background-color: #E8F5E9;
    }
    
    .dialog-office-item.current {
      background-color: #E8F5E9;
    }
    
    .dialog-actions {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      margin-top: 20px;
    }
    
    .dialog-actions button {
      padding: 10px 20px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 14px;
      cursor: pointer;
    }
    
    .dialog-actions button:hover {
      background-color: #0b7dda;
    }
    
    .close-dialog-button {
      background-color: #9E9E9E !important;
    }
    
    .close-dialog-button:hover {
      background-color: #757575 !important;
    }
    
    /* レスポンシブデザイン */
    @media (max-width: 1024px) {
      .game-container {
        grid-template-columns: 1fr;
      }
      
      .game-header {
        grid-column: 1;
      }
      
      .rakuko-area {
        grid-column: 1;
      }
      
      .game-main {
        grid-column: 1;
      }
      
      .game-content {
        grid-template-columns: 1fr;
      }
      
      .projects-section {
        grid-column: 1;
      }
      
      .staff-section,
      .equipment-section {
        grid-column: 1;
      }
    }

```
</details>


## 🔄 ゲームループの実装

ゲームの動作を制御するために、HTMLとJavaScriptを連携させます。以下は、UIの更新とゲームの進行を管理するコードです。

```javascript

// HTMLとの連携用コード
document.addEventListener('DOMContentLoaded', function() {
  // ゲームインスタンスの作成
  const game = new WebDevTycoon();
  
  // UI要素の取得
  const companyNameElement = document.getElementById('company-name');
  const fundsElement = document.getElementById('funds');
  const reputationElement = document.getElementById('reputation');
  const dateElement = document.getElementById('date');
  const characterMessageElement = document.getElementById('character-message');
  const projectsListElement = document.getElementById('projects-list');
  const staffListElement = document.getElementById('staff-list');
  const equipmentListElement = document.getElementById('equipment-list');
  const nextDayButton = document.getElementById('next-day-button');
  const messageLogElement = document.getElementById('message-log');
  
  // ゲーム状態の更新
  function updateUI() {
    // 基本情報の更新
    companyNameElement.textContent = game.company.name;
    fundsElement.textContent = game.company.funds.toLocaleString() + '円';
    reputationElement.textContent = game.company.reputation;
    dateElement.textContent = `${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日`;
    
    // プロジェクトリストの更新
    updateProjectsList(game.company.projects);
    
    // スタッフリストの更新
    updateStaffList(game.company.staff);
    
    // 機材リストの更新
    updateEquipmentList(game.company.equipment);
  }
  
  // 次の日へボタンのイベントリスナー
  nextDayButton.addEventListener('click', function() {
    const result = game.advanceDay();
    addMessageToLog(`${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日になりました。`);
    
    // ランダムイベントの処理
    const event = game.generateRandomEvents();
    if (event) {
      addMessageToLog(event.message);
      
      if (event.type === 'inquiry') {
        showNewProjectDialog(event.project);
      }
    }
    
    updateUI();
  });
  
  // 初期UI更新
  updateUI();
  
  // 初期メッセージ
  addMessageToLog('ゲームを開始しました。WEB制作会社経営へようこそ！');
});
```

## 🧩 主要機能の実装

ここからは、ゲームの主要機能を一つずつ実装していきます。

### 1. プロジェクト管理システム

プロジェクト管理は経営シミュレーションの核となる部分です。プロジェクトの受注から完了までの流れを実装します。

```javascript

// プロジェクトリストの更新
function updateProjectsList(projects) {
  projectsListElement.innerHTML = '';
  
  if (projects.length === 0) {
    projectsListElement.innerHTML = '現在進行中のプロジェクトはありません';
    return;
  }
  
  for (const project of projects) {
    const projectElement = document.createElement('li');
    projectElement.className = 'project-item';
    
    const statusClass = project.completed ? 'completed' : 
                       (project.daysLeft 
        ${project.name}
        ${project.client}
      
      
        予算: ${project.budget.toLocaleString()}円
        残り日数: ${project.daysLeft}日
        難易度: ${getDifficultyText(project.difficulty)}
        担当者: ${getAssignedStaffNames(project.assignedStaff)}
      
      
        ${project.completed ? 
          '完了' : 
          `スタッフ割当
           納品する`
        }
      
    `;
    
    projectsListElement.appendChild(projectElement);
  }
  
  // イベントリスナーの追加
  document.querySelectorAll('.assign-staff-button').forEach(button => {
    button.addEventListener('click', function() {
      const projectId = parseInt(this.getAttribute('data-project-id'));
      showAssignStaffDialog(projectId);
    });
  });
}
```

### 2. スタッフ管理システム

会社の成長には優秀なスタッフが欠かせません。スタッフの雇用、育成、管理機能を実装します。

```javascript
// スタッフを雇用
function hireStaff(staffType) {
  const staffTypes = {
    "デザイナー": {
      baseSalary: 250000,
      skills: { design: 70, coding: 20, management: 30, communication: 50 },
      hireCost: 100000
    },
    "エンジニア": {
      baseSalary: 300000,
      skills: { design: 30, coding: 80, management: 40, communication: 40 },
      hireCost: 150000
    },
    "ディレクター": {
      baseSalary: 350000,
      skills: { design: 40, coding: 30, management: 70, communication: 60 },
      hireCost: 200000
    }
  };
  
  const template = staffTypes[staffType];
  if (!template) return { success: false, message: "指定されたスタッフタイプが見つかりません。" };
  
  if (game.company.funds  s.id === staffId);
    if (staff) {
      // プロジェクトに必要なスキルの平均値を計算
      let skillSum = 0;
      let skillCount = 0;
      
      for (const skill of project.requiredSkills) {
        if (staff.skills[skill]) {
          skillSum += staff.skills[skill];
          skillCount++;
        }
      }
      
      if (skillCount > 0) {
        const avgSkill = skillSum / skillCount;
        // スタッフのスキルの影響を大きくする
        quality += (avgSkill / assignedStaff.length) * 1.5;
      }
      
      // モチベーションの影響も大きくする
      quality += (staff.motivation - 50) / 5;
    }
  }
  
  // 設備の影響（新たに追加）
  const relevantEquipment = game.company.equipment.filter(eq => 
    eq.name.includes("PC") || 
    (project.requiredSkills.includes("design") && eq.name.includes("タブレット"))
  );
  
  if (relevantEquipment.length > 0) {
    const avgEquipmentPerformance = relevantEquipment.reduce((sum, eq) => sum + eq.performance, 0) / relevantEquipment.length;
    quality += (avgEquipmentPerformance - 50) / 4;
  } else {
    // 必要な機材がない場合はペナルティ
    quality -= 10;
  }
  
  // 納期影響（調整）
  if (project.daysLeft > project.duration * 0.3) {
    quality += 15; // 余裕があると品質アップ（より大きく）
  } else if (project.daysLeft < 0) {
    quality -= 25; // 納期遅れは大幅減点（より厳しく）
  }
  
  // 最終調整
  return Math.max(0, Math.min(100, Math.floor(quality)));
}
```

## 🎮 ゲームの完成と拡張

基本的なゲームシステムが完成したら、以下のような拡張機能を追加して、ゲームをより面白くすることができます：

1. **セーブ＆ロード機能**：LocalStorageを使ってゲーム状態を保存
2. **実績システム**：特定の条件を達成すると解除される実績
3. **ランダムイベント**：予期せぬ出来事が発生するシステム
4. **競合他社**：AIが操作する競合会社との競争
5. **特殊プロジェクト**：稀に出現する高難度・高報酬のプロジェクト

例えば、セーブ＆ロード機能は以下のように実装できます：

```javascript

// ゲームの状態を保存
function saveGame() {
  const saveData = {
    company: game.company,
    gameTime: game.gameTime,
    version: "1.0.0" // バージョン管理用
  };
  
  localStorage.setItem('webDevTycoonSave', JSON.stringify(saveData));
  return { success: true, message: "ゲームを保存しました。" };
}

// ゲームの状態を読み込み
function loadGame() {
  const saveData = localStorage.getItem('webDevTycoonSave');
  if (!saveData) return { success: false, message: "セーブデータが見つかりません。" };
  
  try {
    const parsedData = JSON.parse(saveData);
    
    // バージョンチェック（将来的な互換性のため）
    if (parsedData.version !== "1.0.0") {
      return { success: false, message: "互換性のないセーブデータです。" };
    }
// データの復元
    game.company = parsedData.company;
    game.gameTime = parsedData.gameTime;
    
    updateUI();
    return { success: true, message: "ゲームを読み込みました。" };
  } catch (error) {
    return { success: false, message: "セーブデータの読み込みに失敗しました。" };
  }
}
```

## 📱 レスポンシブデザインの実装

現代のWebアプリケーションには、レスポンシブデザインが不可欠です。様々な画面サイズに対応するためのCSSを追加しましょう。

```css
/* レスポンシブデザイン */
@media (max-width: 1024px) {
  .game-container {
    grid-template-columns: 1fr;
  }
  
  .game-header {
    grid-column: 1;
  }
  
  .side-panel {
    grid-column: 1;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
  
  .character-area {
    display: flex;
    align-items: center;
  }
  
  .character-image {
    width: 80px;
    height: 80px;
    margin-right: 15px;
  }
  
  .game-main {
    grid-column: 1;
  }
  
  .game-content {
    grid-template-columns: 1fr;
  }
  
  .projects-section,
  .staff-section,
  .equipment-section {
    grid-column: 1;
  }
}

@media (max-width: 600px) {
  .game-header {
    flex-direction: column;
    align-items: flex-start;
  }
  
  .company-stats {
    margin-top: 10px;
  }
  
  .game-date {
    margin-top: 10px;
    align-self: flex-end;
  }
  
  .action-buttons {
    flex-direction: column;
    gap: 5px;
  }
  
  .project-details,
  .staff-details {
    grid-template-columns: 1fr;
  }
}
```

## 🔍 デバッグとテスト

ゲーム開発では、デバッグとテストが非常に重要です。以下のようなテスト関数を作成して、ゲームの各機能をテストしましょう。

```javascript
// テスト用関数
function runGameTests() {
  console.log("=== ゲームテスト開始 ===");
  
  // 1. 日付進行テスト
  console.log("1. 日付進行テスト");
  const initialDate = { ...game.gameTime };
  game.advanceDay();
  console.assert(
    game.gameTime.day === initialDate.day + 1 || 
    (initialDate.day === 30 && game.gameTime.day === 1),
    "日付進行に問題があります"
  );
  
  // 2. プロジェクト生成テスト
  console.log("2. プロジェクト生成テスト");
  const newProject = game.generateNewProject();
  console.assert(
    newProject && newProject.name && newProject.budget > 0,
    "プロジェクト生成に問題があります"
  );
  
  // 3. プロジェクト受注テスト
  console.log("3. プロジェクト受注テスト");
  const initialFunds = game.company.funds;
  const initialProjectCount = game.company.projects.length;
  const acceptResult = game.acceptProject(newProject);
  console.assert(
    acceptResult.success && 
    game.company.funds === initialFunds + newProject.advancePayment &&
    game.company.projects.length === initialProjectCount + 1,
    "プロジェクト受注に問題があります"
  );
  
  // 4. スタッフ割り当てテスト
  console.log("4. スタッフ割り当てテスト");
  const projectId = game.company.projects.id;
  const staffId = game.company.staff.id;
  const assignResult = game.assignStaffToProject(projectId, staffId);
  console.assert(
    assignResult.success && 
    game.company.projects.assignedStaff.includes(staffId),
    "スタッフ割り当てに問題があります"
  );
  
  // 5. プロジェクト完了テスト
  console.log("5. プロジェクト完了テスト");
  const projectToComplete = game.company.projects;
  projectToComplete.daysLeft = 0; // 強制的に納期を到来させる
  const completeResult = game.completeProject(projectToComplete.id);
  console.assert(
    completeResult.success && 
    projectToComplete.completed,
    "プロジェクト完了に問題があります"
  );
  
  console.log("=== テスト完了 ===");
}
```

## 🚀 パフォーマンス最適化

ブラウザゲームでは、パフォーマンスも重要な要素です。特に、UIの更新処理は効率的に行う必要があります。

```javascript

// パフォーマンス最適化版のUI更新
function optimizedUpdateUI() {
  // 変更があった部分だけを更新する
  
  // 1. 基本情報の更新（変更があった場合のみ）
  if (companyNameElement.textContent !== game.company.name) {
    companyNameElement.textContent = game.company.name;
  }
  
  const formattedFunds = game.company.funds.toLocaleString() + '円';
  if (fundsElement.textContent !== formattedFunds) {
    fundsElement.textContent = formattedFunds;
  }
  
  if (reputationElement.textContent != game.company.reputation) {
    reputationElement.textContent = game.company.reputation;
  }
  
  const formattedDate = `${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日`;
  if (dateElement.textContent !== formattedDate) {
    dateElement.textContent = formattedDate;
  }
  
  // 2. リストの更新（差分だけ更新）
  updateProjectsListOptimized(game.company.projects);
  updateStaffListOptimized(game.company.staff);
  updateEquipmentListOptimized(game.company.equipment);
}

// プロジェクトリストの最適化更新
function updateProjectsListOptimized(projects) {
  // 既存のプロジェクト要素を取得
  const existingProjects = Array.from(projectsListElement.children).filter(
    el => el.classList.contains('project-item')
  );
  
  // 空リストの表示/非表示を切り替え
  const emptyListElement = projectsListElement.querySelector('.empty-list');
  if (projects.length === 0) {
    if (!emptyListElement) {
      const emptyEl = document.createElement('li');
      emptyEl.className = 'empty-list';
      emptyEl.textContent = '現在進行中のプロジェクトはありません';
      projectsListElement.appendChild(emptyEl);
    }
    // 既存のプロジェクト要素をすべて削除
    existingProjects.forEach(el => el.remove());
    return;
  } else if (emptyListElement) {
    emptyListElement.remove();
  }
  
  // 既存のプロジェクトIDを取得
  const existingProjectIds = existingProjects.map(
    el => parseInt(el.querySelector('button')?.getAttribute('data-project-id') || '0')
  );
  
  // 新しいプロジェクトを追加
  for (const project of projects) {
    const existingIndex = existingProjectIds.indexOf(project.id);
    
    if (existingIndex === -1) {
      // 新規プロジェクトの場合、要素を作成して追加
      const projectElement = createProjectElement(project);
      projectsListElement.appendChild(projectElement);
    } else {
      // 既存プロジェクトの場合、内容を更新
      updateExistingProjectElement(existingProjects[existingIndex], project);
    }
  }
  
  // 削除されたプロジェクトの要素を削除
  const currentProjectIds = projects.map(p => p.id);
  existingProjects.forEach((el, index) => {
    const projectId = existingProjectIds[index];
    if (!currentProjectIds.includes(projectId)) {
      el.remove();
    }
  });
}
```

## 🎯 実践的なゲーム開発のポイント

最後に、ブラウザゲーム開発で重要なポイントをいくつか紹介します：

### 1. 状態管理の設計

ゲームの状態管理は非常に重要です。今回は単純なオブジェクトで管理しましたが、規模が大きくなる場合は以下の方法も検討してください：

- **Reduxパターン**：状態の一元管理と予測可能な更新
- **イミュータブルデータ構造**：状態の変更を追跡しやすくする
- **オブザーバーパターン**：状態変更を監視して自動的にUIを更新

### 2. モジュール化と再利用性

コードの再利用性を高めるために、機能ごとにモジュール化することが重要です：

```javascript
// モジュール化の例
const GameTimeModule = {
  advanceDay(gameTime, callbacks) {
    gameTime.day++;
    
    if (gameTime.day > 30) {
      gameTime.day = 1;
      gameTime.month++;
      
      if (callbacks.onMonthEnd) {
        callbacks.onMonthEnd();
      }
    }
    
    if (gameTime.month > 12) {
      gameTime.month = 1;
      gameTime.year++;
      
      if (callbacks.onYearEnd) {
        callbacks.onYearEnd();
      }
    }
    
    return gameTime;
  }
};

const ProjectModule = {
  generateNewProject(companyReputation, templates) {
    // プロジェクト生成ロジック
  },
  
  calculateQuality(project, assignedStaff, equipment) {
    // 品質計算ロジック
  }
};
```

### 3. ローカライゼーション対応

国際的なユーザーに対応するために、ローカライゼーション（多言語対応）を考慮することも重要です：

```javascript
// 言語ファイルの例
const translations = {
  ja: {
    next_day: "次の日へ",
    hire_staff: "スタッフを雇用",
    buy_equipment: "機材を購入",
    upgrade_office: "オフィスをアップグレード",
    // ...
  },
  en: {
    next_day: "Next Day",
    hire_staff: "Hire Staff",
    buy_equipment: "Buy Equipment",
    upgrade_office: "Upgrade Office",
    // ...
  }
};

// 言語切り替え関数
function setLanguage(lang) {
  const texts = translations[lang] || translations.en;
  
  // UI要素のテキストを更新
  document.getElementById('next-day-button').textContent = texts.next_day;
  document.getElementById('hire-staff-button').textContent = texts.hire_staff;
  document.getElementById('buy-equipment-button').textContent = texts.buy_equipment;
  document.getElementById('upgrade-office-button').textContent = texts.upgrade_office;
  // ...
}
```

## 📝 まとめ

今回は、JavaScriptを使ったブラウザゲーム開発の基本を解説しました。経営シミュレーションゲームは、オブジェクト指向プログラミングの練習に最適であり、状態管理やUI設計など、Webアプリケーション開発の多くの要素を学ぶことができます。

このゲームはシンプルな構造ですが、拡張性が高いため、様々な機能を追加してより複雑で面白いゲームに発展させることができます。例えば、マーケティング要素の追加、スタッフ間の相性システム、技術ツリーの実装など、アイデア次第で無限に拡張可能です。

![スクリーンショット 2025-03-08 21.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637864/6f820a3b-d28b-4d88-857f-4edef7753cb6.png)


ぜひ、このコードをベースに自分だけのオリジナルゲームを作ってみてください！プログラミングの楽しさを実感できるはずです。

次回は、このゲームにアニメーションやサウンドを追加して、よりリッチな体験を提供する方法について解説する予定です。お楽しみに！

## 🔗 参考リソース

- [MDN Web Docs - JavaScript ガイド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide)
- [HTML5 Game Development](https://www.w3schools.com/graphics/game_intro.asp)
- [JavaScript Design Patterns](https://addyosmani.com/resources/essentialjsdesignpatterns/book/)

---

この記事が気に入ったら、ぜひコメントやシェアをお願いします！また、質問や感想も大歓迎です。一緒にゲーム開発の世界を楽しみましょう！

---

### 最後に：業務委託のご相談を承ります

私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう！

👉 [ポートフォリオ](https://yushiyamamoto.vercel.app/)

🌳 [らくらくサイト](https://rakuraku-site.prodouga.com/)